You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

logger.py 13 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. """
  2. Copyright (c) OpenMMLab. All rights reserved.
  3. Modified from
  4. https://github.com/open-mmlab/mmengine/blob/main/mmengine/logging/logger.py
  5. """
  6. import logging
  7. import os
  8. import os.path as osp
  9. import sys
  10. from logging import Logger, LogRecord
  11. from typing import Optional, Union
  12. from termcolor import colored
  13. from .manager import ManagerMixin, _accquire_lock, _release_lock
  14. class FilterDuplicateWarning(logging.Filter):
  15. """
  16. Filter for eliminating repeated warning messages in logging.
  17. This filter checks for duplicate warning messages and allows only the first occurrence of
  18. each message to be logged, filtering out subsequent duplicates.
  19. Parameters
  20. ----------
  21. name : str, optional
  22. The name of the filter. Defaults to "abl".
  23. """
  24. def __init__(self, name: Optional[str] = "abl"):
  25. super().__init__(name)
  26. self.seen: set = set()
  27. def filter(self, record: LogRecord) -> bool:
  28. """Filter the repeated warning message.
  29. Args:
  30. record (LogRecord): The log record.
  31. Returns:
  32. bool: Whether to output the log record.
  33. """
  34. if record.levelno != logging.WARNING:
  35. return True
  36. if record.msg not in self.seen:
  37. self.seen.add(record.msg)
  38. return True
  39. return False
  40. class ABLFormatter(logging.Formatter):
  41. """
  42. Colorful format for ABLLogger. If the log level is error, the logger will
  43. additionally output the location of the code.
  44. Parameters
  45. ----------
  46. color : bool, optional
  47. Whether to use colorful format. filehandler is not
  48. allowed to use color format, otherwise it will be garbled.
  49. Defaults to True.
  50. blink : bool, optional
  51. Whether to blink the ``INFO`` and ``DEBUG`` logging
  52. level. Defaults to False.
  53. kwargs : dict
  54. Keyword arguments passed to
  55. :meth:``logging.Formatter.__init__``.
  56. """
  57. _color_mapping: dict = dict(ERROR="red", WARNING="yellow", INFO="white", DEBUG="green")
  58. def __init__(self, color: bool = True, blink: bool = False, **kwargs):
  59. super().__init__(**kwargs)
  60. assert not (not color and blink), "blink should only be available when color is True"
  61. # Get prefix format according to color.
  62. error_prefix = self._get_prefix("ERROR", color, blink=True)
  63. warn_prefix = self._get_prefix("WARNING", color, blink=True)
  64. info_prefix = self._get_prefix("INFO", color, blink)
  65. debug_prefix = self._get_prefix("DEBUG", color, blink)
  66. # Config output format.
  67. self.err_format = (
  68. f"%(asctime)s - %(name)s - {error_prefix} - "
  69. "%(pathname)s - %(funcName)s - %(lineno)d - "
  70. "%(message)s"
  71. )
  72. self.warn_format = f"%(asctime)s - %(name)s - {warn_prefix} - %(" "message)s"
  73. self.info_format = f"%(asctime)s - %(name)s - {info_prefix} - %(" "message)s"
  74. self.debug_format = f"%(asctime)s - %(name)s - {debug_prefix} - %(" "message)s"
  75. def _get_prefix(self, level: str, color: bool, blink: bool = False) -> str:
  76. """
  77. Get the prefix of the target log level.
  78. Parameters
  79. ----------
  80. level : str
  81. Log level.
  82. color : bool
  83. Whether to get a colorful prefix.
  84. blink : bool, optional
  85. Whether the prefix will blink. Defaults to False.
  86. Returns
  87. -------
  88. str
  89. The plain or colorful prefix.
  90. """
  91. if color:
  92. attrs = ["underline"]
  93. if blink:
  94. attrs.append("blink")
  95. prefix = colored(level, self._color_mapping[level], attrs=attrs)
  96. else:
  97. prefix = level
  98. return prefix
  99. def format(self, record: LogRecord) -> str:
  100. """
  101. Override the ``logging.Formatter.format`` method. Output the
  102. message according to the specified log level.
  103. Parameters
  104. ----------
  105. record : LogRecord
  106. A LogRecord instance representing an event being logged.
  107. Returns
  108. -------
  109. str
  110. Formatted result.
  111. """
  112. if record.levelno == logging.ERROR:
  113. self._style._fmt = self.err_format # pylint: disable=protected-access
  114. elif record.levelno == logging.WARNING:
  115. self._style._fmt = self.warn_format # pylint: disable=protected-access
  116. elif record.levelno == logging.INFO:
  117. self._style._fmt = self.info_format # pylint: disable=protected-access
  118. elif record.levelno == logging.DEBUG:
  119. self._style._fmt = self.debug_format # pylint: disable=protected-access
  120. result = logging.Formatter.format(self, record)
  121. return result
  122. class ABLLogger(Logger, ManagerMixin):
  123. """
  124. Formatted logger used to record messages with different log levels and features.
  125. ``ABLLogger`` provides a formatted logger that can log messages with different
  126. log levels. It allows the creation of logger instances in a similar manner to ``ManagerMixin``.
  127. The logger has features like distributed log storage and colored terminal output for different
  128. log levels.
  129. Parameters
  130. ----------
  131. name : str
  132. Global instance name.
  133. logger_name : str, optional
  134. ``name`` attribute of ``logging.Logger`` instance. Defaults to 'abl'.
  135. log_file : str, optional
  136. The log filename. If specified, a ``FileHandler`` will be added to the logger.
  137. Defaults to None.
  138. log_level : Union[int, str], optional
  139. The log level of the handler. Defaults to 'INFO'.
  140. If log level is 'DEBUG', distributed logs will be saved during distributed training.
  141. file_mode : str, optional
  142. The file mode used to open log file. Defaults to 'w'.
  143. Notes
  144. -----
  145. - The ``name`` of the logger and the ``instance_name`` of ``ABLLogger`` could be different.
  146. ``ABLLogger`` instances are retrieved using ``ABLLogger.get_instance``, not
  147. ``logging.getLogger``. This ensures ``ABLLogger`` is not influenced by third-party logging
  148. configurations.
  149. - Unlike ``logging.Logger``, ``ABLLogger`` will not log warning or error messages without
  150. ``Handler``.
  151. Examples
  152. --------
  153. >>> logger = ABLLogger.get_instance(name='ABLLogger', logger_name='Logger')
  154. >>> # Although logger has a name attribute like ``logging.Logger``
  155. >>> # We cannot get logger instance by ``logging.getLogger``.
  156. >>> assert logger.name == 'Logger'
  157. >>> assert logger.instance_name == 'ABLLogger'
  158. >>> assert id(logger) != id(logging.getLogger('Logger'))
  159. >>> # Get logger that does not store logs.
  160. >>> logger1 = ABLLogger.get_instance('logger1')
  161. >>> # Get logger only save rank0 logs.
  162. >>> logger2 = ABLLogger.get_instance('logger2', log_file='out.log')
  163. >>> # Get logger only save multiple ranks logs.
  164. >>> logger3 = ABLLogger.get_instance('logger3', log_file='out.log', distributed=True)
  165. """
  166. def __init__(
  167. self,
  168. name: str,
  169. logger_name="abl",
  170. log_file: Optional[str] = None,
  171. log_level: Union[int, str] = "INFO",
  172. file_mode: str = "w",
  173. ):
  174. Logger.__init__(self, logger_name)
  175. ManagerMixin.__init__(self, name)
  176. if isinstance(log_level, str):
  177. log_level = logging._nameToLevel[log_level]
  178. stream_handler = logging.StreamHandler(stream=sys.stdout)
  179. # ``StreamHandler`` record month, day, hour, minute, and second
  180. # timestamp.
  181. stream_handler.setFormatter(ABLFormatter(color=True, datefmt="%m/%d %H:%M:%S"))
  182. stream_handler.setLevel(log_level)
  183. stream_handler.addFilter(FilterDuplicateWarning(logger_name))
  184. self.handlers.append(stream_handler)
  185. if log_file is None:
  186. import time # pylint: disable=import-outside-toplevel
  187. local_time = time.strftime("%Y%m%d_%H_%M_%S", time.localtime())
  188. _log_dir = os.path.join("results", local_time)
  189. self._log_dir = _log_dir
  190. if not os.path.exists(_log_dir):
  191. os.makedirs(_log_dir)
  192. log_file = osp.join(_log_dir, local_time + ".log")
  193. file_handler = logging.FileHandler(log_file, file_mode)
  194. file_handler.setFormatter(ABLFormatter(color=False, datefmt="%Y/%m/%d %H:%M:%S"))
  195. file_handler.setLevel(log_level)
  196. file_handler.addFilter(FilterDuplicateWarning(logger_name))
  197. self.handlers.append(file_handler)
  198. self._log_file = log_file
  199. @property
  200. def log_file(self):
  201. """Get the file path of the log.
  202. Returns:
  203. str: Path of the log.
  204. """
  205. return self._log_file
  206. @property
  207. def log_dir(self):
  208. """Get the directory where the log is stored.
  209. Returns:
  210. str: Directory where the log is stored.
  211. """
  212. return self._log_dir
  213. @classmethod
  214. def get_current_instance(cls) -> "ABLLogger":
  215. """
  216. Get the latest created ``ABLLogger`` instance.
  217. Returns
  218. -------
  219. ABLLogger
  220. The latest created ``ABLLogger`` instance. If no instance has been created,
  221. returns a logger with the instance name "abl".
  222. """
  223. if not cls._instance_dict:
  224. cls.get_instance("abl")
  225. return super().get_current_instance()
  226. def callHandlers(self, record: LogRecord) -> None:
  227. """
  228. Pass a record to all relevant handlers.
  229. Override the ``callHandlers`` method in ``logging.Logger`` to avoid
  230. multiple warning messages in DDP mode. This method loops through all
  231. handlers of the logger instance and its parents in the logger hierarchy.
  232. Parameters
  233. ----------
  234. record : LogRecord
  235. A ``LogRecord`` instance containing the logged message.
  236. """
  237. for handler in self.handlers:
  238. if record.levelno >= handler.level:
  239. handler.handle(record)
  240. def setLevel(self, level):
  241. """
  242. Set the logging level of this logger.
  243. Override the ``setLevel`` method to clear caches of all ``ABLLogger`` instances
  244. managed by ``ManagerMixin``. The level must be an int or a str.
  245. Parameters
  246. ----------
  247. level : Union[int, str]
  248. The logging level to set.
  249. """
  250. self.level = logging._checkLevel(level) # pylint: disable=protected-access
  251. _accquire_lock()
  252. # The same logic as ``logging.Manager._clear_cache``.
  253. for logger in ABLLogger._instance_dict.values():
  254. logger._cache.clear() # pylint: disable=protected-access
  255. _release_lock()
  256. def print_log(
  257. msg,
  258. logger: Optional[Union[Logger, str]] = None,
  259. level: Optional[int] = logging.INFO,
  260. ) -> None:
  261. """
  262. Print a log message using the specified logger or a default method.
  263. This function logs a message with a given logger, if provided, or prints it using
  264. the standard ``print`` function. It supports special logger types such as 'silent'
  265. and 'current'.
  266. Parameters
  267. ----------
  268. msg : str
  269. The message to be logged.
  270. logger : Union[Logger, str], optional
  271. The logger to use for logging the message. It can be a ``logging.Logger`` instance, a string
  272. specifying the logger name, 'silent', 'current', or None. If None, the ``print``
  273. method is used.
  274. - 'silent': No message will be printed.
  275. - 'current': Use the latest created logger to log the message.
  276. - other str: The instance name of the logger. A ``ValueError`` is raised if the logger has
  277. not been created.
  278. - None: The ``print()`` method is used for logging.
  279. level : int, optional
  280. The logging level. This is only applicable when ``logger`` is a Logger object, 'current',
  281. or a named logger instance. The default is ``logging.INFO``.
  282. """
  283. if logger is None:
  284. print(msg)
  285. elif isinstance(logger, logging.Logger):
  286. logger.log(level, msg)
  287. elif logger == "silent":
  288. pass
  289. elif logger == "current":
  290. logger_instance = ABLLogger.get_current_instance()
  291. logger_instance.log(level, msg)
  292. elif isinstance(logger, str):
  293. # If the type of ``logger`` is ``str``, but not with value of ``current`` or
  294. # ``silent``, we assume it indicates the name of the logger. If the
  295. # corresponding logger has not been created, ``print_log`` will raise
  296. # a ``ValueError``.
  297. if ABLLogger.check_instance_created(logger):
  298. logger_instance = ABLLogger.get_instance(logger)
  299. logger_instance.log(level, msg)
  300. else:
  301. raise ValueError(f"ABLLogger: {logger} has not been created!")
  302. else:
  303. raise TypeError(
  304. "``logger`` should be either a logging.Logger object, str, "
  305. f'"silent", "current" or None, but got {type(logger)}'
  306. )

An efficient Python toolkit for Abductive Learning (ABL), a novel paradigm that integrates machine learning and logical reasoning in a unified framework.