| |
| import inspect |
| import logging |
| import os |
| import os.path as osp |
| import sys |
| import warnings |
| from getpass import getuser |
| from logging import Logger, LogRecord, handlers |
| from socket import gethostname |
| from typing import Dict, Optional, Union |
|
|
| from termcolor import colored |
|
|
| from mmengine.utils import ManagerMixin |
| from mmengine.utils.manager import _accquire_lock, _release_lock |
|
|
|
|
| class FilterDuplicateWarning(logging.Filter): |
| """Filter the repeated warning message. |
| |
| Args: |
| name (str): name of the filter. |
| """ |
|
|
| def __init__(self, name: str = 'mmengine'): |
| 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 MMFormatter(logging.Formatter): |
| """Colorful format for MMLogger. If the log level is error, the logger will |
| additionally output the location of the code. |
| |
| Args: |
| color (bool): Whether to use colorful format. filehandler is not |
| allowed to use color format, otherwise it will be garbled. |
| blink (bool): Whether to blink the ``INFO`` and ``DEBUG`` logging |
| level. |
| **kwargs: 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') |
| |
| 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) |
|
|
| |
| 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=False) -> str: |
| """Get the prefix of the target log level. |
| |
| Args: |
| level (str): log level. |
| color (bool): Whether to get colorful prefix. |
| blink (bool): Whether the prefix will blink. |
| |
| 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. |
| |
| Args: |
| record (LogRecord): A LogRecord instance represents an event being |
| logged. |
| |
| Returns: |
| str: Formatted result. |
| """ |
| if record.levelno == logging.ERROR: |
| self._style._fmt = self.err_format |
| elif record.levelno == logging.WARNING: |
| self._style._fmt = self.warn_format |
| elif record.levelno == logging.INFO: |
| self._style._fmt = self.info_format |
| elif record.levelno == logging.DEBUG: |
| self._style._fmt = self.debug_format |
|
|
| result = logging.Formatter.format(self, record) |
| return result |
|
|
|
|
| class MMLogger(Logger, ManagerMixin): |
| """Formatted logger used to record messages. |
| |
| ``MMLogger`` can create formatted logger to log message with different |
| log levels and get instance in the same way as ``ManagerMixin``. |
| ``MMLogger`` has the following features: |
| |
| - Distributed log storage, ``MMLogger`` can choose whether to save log of |
| different ranks according to `log_file`. |
| - Message with different log levels will have different colors and format |
| when displayed on terminal. |
| |
| Note: |
| - The `name` of logger and the ``instance_name`` of ``MMLogger`` could |
| be different. We can only get ``MMLogger`` instance by |
| ``MMLogger.get_instance`` but not ``logging.getLogger``. This feature |
| ensures ``MMLogger`` will not be incluenced by third-party logging |
| config. |
| - Different from ``logging.Logger``, ``MMLogger`` will not log warning |
| or error message without ``Handler``. |
| |
| Examples: |
| >>> logger = MMLogger.get_instance(name='MMLogger', |
| >>> logger_name='Logger') |
| >>> # Although logger has name attribute just like `logging.Logger` |
| >>> # We cannot get logger instance by `logging.getLogger`. |
| >>> assert logger.name == 'Logger' |
| >>> assert logger.instance_name = 'MMLogger' |
| >>> assert id(logger) != id(logging.getLogger('Logger')) |
| >>> # Get logger that do not store logs. |
| >>> logger1 = MMLogger.get_instance('logger1') |
| >>> # Get logger only save rank0 logs. |
| >>> logger2 = MMLogger.get_instance('logger2', log_file='out.log') |
| >>> # Get logger only save multiple ranks logs. |
| >>> logger3 = MMLogger.get_instance('logger3', log_file='out.log', |
| >>> distributed=True) |
| |
| Args: |
| name (str): Global instance name. |
| logger_name (str): ``name`` attribute of ``Logging.Logger`` instance. |
| If `logger_name` is not defined, defaults to 'mmengine'. |
| log_file (str, optional): The log filename. If specified, a |
| ``FileHandler`` will be added to the logger. Defaults to None. |
| log_level (str): 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): The file mode used to open log file. Defaults to 'w'. |
| distributed (bool): Whether to save distributed logs, Defaults to |
| false. |
| file_handler_cfg (dict, optional): Configuration of file handler. |
| Defaults to None. If ``file_handler_cfg`` is not specified, |
| ``logging.FileHandler`` will be used by default. If it is |
| specified, the ``type`` key should be set. It can be |
| ``RotatingFileHandler``, ``TimedRotatingFileHandler``, |
| ``WatchedFileHandler`` or other file handlers, and the remaining |
| fields will be used to build the handler. |
| |
| Examples: |
| >>> file_handler_cfg = dict( |
| >>> type='TimedRotatingFileHandler', |
| >>> when='MIDNIGHT', |
| >>> interval=1, |
| >>> backupCount=365) |
| |
| `New in version 0.9.0.` |
| """ |
|
|
| def __init__(self, |
| name: str, |
| logger_name='mmengine', |
| log_file: Optional[str] = None, |
| log_level: Union[int, str] = 'INFO', |
| file_mode: str = 'w', |
| distributed=False, |
| file_handler_cfg: Optional[dict] = None): |
| Logger.__init__(self, logger_name) |
| ManagerMixin.__init__(self, name) |
| |
| if isinstance(log_level, str): |
| log_level = logging._nameToLevel[log_level] |
| global_rank = _get_rank() |
| device_id = _get_device_id() |
|
|
| |
| |
| stream_handler = logging.StreamHandler(stream=sys.stdout) |
| |
| |
| stream_handler.setFormatter( |
| MMFormatter(color=True, datefmt='%m/%d %H:%M:%S')) |
| |
| if global_rank == 0: |
| stream_handler.setLevel(log_level) |
| else: |
| stream_handler.setLevel(logging.ERROR) |
| stream_handler.addFilter(FilterDuplicateWarning(logger_name)) |
| self.handlers.append(stream_handler) |
|
|
| if log_file is not None: |
| world_size = _get_world_size() |
| is_distributed = (log_level <= logging.DEBUG |
| or distributed) and world_size > 1 |
| if is_distributed: |
| filename, suffix = osp.splitext(osp.basename(log_file)) |
| hostname = _get_host_info() |
| if hostname: |
| filename = (f'{filename}_{hostname}_device{device_id}_' |
| f'rank{global_rank}{suffix}') |
| else: |
| |
| filename = (f'{filename}_device{device_id}_' |
| f'rank{global_rank}{suffix}') |
| log_file = osp.join(osp.dirname(log_file), filename) |
| |
| |
| if global_rank == 0 or is_distributed: |
| if file_handler_cfg is not None: |
| assert 'type' in file_handler_cfg |
| file_handler_type = file_handler_cfg.pop('type') |
| file_handlers_map = _get_logging_file_handlers() |
| if file_handler_type in file_handlers_map: |
| file_handler_cls = file_handlers_map[file_handler_type] |
| file_handler_cfg.setdefault('filename', log_file) |
| file_handler = file_handler_cls(**file_handler_cfg) |
| else: |
| raise ValueError('`logging.handlers` does not ' |
| f'contain {file_handler_type}') |
| else: |
| |
| |
| |
| |
| |
| file_handler = logging.FileHandler(log_file, file_mode) |
|
|
| |
| |
| |
| file_handler.setFormatter( |
| MMFormatter(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): |
| return self._log_file |
|
|
| @classmethod |
| def get_current_instance(cls) -> 'MMLogger': |
| """Get latest created ``MMLogger`` instance. |
| |
| :obj:`MMLogger` can call :meth:`get_current_instance` before any |
| instance has been created, and return a logger with the instance name |
| "mmengine". |
| |
| Returns: |
| MMLogger: Configured logger instance. |
| """ |
| if not cls._instance_dict: |
| cls.get_instance('mmengine') |
| return super().get_current_instance() |
|
|
| def callHandlers(self, record: LogRecord) -> None: |
| """Pass a record to all relevant handlers. |
| |
| Override ``callHandlers`` method in ``logging.Logger`` to avoid |
| multiple warning messages in DDP mode. Loop through all handlers of |
| the logger instance and its parents in the logger hierarchy. If no |
| handler was found, the record will not be output. |
| |
| Args: |
| record (LogRecord): A ``LogRecord`` instance contains 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. |
| |
| If ``logging.Logger.selLevel`` is called, all ``logging.Logger`` |
| instances managed by ``logging.Manager`` will clear the cache. Since |
| ``MMLogger`` is not managed by ``logging.Manager`` anymore, |
| ``MMLogger`` should override this method to clear caches of all |
| ``MMLogger`` instance which is managed by :obj:`ManagerMixin`. |
| |
| level must be an int or a str. |
| """ |
| self.level = logging._checkLevel(level) |
| _accquire_lock() |
| |
| for logger in MMLogger._instance_dict.values(): |
| logger._cache.clear() |
| _release_lock() |
|
|
|
|
| def print_log(msg, |
| logger: Optional[Union[Logger, str]] = None, |
| level=logging.INFO) -> None: |
| """Print a log message. |
| |
| Args: |
| msg (str): The message to be logged. |
| logger (Logger or str, optional): If the type of logger is |
| ``logging.Logger``, we directly use logger to log messages. |
| Some special loggers are: |
| |
| - "silent": No message will be printed. |
| - "current": Use latest created logger to log message. |
| - other str: Instance name of logger. The corresponding logger |
| will log message if it has been created, otherwise ``print_log`` |
| will raise a `ValueError`. |
| - None: The `print()` method will be used to print log messages. |
| level (int): Logging level. Only available when `logger` is a Logger |
| object, "current", or a created logger instance name. |
| """ |
| if logger is None: |
| print(msg) |
| elif isinstance(logger, logging.Logger): |
| logger.log(level, msg) |
| elif logger == 'silent': |
| pass |
| elif logger == 'current': |
| logger_instance = MMLogger.get_current_instance() |
| logger_instance.log(level, msg) |
| elif isinstance(logger, str): |
| |
| |
| |
| |
| if MMLogger.check_instance_created(logger): |
| logger_instance = MMLogger.get_instance(logger) |
| logger_instance.log(level, msg) |
| else: |
| raise ValueError(f'MMLogger: {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)}') |
|
|
|
|
| def _get_world_size(): |
| """Support using logging module without torch.""" |
| try: |
| |
| from mmengine.dist import get_world_size |
| except ImportError: |
| return 1 |
| else: |
| return get_world_size() |
|
|
|
|
| def _get_rank(): |
| """Support using logging module without torch.""" |
| try: |
| |
| from mmengine.dist import get_rank |
| except ImportError: |
| return 0 |
| else: |
| return get_rank() |
|
|
|
|
| def _get_device_id(): |
| """Get device id of current machine.""" |
| try: |
| import torch |
| except ImportError: |
| return 0 |
| else: |
| MUSA_AVAILABLE = False |
| try: |
| import torch_musa |
| MUSA_AVAILABLE = True |
| except ImportError: |
| pass |
| if MUSA_AVAILABLE: |
| local_rank = int(os.getenv('LOCAL_RANK', '0')) |
| musa_visible_devices = os.getenv('MUSA_VISIBLE_DEVICES', None) |
| if musa_visible_devices is None: |
| num_device = torch_musa.device_count() |
| musa_visible_devices = list(range(num_device)) |
| else: |
| musa_visible_devices = musa_visible_devices.split(',') |
| return int(musa_visible_devices[local_rank]) |
| else: |
| local_rank = int(os.getenv('LOCAL_RANK', '0')) |
| |
| if not torch.cuda.is_available(): |
| return local_rank |
| cuda_visible_devices = os.getenv('CUDA_VISIBLE_DEVICES', None) |
| if cuda_visible_devices is None: |
| num_device = torch.cuda.device_count() |
| cuda_visible_devices = list(range(num_device)) |
| else: |
| cuda_visible_devices = cuda_visible_devices.split(',') |
| try: |
| return int(cuda_visible_devices[local_rank]) |
| except ValueError: |
| |
| |
| return cuda_visible_devices[local_rank] |
|
|
|
|
| def _get_host_info() -> str: |
| """Get hostname and username. |
| |
| Return empty string if exception raised, e.g. ``getpass.getuser()`` will |
| lead to error in docker container |
| """ |
| host = '' |
| try: |
| host = f'{getuser()}@{gethostname()}' |
| except Exception as e: |
| warnings.warn(f'Host or user not found: {str(e)}') |
| finally: |
| return host |
|
|
|
|
| def _get_logging_file_handlers() -> Dict: |
| """Get additional file_handlers in ``logging.handlers``. |
| |
| Returns: |
| Dict: A map of file_handlers. |
| """ |
| file_handlers_map = {} |
| for module_name in dir(handlers): |
| if module_name.startswith('__'): |
| continue |
| _fh = getattr(handlers, module_name) |
| if inspect.isclass(_fh) and issubclass(_fh, logging.FileHandler): |
| file_handlers_map[module_name] = _fh |
| return file_handlers_map |
|
|