xref: /dpdk/dts/framework/logger.py (revision bd4a5aa413583aa698f10849c4784a3d524566bc)
1# SPDX-License-Identifier: BSD-3-Clause
2# Copyright(c) 2010-2014 Intel Corporation
3# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
4# Copyright(c) 2022-2023 University of New Hampshire
5
6"""DTS logger module.
7
8The module provides several additional features:
9
10    * The storage of DTS execution stages,
11    * Logging to console, a human-readable log file and a machine-readable log file,
12    * Optional log files for specific stages.
13"""
14
15import logging
16from enum import auto
17from logging import FileHandler, StreamHandler
18from pathlib import Path
19from typing import ClassVar
20
21from .utils import StrEnum
22
23date_fmt = "%Y/%m/%d %H:%M:%S"
24stream_fmt = "%(asctime)s - %(stage)s - %(name)s - %(levelname)s - %(message)s"
25dts_root_logger_name = "dts"
26
27
28class DtsStage(StrEnum):
29    """The DTS execution stage."""
30
31    #:
32    pre_execution = auto()
33    #:
34    execution_setup = auto()
35    #:
36    execution_teardown = auto()
37    #:
38    build_target_setup = auto()
39    #:
40    build_target_teardown = auto()
41    #:
42    test_suite_setup = auto()
43    #:
44    test_suite = auto()
45    #:
46    test_suite_teardown = auto()
47    #:
48    post_execution = auto()
49
50
51class DTSLogger(logging.Logger):
52    """The DTS logger class.
53
54    The class extends the :class:`~logging.Logger` class to add the DTS execution stage information
55    to log records. The stage is common to all loggers, so it's stored in a class variable.
56
57    Any time we switch to a new stage, we have the ability to log to an additional log file along
58    with a supplementary log file with machine-readable format. These two log files are used until
59    a new stage switch occurs. This is useful mainly for logging per test suite.
60    """
61
62    _stage: ClassVar[DtsStage] = DtsStage.pre_execution
63    _extra_file_handlers: list[FileHandler] = []
64
65    def __init__(self, *args, **kwargs):
66        """Extend the constructor with extra file handlers."""
67        self._extra_file_handlers = []
68        super().__init__(*args, **kwargs)
69
70    def makeRecord(self, *args, **kwargs) -> logging.LogRecord:
71        """Generates a record with additional stage information.
72
73        This is the default method for the :class:`~logging.Logger` class. We extend it
74        to add stage information to the record.
75
76        :meta private:
77
78        Returns:
79            record: The generated record with the stage information.
80        """
81        record = super().makeRecord(*args, **kwargs)
82        record.stage = DTSLogger._stage  # type: ignore[attr-defined]
83        return record
84
85    def add_dts_root_logger_handlers(self, verbose: bool, output_dir: str) -> None:
86        """Add logger handlers to the DTS root logger.
87
88        This method should be called only on the DTS root logger.
89        The log records from child loggers will propagate to these handlers.
90
91        Three handlers are added:
92
93            * A console handler,
94            * A file handler,
95            * A supplementary file handler with machine-readable logs
96              containing more debug information.
97
98        All log messages will be logged to files. The log level of the console handler
99        is configurable with `verbose`.
100
101        Args:
102            verbose: If :data:`True`, log all messages to the console.
103                If :data:`False`, log to console with the :data:`logging.INFO` level.
104            output_dir: The directory where the log files will be located.
105                The names of the log files correspond to the name of the logger instance.
106        """
107        self.setLevel(1)
108
109        sh = StreamHandler()
110        sh.setFormatter(logging.Formatter(stream_fmt, date_fmt))
111        if not verbose:
112            sh.setLevel(logging.INFO)
113        self.addHandler(sh)
114
115        self._add_file_handlers(Path(output_dir, self.name))
116
117    def set_stage(self, stage: DtsStage, log_file_path: Path | None = None) -> None:
118        """Set the DTS execution stage and optionally log to files.
119
120        Set the DTS execution stage of the DTSLog class and optionally add
121        file handlers to the instance if the log file name is provided.
122
123        The file handlers log all messages. One is a regular human-readable log file and
124        the other one is a machine-readable log file with extra debug information.
125
126        Args:
127            stage: The DTS stage to set.
128            log_file_path: An optional path of the log file to use. This should be a full path
129                (either relative or absolute) without suffix (which will be appended).
130        """
131        self._remove_extra_file_handlers()
132
133        if DTSLogger._stage != stage:
134            self.info(f"Moving from stage '{DTSLogger._stage}' to stage '{stage}'.")
135            DTSLogger._stage = stage
136
137        if log_file_path:
138            self._extra_file_handlers.extend(self._add_file_handlers(log_file_path))
139
140    def _add_file_handlers(self, log_file_path: Path) -> list[FileHandler]:
141        """Add file handlers to the DTS root logger.
142
143        Add two type of file handlers:
144
145            * A regular file handler with suffix ".log",
146            * A machine-readable file handler with suffix ".verbose.log".
147              This format provides extensive information for debugging and detailed analysis.
148
149        Args:
150            log_file_path: The full path to the log file without suffix.
151
152        Returns:
153            The newly created file handlers.
154
155        """
156        fh = FileHandler(f"{log_file_path}.log")
157        fh.setFormatter(logging.Formatter(stream_fmt, date_fmt))
158        self.addHandler(fh)
159
160        verbose_fh = FileHandler(f"{log_file_path}.verbose.log")
161        verbose_fh.setFormatter(
162            logging.Formatter(
163                "%(asctime)s|%(stage)s|%(name)s|%(levelname)s|%(pathname)s|%(lineno)d|"
164                "%(funcName)s|%(process)d|%(thread)d|%(threadName)s|%(message)s",
165                datefmt=date_fmt,
166            )
167        )
168        self.addHandler(verbose_fh)
169
170        return [fh, verbose_fh]
171
172    def _remove_extra_file_handlers(self) -> None:
173        """Remove any extra file handlers that have been added to the logger."""
174        if self._extra_file_handlers:
175            for extra_file_handler in self._extra_file_handlers:
176                self.removeHandler(extra_file_handler)
177
178            self._extra_file_handlers = []
179
180
181def get_dts_logger(name: str = None) -> DTSLogger:
182    """Return a DTS logger instance identified by `name`.
183
184    Args:
185        name: If :data:`None`, return the DTS root logger.
186            If specified, return a child of the DTS root logger.
187
188    Returns:
189         The DTS root logger or a child logger identified by `name`.
190    """
191    original_logger_class = logging.getLoggerClass()
192    logging.setLoggerClass(DTSLogger)
193    if name:
194        name = f"{dts_root_logger_name}.{name}"
195    else:
196        name = dts_root_logger_name
197    logger = logging.getLogger(name)
198    logging.setLoggerClass(original_logger_class)
199    return logger  # type: ignore[return-value]
200