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 12 kB

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

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