""" Copyright (c) OpenMMLab. All rights reserved. Modified from https://github.com/open-mmlab/mmengine/blob/main/mmengine/logging/logger.py """ import logging import os import os.path as osp import sys from logging import Logger, LogRecord from typing import Optional, Union from termcolor import colored from .manager import ManagerMixin, _accquire_lock, _release_lock class FilterDuplicateWarning(logging.Filter): """ Filter for eliminating repeated warning messages in logging. This filter checks for duplicate warning messages and allows only the first occurrence of each message to be logged, filtering out subsequent duplicates. Parameters ---------- name : str, optional The name of the filter, by default "abl". """ def __init__(self, name: Optional[str] = "abl"): super().__init__(name) self.seen: set = set() def filter(self, record: LogRecord) -> bool: """Filter the repeated warning message. Args: record (LogRecord): The log record. Returns: bool: Whether to output the log record. """ if record.levelno != logging.WARNING: return True if record.msg not in self.seen: self.seen.add(record.msg) return True return False class ABLFormatter(logging.Formatter): """ Colorful format for ABLLogger. If the log level is error, the logger will additionally output the location of the code. Parameters ---------- color : bool, optional Whether to use colorful format. filehandler is not allowed to use color format, otherwise it will be garbled. Defaults to True. blink : bool, optional Whether to blink the ``INFO`` and ``DEBUG`` logging level. Defaults to False. kwargs : dict Keyword arguments passed to :meth:``logging.Formatter.__init__``. """ _color_mapping: dict = dict(ERROR="red", WARNING="yellow", INFO="white", DEBUG="green") def __init__(self, color: bool = True, blink: bool = False, **kwargs): super().__init__(**kwargs) assert not (not color and blink), "blink should only be available when color is True" # Get prefix format according to color. error_prefix = self._get_prefix("ERROR", color, blink=True) warn_prefix = self._get_prefix("WARNING", color, blink=True) info_prefix = self._get_prefix("INFO", color, blink) debug_prefix = self._get_prefix("DEBUG", color, blink) # Config output format. self.err_format = ( f"%(asctime)s - %(name)s - {error_prefix} - " "%(pathname)s - %(funcName)s - %(lineno)d - " "%(message)s" ) self.warn_format = f"%(asctime)s - %(name)s - {warn_prefix} - %(" "message)s" self.info_format = f"%(asctime)s - %(name)s - {info_prefix} - %(" "message)s" self.debug_format = f"%(asctime)s - %(name)s - {debug_prefix} - %(" "message)s" def _get_prefix(self, level: str, color: bool, blink: bool = False) -> str: """ Get the prefix of the target log level. Parameters ---------- level : str Log level. color : bool Whether to get a colorful prefix. blink : bool, optional Whether the prefix will blink. Defaults to False. Returns ------- str The plain or colorful prefix. """ if color: attrs = ["underline"] if blink: attrs.append("blink") prefix = colored(level, self._color_mapping[level], attrs=attrs) else: prefix = level return prefix def format(self, record: LogRecord) -> str: """ Override the ``logging.Formatter.format`` method. Output the message according to the specified log level. Parameters ---------- record : LogRecord A LogRecord instance representing an event being logged. Returns ------- str Formatted result. """ if record.levelno == logging.ERROR: self._style._fmt = self.err_format # pylint: disable=protected-access elif record.levelno == logging.WARNING: self._style._fmt = self.warn_format # pylint: disable=protected-access elif record.levelno == logging.INFO: self._style._fmt = self.info_format # pylint: disable=protected-access elif record.levelno == logging.DEBUG: self._style._fmt = self.debug_format # pylint: disable=protected-access result = logging.Formatter.format(self, record) return result class ABLLogger(Logger, ManagerMixin): """ Formatted logger used to record messages with different log levels and features. ``ABLLogger`` provides a formatted logger that can log messages with different log levels. It allows the creation of logger instances in a similar manner to ``ManagerMixin``. The logger has features like distributed log storage and colored terminal output for different log levels. Parameters ---------- name : str Global instance name. logger_name : str, optional ``name`` attribute of ``logging.Logger`` instance. Defaults to 'abl'. log_file : str, optional The log filename. If specified, a ``FileHandler`` will be added to the logger. Defaults to None. log_level : Union[int, str], optional The log level of the handler. Defaults to 'INFO'. If log level is 'DEBUG', distributed logs will be saved during distributed training. file_mode : str, optional The file mode used to open log file. Defaults to 'w'. Notes ----- - The ``name`` of the logger and the ``instance_name`` of ``ABLLogger`` could be different. ``ABLLogger`` instances are retrieved using ``ABLLogger.get_instance``, not ``logging.getLogger``. This ensures ``ABLLogger`` is not influenced by third-party logging configurations. - Unlike ``logging.Logger``, ``ABLLogger`` will not log warning or error messages without ``Handler``. Examples -------- >>> logger = ABLLogger.get_instance(name='ABLLogger', logger_name='Logger') >>> # Although logger has a name attribute like ``logging.Logger`` >>> # We cannot get logger instance by ``logging.getLogger``. >>> assert logger.name == 'Logger' >>> assert logger.instance_name == 'ABLLogger' >>> assert id(logger) != id(logging.getLogger('Logger')) >>> # Get logger that does not store logs. >>> logger1 = ABLLogger.get_instance('logger1') >>> # Get logger only save rank0 logs. >>> logger2 = ABLLogger.get_instance('logger2', log_file='out.log') >>> # Get logger only save multiple ranks logs. >>> logger3 = ABLLogger.get_instance('logger3', log_file='out.log', distributed=True) """ def __init__( self, name: str, logger_name="abl", log_file: Optional[str] = None, log_level: Union[int, str] = "INFO", file_mode: str = "w", ): Logger.__init__(self, logger_name) ManagerMixin.__init__(self, name) if isinstance(log_level, str): log_level = logging._nameToLevel[log_level] stream_handler = logging.StreamHandler(stream=sys.stdout) # ``StreamHandler`` record month, day, hour, minute, and second # timestamp. stream_handler.setFormatter(ABLFormatter(color=True, datefmt="%m/%d %H:%M:%S")) stream_handler.setLevel(log_level) stream_handler.addFilter(FilterDuplicateWarning(logger_name)) self.handlers.append(stream_handler) if log_file is None: import time # pylint: disable=import-outside-toplevel local_time = time.strftime("%Y%m%d_%H_%M_%S", time.localtime()) _log_dir = os.path.join("results", local_time) self._log_dir = _log_dir if not os.path.exists(_log_dir): os.makedirs(_log_dir) log_file = osp.join(_log_dir, local_time + ".log") file_handler = logging.FileHandler(log_file, file_mode) file_handler.setFormatter(ABLFormatter(color=False, datefmt="%Y/%m/%d %H:%M:%S")) file_handler.setLevel(log_level) file_handler.addFilter(FilterDuplicateWarning(logger_name)) self.handlers.append(file_handler) self._log_file = log_file @property def log_file(self): """Get the file path of the log. Returns: str: Path of the log. """ return self._log_file @property def log_dir(self): """Get the directory where the log is stored. Returns: str: Directory where the log is stored. """ return self._log_dir @classmethod def get_current_instance(cls) -> "ABLLogger": """ Get the latest created ``ABLLogger`` instance. Returns ------- ABLLogger The latest created ``ABLLogger`` instance. If no instance has been created, returns a logger with the instance name "abl". """ if not cls._instance_dict: cls.get_instance("abl") return super().get_current_instance() def callHandlers(self, record: LogRecord) -> None: """ Pass a record to all relevant handlers. Override the ``callHandlers`` method in ``logging.Logger`` to avoid multiple warning messages in DDP mode. This method loops through all handlers of the logger instance and its parents in the logger hierarchy. Parameters ---------- record : LogRecord A ``LogRecord`` instance containing the logged message. """ for handler in self.handlers: if record.levelno >= handler.level: handler.handle(record) def setLevel(self, level): """ Set the logging level of this logger. Override the ``setLevel`` method to clear caches of all ``ABLLogger`` instances managed by ``ManagerMixin``. The level must be an int or a str. Parameters ---------- level : Union[int, str] The logging level to set. """ self.level = logging._checkLevel(level) # pylint: disable=protected-access _accquire_lock() # The same logic as ``logging.Manager._clear_cache``. for logger in ABLLogger._instance_dict.values(): logger._cache.clear() # pylint: disable=protected-access _release_lock() def print_log( msg, logger: Optional[Union[Logger, str]] = None, level: Optional[int] = logging.INFO, ) -> None: """ Print a log message using the specified logger or a default method. This function logs a message with a given logger, if provided, or prints it using the standard ``print`` function. It supports special logger types such as 'silent' and 'current'. Parameters ---------- msg : str The message to be logged. logger : Union[Logger, str], optional The logger to use for logging the message. It can be a ``logging.Logger`` instance, a string specifying the logger name, 'silent', 'current', or None. If None, the ``print`` method is used. - 'silent': No message will be printed. - 'current': Use the latest created logger to log the message. - other str: The instance name of the logger. A ``ValueError`` is raised if the logger has not been created. - None: The ``print()`` method is used for logging. level : int, optional The logging level. This is only applicable when ``logger`` is a Logger object, 'current', or a named logger instance. The default is ``logging.INFO``. """ if logger is None: print(msg) elif isinstance(logger, logging.Logger): logger.log(level, msg) elif logger == "silent": pass elif logger == "current": logger_instance = ABLLogger.get_current_instance() logger_instance.log(level, msg) elif isinstance(logger, str): # If the type of ``logger`` is ``str``, but not with value of ``current`` or # ``silent``, we assume it indicates the name of the logger. If the # corresponding logger has not been created, ``print_log`` will raise # a ``ValueError``. if ABLLogger.check_instance_created(logger): logger_instance = ABLLogger.get_instance(logger) logger_instance.log(level, msg) else: raise ValueError(f"ABLLogger: {logger} has not been created!") else: raise TypeError( "``logger`` should be either a logging.Logger object, str, " f'"silent", "current" or None, but got {type(logger)}' )