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