Source code for bioat.logger

import logging
import logging.handlers
import os

import coloredlogs

from .exceptions import BioatInvalidParameterError

__all__ = ["LoggerManager"]


[docs] class LoggerManager: LOG_FORMAT = "%(asctime)s.%(msecs)03d - [%(name)s] - %(filename)s[line:%(lineno)4d] - %(levelname)+8s: %(message)s"
[docs] def __init__( self, log_level: str = "ERROR", mod_name: str = "bioat.logger", cls_name: str | None = None, func_name: str | None = None, ): """ Initialize the LoggerManager with default log level and module name. Args: log_level (str, optional): Default logger level. Defaults to "ERROR". mod_name (str, optional): Module name. Defaults to "fbt". cls_name (str, optional): The name of the class for which the logger is created. Defaults to None. func_name (str, optional): The name of the function for which the logger is created. Defaults to None. """ self.log_level = self._get_log_level(log_level) self.mod_name = mod_name self.cls_name = cls_name self.func_name = func_name self.logger = self._create_logger() self.logger.propagate = False
def _get_log_level(self, log_level: str) -> int: """Parse and return the logging level.""" if log_level.upper() not in logging._nameToLevel: raise BioatInvalidParameterError( f"Invalid log level: {log_level}. " "Choose from CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET." ) return logging._nameToLevel[log_level.upper()] def _get_logger_name(self) -> str: """Construct the logger name based on module, class, and function.""" if self.cls_name and self.func_name: return f"{self.mod_name}.{self.cls_name}.{self.func_name}" elif self.cls_name: return f"{self.mod_name}.{self.cls_name}" elif self.func_name: return f"{self.mod_name}.{self.func_name}" else: return self.mod_name def _create_logger(self) -> logging.Logger: """Create and configure the logger.""" name = self._get_logger_name() logger = logging.getLogger(name) # Console handler with coloredlogs coloredlogs.DEFAULT_FIELD_STYLES = dict( asctime=dict(color="yellow", bold=True), name=dict(color="blue", bold=True), filename=dict(color="cyan"), lineno=dict(color="green", bold=True), levelname=dict(color="magenta", bold=True), ) coloredlogs.DEFAULT_LEVEL_STYLES = dict( critical=dict(color="red", bold=True), error=dict(color="red"), warning=dict(color="yellow"), info=dict(color="green"), debug=dict(color="blue"), ) coloredlogs.install( fmt=self.LOG_FORMAT, # 使用你的自定义日志格式 level=self.log_level, logger=logger, field_styles=dict( asctime=dict(color="yellow", bold=True), name=dict(color="blue", bold=True), filename=dict(color="cyan"), lineno=dict(color="green", bold=True), levelname=dict(color="magenta", bold=True), ), level_styles=dict( critical=dict(color="red", bold=True), error=dict(color="red"), warning=dict(color="yellow"), info=dict(color="green"), debug=dict(color="blue"), ), ) logger.propagate = False return logger
[docs] def set_names(self, cls_name: str | None = None, func_name: str | None = None): """ Update the class and/or function context for the logger. Args: cls_name (str, optional): New class name. func_name (str, optional): New function name. """ self.cls_name = cls_name self.func_name = func_name self.logger.name = self._get_logger_name() self.logger.propagate = False
[docs] def set_level(self, log_level: str): """ Update the logging level dynamically. Args: log_level (str): The new logging level. """ self.log_level = self._get_log_level(log_level) self.logger.setLevel(self.log_level) coloredlogs.install( fmt=self.LOG_FORMAT, # 使用你的自定义日志格式 level=self.log_level, logger=self.logger, field_styles=dict( asctime=dict(color="yellow", bold=True), name=dict(color="blue", bold=True), filename=dict(color="cyan"), lineno=dict(color="green", bold=True), levelname=dict(color="magenta", bold=True), ), level_styles=dict( critical=dict(color="red", bold=True), error=dict(color="red"), warning=dict(color="yellow"), info=dict(color="green"), debug=dict(color="blue"), ), reconfigure=True, ) self.logger.propagate = False
[docs] def set_file( self, file: str, mode: str = "a", max_bytes: int = 10 * 1024 * 1024, backup_count: int = 5, ): """ Configure file logging for the logger. Args: file (str): The name of the log file. mode (str, optional): File mode. Use 'a' for append or 'w' for overwrite. Defaults to "a". max_bytes (int, optional): Maximum size of a single log file before rotation. Defaults to 10MB. backup_count (int, optional): Number of backup files to keep. Defaults to 5. """ if mode not in {"a", "w"}: raise BioatInvalidParameterError( f"Invalid file mode: {mode}. Choose 'a' (append) or 'w' (overwrite)." ) # Ensure the directory exists directory = os.path.dirname(file) if directory and not os.path.exists(directory): os.makedirs(directory) self.logger.debug(f"Created directory for log file: {directory}") # Remove existing file handlers to prevent duplication self.logger.handlers = [ h for h in self.logger.handlers if not isinstance(h, logging.FileHandler) ] # Use FileHandler for overwrite mode or RotatingFileHandler for append/rotation if mode == "w" or max_bytes <= 0: # Simple overwrite mode file_handler = logging.FileHandler(file, mode=mode) else: # Rotating mode file_handler = logging.handlers.RotatingFileHandler( file, mode=mode, maxBytes=max_bytes, backupCount=backup_count ) # Set file log format file_handler.setFormatter(logging.Formatter(self.LOG_FORMAT)) self.logger.addHandler(file_handler) self.logger.propagate = False
[docs] def add_stream_handler(self): """ Add a stream handler to the logger for writing logs to the console. """ # 检查是否已有 StreamHandler if not any(isinstance(h, logging.StreamHandler) for h in self.logger.handlers): stream_handler = logging.StreamHandler() stream_handler.setFormatter(logging.Formatter(self.LOG_FORMAT)) self.logger.addHandler(stream_handler) self.logger.propagate = False
if __name__ == "__main__": # 初始化 LoggerManager lm = LoggerManager(log_level="debug") print("=== 基础日志测试 ===") lm.logger.debug("Debug message: Basic logging test.") lm.logger.info("Info message: Basic logging test.") lm.logger.warning("Warning message: Basic logging test.") lm.logger.error("Error message: Basic logging test.") lm.logger.critical("Critical message: Basic logging test.") print("\n=== 配置文件日志(追加模式) ===") lm.set_file("append_test.log", mode="a") lm.logger.info("Info message: Appending to 'append_test.log'.") lm.logger.warning("Warning message: Appending to 'append_test.log'.") lm.logger.error("Error message: Appending to 'append_test.log'.") print("\n=== 配置文件日志(覆盖模式) ===") lm.set_file("overwrite_test.log", mode="w") lm.logger.info("Info message: Overwriting 'overwrite_test.log'.") lm.logger.warning("Warning message: Overwriting 'overwrite_test.log'.") lm.logger.error("Error message: Overwriting 'overwrite_test.log'.") print("\n=== 配置文件日志(追加到不同文件) ===") # 追加另一个文件的同时不影响其他日志的写出 lm.set_file("different_file.log", mode="a") lm.logger.info("Info message: Appending to 'different_file.log'.") lm.logger.warning("Warning message: Appending to 'different_file.log'.") lm.logger.error("Error message: Appending to 'different_file.log'.") print("\n=== 动态更新日志级别 ===") lm.set_level("WARNING") lm.logger.debug("Debug message: Should not appear (level=WARNING).") lm.logger.info("Info message: Should not appear (level=WARNING).") lm.logger.warning("Warning message: Should appear (level=WARNING).") lm.logger.error("Error message: Should appear (level=WARNING).") print("\n=== 动态更新类名和函数名 ===") lm.set_level("debug") lm.set_names(cls_name="TestClass", func_name="test_function") lm.logger.info("Info message: Logging with class and function names.") print("\n=== 多次调用文件日志写入 ===") lm.set_file("multi_file1.log", mode="w") lm.logger.info("First file: multi_file1.log.") lm.set_file("multi_file2.log", mode="w") lm.logger.info("Second file: multi_file2.log.") lm.set_file("multi_file3.log", mode="w") lm.logger.info("Third file: multi_file3.log.") print("\n=== 测试不合法的日志级别 ===") try: lm.set_level("fake_level") except BioatInvalidParameterError as err: print("Should raise FbtValueError") print(err) print("\n=== 测试重复调用 add_stream_handler ===") lm.add_stream_handler() # 第一次调用 lm.logger.info("First stream handler added.") lm.add_stream_handler() # 第二次调用,不应重复添加 lm.logger.info("No duplicate stream handler should be added.") print("\n=== 测试自动创建目录 ===") lm.set_file("logs/test_folder/append_test.log", mode="a") lm.logger.info( "This message should be written to 'append_test.log' inside 'logs/test_folder'." )