• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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