logging.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import logging
  2. import os
  3. import sys
  4. import threading
  5. from enum import Enum
  6. from typing import Any, Optional, Union
  7. def in_ipython() -> bool:
  8. """Check if the code is run in IPython, Jupyter, or Colab"""
  9. try:
  10. __IPYTHON__
  11. return True
  12. except NameError:
  13. return False
  14. logging.addLevelName(logging.WARNING, "WARN")
  15. loglevel = os.getenv("HIVEMIND_LOGLEVEL", "INFO")
  16. TRUE_CONSTANTS = ["TRUE", "1"]
  17. _env_colors = os.getenv("HIVEMIND_COLORS")
  18. if _env_colors is not None:
  19. use_colors = _env_colors.upper() in TRUE_CONSTANTS
  20. else:
  21. use_colors = sys.stderr.isatty() or in_ipython()
  22. _env_log_caller = os.getenv("HIVEMIND_ALWAYS_LOG_CALLER", "0")
  23. always_log_caller = _env_log_caller.upper() in TRUE_CONSTANTS
  24. class HandlerMode(Enum):
  25. NOWHERE = 0
  26. IN_HIVEMIND = 1
  27. IN_ROOT_LOGGER = 2
  28. _init_lock = threading.RLock()
  29. _current_mode = HandlerMode.IN_HIVEMIND
  30. _default_handler = None
  31. class _DisableIfNoColors(type):
  32. def __getattribute__(self, name: str) -> Any:
  33. if name.isupper() and not use_colors:
  34. return ""
  35. return super().__getattribute__(name)
  36. class TextStyle(metaclass=_DisableIfNoColors):
  37. """
  38. ANSI escape codes. Details: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
  39. """
  40. RESET = "\033[0m"
  41. BOLD = "\033[1m"
  42. RED = "\033[31m"
  43. BLUE = "\033[34m"
  44. PURPLE = "\033[35m"
  45. ORANGE = "\033[38;5;208m" # From 8-bit palette
  46. class CustomFormatter(logging.Formatter):
  47. """
  48. A formatter that allows a log time and caller info to be overridden via
  49. ``logger.log(level, message, extra={"origin_created": ..., "caller": ...})``.
  50. """
  51. # Details: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
  52. _LEVEL_TO_COLOR = {
  53. logging.DEBUG: TextStyle.PURPLE,
  54. logging.INFO: TextStyle.BLUE,
  55. logging.WARNING: TextStyle.ORANGE,
  56. logging.ERROR: TextStyle.RED,
  57. logging.CRITICAL: TextStyle.RED,
  58. }
  59. def format(self, record: logging.LogRecord) -> str:
  60. if hasattr(record, "origin_created"):
  61. record.created = record.origin_created
  62. record.msecs = (record.created - int(record.created)) * 1000
  63. if record.levelno != logging.INFO or always_log_caller:
  64. if not hasattr(record, "caller"):
  65. record.caller = f"{record.name}.{record.funcName}:{record.lineno}"
  66. record.caller_block = f" [{TextStyle.BOLD}{record.caller}{TextStyle.RESET}]"
  67. else:
  68. record.caller_block = ""
  69. # Aliases for the format argument
  70. record.levelcolor = self._LEVEL_TO_COLOR[record.levelno]
  71. record.bold = TextStyle.BOLD
  72. record.reset = TextStyle.RESET
  73. return super().format(record)
  74. def _initialize_if_necessary():
  75. global _current_mode, _default_handler
  76. with _init_lock:
  77. if _default_handler is not None:
  78. return
  79. formatter = CustomFormatter(
  80. fmt="{asctime}.{msecs:03.0f} [{bold}{levelcolor}{levelname}{reset}]{caller_block} {message}",
  81. style="{",
  82. datefmt="%b %d %H:%M:%S",
  83. )
  84. _default_handler = logging.StreamHandler()
  85. _default_handler.setFormatter(formatter)
  86. _enable_default_handler("hivemind")
  87. def get_logger(name: Optional[str] = None) -> logging.Logger:
  88. """
  89. Same as ``logging.getLogger()`` but ensures that the default hivemind log handler is initialized.
  90. :note: By default, the hivemind log handler (that reads the ``HIVEMIND_LOGLEVEL`` env variable and uses
  91. the colored log formatter) is only applied to messages logged inside the hivemind package.
  92. If you want to extend this handler to other loggers in your application, call
  93. ``use_hivemind_log_handler("in_root_logger")``.
  94. """
  95. _initialize_if_necessary()
  96. return logging.getLogger(name)
  97. def _enable_default_handler(name: Optional[str]) -> None:
  98. logger = get_logger(name)
  99. # Remove the extra default handler in the Colab's default logger before adding a new one
  100. if isinstance(logger, logging.RootLogger):
  101. for handler in list(logger.handlers):
  102. if isinstance(handler, logging.StreamHandler) and handler.stream is sys.stderr:
  103. logger.removeHandler(handler)
  104. logger.addHandler(_default_handler)
  105. logger.propagate = False
  106. logger.setLevel(loglevel)
  107. def _disable_default_handler(name: Optional[str]) -> None:
  108. logger = get_logger(name)
  109. logger.removeHandler(_default_handler)
  110. logger.propagate = True
  111. logger.setLevel(logging.NOTSET)
  112. def use_hivemind_log_handler(where: Union[HandlerMode, str]) -> None:
  113. """
  114. Choose loggers where the default hivemind log handler is applied. Options for the ``where`` argument are:
  115. * "in_hivemind" (default): Use the hivemind log handler in the loggers of the ``hivemind`` package.
  116. Don't propagate their messages to the root logger.
  117. * "nowhere": Don't use the hivemind log handler anywhere.
  118. Propagate the ``hivemind`` messages to the root logger.
  119. * "in_root_logger": Use the hivemind log handler in the root logger
  120. (that is, in all application loggers until they disable propagation to the root logger).
  121. Propagate the ``hivemind`` messages to the root logger.
  122. The options may be defined as strings (case-insensitive) or values from the HandlerMode enum.
  123. """
  124. global _current_mode
  125. if isinstance(where, str):
  126. # We allow `where` to be a string, so a developer does not have to import the enum for one usage
  127. where = HandlerMode[where.upper()]
  128. _initialize_if_necessary()
  129. if where == _current_mode:
  130. return
  131. if _current_mode == HandlerMode.IN_HIVEMIND:
  132. _disable_default_handler("hivemind")
  133. elif _current_mode == HandlerMode.IN_ROOT_LOGGER:
  134. _disable_default_handler(None)
  135. _current_mode = where
  136. if _current_mode == HandlerMode.IN_HIVEMIND:
  137. _enable_default_handler("hivemind")
  138. elif _current_mode == HandlerMode.IN_ROOT_LOGGER:
  139. _enable_default_handler(None)
  140. def golog_level_to_python(level: str) -> int:
  141. level = level.upper()
  142. if level in ["DPANIC", "PANIC", "FATAL"]:
  143. return logging.CRITICAL
  144. level = logging.getLevelName(level)
  145. if not isinstance(level, int):
  146. raise ValueError(f"Unknown go-log level: {level}")
  147. return level
  148. def python_level_to_golog(level: str) -> str:
  149. if not isinstance(level, str):
  150. raise ValueError("`level` is expected to be a Python log level in the string form")
  151. if level == "CRITICAL":
  152. return "FATAL"
  153. if level == "WARNING":
  154. return "WARN"
  155. return level