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