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