1# Copyright 2021 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""LogLine storage class.""" 15 16import logging 17from dataclasses import dataclass 18from datetime import datetime 19from typing import Dict, Optional 20 21from prompt_toolkit.formatted_text import ANSI, StyleAndTextTuples 22 23from pw_log_tokenized import FormatStringWithMetadata 24 25 26@dataclass 27class LogLine: 28 """Class to hold a single log event.""" 29 30 record: logging.LogRecord 31 formatted_log: str 32 ansi_stripped_log: str 33 34 def __post_init__(self): 35 self.metadata = None 36 self.fragment_cache = None 37 38 def time(self): 39 """Return a datetime object for the log record.""" 40 return datetime.fromtimestamp(self.record.created) 41 42 def update_metadata(self, extra_fields: Optional[Dict] = None): 43 """Parse log metadata fields from various sources.""" 44 45 # 1. Parse any metadata from the message itself. 46 self.metadata = FormatStringWithMetadata( 47 str(self.record.message) # pylint: disable=no-member 48 ) # pylint: disable=no-member 49 self.formatted_log = self.formatted_log.replace( 50 self.metadata.raw_string, self.metadata.message 51 ) 52 # Remove any trailing line breaks. 53 self.formatted_log = self.formatted_log.rstrip() 54 55 # 2. Check for a metadata Dict[str, str] stored in the log record in the 56 # `extra_metadata_fields` attribute. This should be set using the 57 # extra={} kwarg. For example: 58 # LOGGER.log( 59 # level, 60 # '%s', 61 # message, 62 # extra=dict( 63 # extra_metadata_fields={ 64 # 'Field1': 'Value1', 65 # 'Field2': 'Value2', 66 # })) 67 # See: 68 # https://docs.python.org/3/library/logging.html#logging.debug 69 if hasattr(self.record, 'extra_metadata_fields') and ( 70 self.record.extra_metadata_fields # type: ignore # pylint: disable=no-member 71 ): 72 fields = self.record.extra_metadata_fields # type: ignore # pylint: disable=no-member 73 for key, value in fields.items(): 74 self.metadata.fields[key] = value 75 76 # 3. Check for additional passed in metadata. 77 if extra_fields: 78 for key, value in extra_fields.items(): 79 self.metadata.fields[key] = value 80 81 lineno = self.record.lineno 82 file_name = str(self.record.filename) 83 self.metadata.fields['py_file'] = f'{file_name}:{lineno}' 84 self.metadata.fields['py_logger'] = str(self.record.name) 85 86 return self.metadata 87 88 def get_fragments(self) -> StyleAndTextTuples: 89 """Return this log line as a list of FormattedText tuples.""" 90 # Parse metadata if any. 91 if self.metadata is None: 92 self.update_metadata() 93 94 # Create prompt_toolkit FormattedText tuples based on the log ANSI 95 # escape sequences. 96 if self.fragment_cache is None: 97 self.fragment_cache = ANSI( 98 self.formatted_log + '\n' # Add a trailing linebreak 99 ).__pt_formatted_text__() 100 101 return self.fragment_cache 102