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 19 20from prompt_toolkit.formatted_text import ANSI, StyleAndTextTuples 21 22from pw_log_tokenized import FormatStringWithMetadata 23 24 25@dataclass 26class LogLine: 27 """Class to hold a single log event.""" 28 29 record: logging.LogRecord 30 formatted_log: str 31 ansi_stripped_log: str 32 33 def __post_init__(self): 34 self.metadata = None 35 self.fragment_cache = None 36 37 def time(self): 38 """Return a datetime object for the log record.""" 39 return datetime.fromtimestamp(self.record.created) 40 41 def update_metadata(self, extra_fields: dict | None = None): 42 """Parse log metadata fields from various sources.""" 43 44 # 1. Parse any metadata from the message itself. 45 self.metadata = FormatStringWithMetadata( 46 str(self.record.message) # pylint: disable=no-member 47 ) # pylint: disable=no-member 48 self.formatted_log = self.formatted_log.replace( 49 self.metadata.raw_string, self.metadata.message 50 ) 51 # Remove any trailing line breaks. 52 self.formatted_log = self.formatted_log.rstrip() 53 54 # 2. Check for a metadata dict[str, str] stored in the log record in the 55 # `extra_metadata_fields` attribute. This should be set using the 56 # extra={} kwarg. For example: 57 # LOGGER.log( 58 # level, 59 # '%s', 60 # message, 61 # extra=dict( 62 # extra_metadata_fields={ 63 # 'Field1': 'Value1', 64 # 'Field2': 'Value2', 65 # })) 66 # See: 67 # https://docs.python.org/3/library/logging.html#logging.debug 68 if hasattr(self.record, 'extra_metadata_fields') and ( 69 self.record.extra_metadata_fields # type: ignore # pylint: disable=no-member 70 ): 71 fields = self.record.extra_metadata_fields # type: ignore # pylint: disable=no-member 72 for key, value in fields.items(): 73 self.metadata.fields[key] = value 74 75 # 3. Check for additional passed in metadata. 76 if extra_fields: 77 for key, value in extra_fields.items(): 78 self.metadata.fields[key] = value 79 80 lineno = self.record.lineno 81 file_name = str(self.record.filename) 82 self.metadata.fields['py_file'] = f'{file_name}:{lineno}' 83 self.metadata.fields['py_logger'] = str(self.record.name) 84 85 return self.metadata 86 87 def get_fragments(self) -> StyleAndTextTuples: 88 """Return this log line as a list of FormattedText tuples.""" 89 # Parse metadata if any. 90 if self.metadata is None: 91 self.update_metadata() 92 93 # Create prompt_toolkit FormattedText tuples based on the log ANSI 94 # escape sequences. 95 if self.fragment_cache is None: 96 self.fragment_cache = ANSI( 97 self.formatted_log + '\n' # Add a trailing linebreak 98 ).__pt_formatted_text__() 99 100 return self.fragment_cache 101