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