xref: /aosp_15_r20/external/pigweed/pw_console/py/pw_console/log_line.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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