logging.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import logging
  2. from datetime import datetime
  3. from logging import Handler, LogRecord
  4. from pathlib import Path
  5. from types import ModuleType
  6. from typing import ClassVar, Iterable, List, Optional, Type, Union
  7. from rich._null_file import NullFile
  8. from . import get_console
  9. from ._log_render import FormatTimeCallable, LogRender
  10. from .console import Console, ConsoleRenderable
  11. from .highlighter import Highlighter, ReprHighlighter
  12. from .text import Text
  13. from .traceback import Traceback
  14. class RichHandler(Handler):
  15. """A logging handler that renders output with Rich. The time / level / message and file are displayed in columns.
  16. The level is color coded, and the message is syntax highlighted.
  17. Note:
  18. Be careful when enabling console markup in log messages if you have configured logging for libraries not
  19. under your control. If a dependency writes messages containing square brackets, it may not produce the intended output.
  20. Args:
  21. level (Union[int, str], optional): Log level. Defaults to logging.NOTSET.
  22. console (:class:`~rich.console.Console`, optional): Optional console instance to write logs.
  23. Default will use a global console instance writing to stdout.
  24. show_time (bool, optional): Show a column for the time. Defaults to True.
  25. omit_repeated_times (bool, optional): Omit repetition of the same time. Defaults to True.
  26. show_level (bool, optional): Show a column for the level. Defaults to True.
  27. show_path (bool, optional): Show the path to the original log call. Defaults to True.
  28. enable_link_path (bool, optional): Enable terminal link of path column to file. Defaults to True.
  29. highlighter (Highlighter, optional): Highlighter to style log messages, or None to use ReprHighlighter. Defaults to None.
  30. markup (bool, optional): Enable console markup in log messages. Defaults to False.
  31. rich_tracebacks (bool, optional): Enable rich tracebacks with syntax highlighting and formatting. Defaults to False.
  32. tracebacks_width (Optional[int], optional): Number of characters used to render tracebacks, or None for full width. Defaults to None.
  33. tracebacks_code_width (int, optional): Number of code characters used to render tracebacks, or None for full width. Defaults to 88.
  34. tracebacks_extra_lines (int, optional): Additional lines of code to render tracebacks, or None for full width. Defaults to None.
  35. tracebacks_theme (str, optional): Override pygments theme used in traceback.
  36. tracebacks_word_wrap (bool, optional): Enable word wrapping of long tracebacks lines. Defaults to True.
  37. tracebacks_show_locals (bool, optional): Enable display of locals in tracebacks. Defaults to False.
  38. tracebacks_suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
  39. tracebacks_max_frames (int, optional): Optional maximum number of frames returned by traceback.
  40. locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
  41. Defaults to 10.
  42. locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
  43. log_time_format (Union[str, TimeFormatterCallable], optional): If ``log_time`` is enabled, either string for strftime or callable that formats the time. Defaults to "[%x %X] ".
  44. keywords (List[str], optional): List of words to highlight instead of ``RichHandler.KEYWORDS``.
  45. """
  46. KEYWORDS: ClassVar[Optional[List[str]]] = [
  47. "GET",
  48. "POST",
  49. "HEAD",
  50. "PUT",
  51. "DELETE",
  52. "OPTIONS",
  53. "TRACE",
  54. "PATCH",
  55. ]
  56. HIGHLIGHTER_CLASS: ClassVar[Type[Highlighter]] = ReprHighlighter
  57. def __init__(
  58. self,
  59. level: Union[int, str] = logging.NOTSET,
  60. console: Optional[Console] = None,
  61. *,
  62. show_time: bool = True,
  63. omit_repeated_times: bool = True,
  64. show_level: bool = True,
  65. show_path: bool = True,
  66. enable_link_path: bool = True,
  67. highlighter: Optional[Highlighter] = None,
  68. markup: bool = False,
  69. rich_tracebacks: bool = False,
  70. tracebacks_width: Optional[int] = None,
  71. tracebacks_code_width: int = 88,
  72. tracebacks_extra_lines: int = 3,
  73. tracebacks_theme: Optional[str] = None,
  74. tracebacks_word_wrap: bool = True,
  75. tracebacks_show_locals: bool = False,
  76. tracebacks_suppress: Iterable[Union[str, ModuleType]] = (),
  77. tracebacks_max_frames: int = 100,
  78. locals_max_length: int = 10,
  79. locals_max_string: int = 80,
  80. log_time_format: Union[str, FormatTimeCallable] = "[%x %X]",
  81. keywords: Optional[List[str]] = None,
  82. ) -> None:
  83. super().__init__(level=level)
  84. self.console = console or get_console()
  85. self.highlighter = highlighter or self.HIGHLIGHTER_CLASS()
  86. self._log_render = LogRender(
  87. show_time=show_time,
  88. show_level=show_level,
  89. show_path=show_path,
  90. time_format=log_time_format,
  91. omit_repeated_times=omit_repeated_times,
  92. level_width=None,
  93. )
  94. self.enable_link_path = enable_link_path
  95. self.markup = markup
  96. self.rich_tracebacks = rich_tracebacks
  97. self.tracebacks_width = tracebacks_width
  98. self.tracebacks_extra_lines = tracebacks_extra_lines
  99. self.tracebacks_theme = tracebacks_theme
  100. self.tracebacks_word_wrap = tracebacks_word_wrap
  101. self.tracebacks_show_locals = tracebacks_show_locals
  102. self.tracebacks_suppress = tracebacks_suppress
  103. self.tracebacks_max_frames = tracebacks_max_frames
  104. self.tracebacks_code_width = tracebacks_code_width
  105. self.locals_max_length = locals_max_length
  106. self.locals_max_string = locals_max_string
  107. self.keywords = keywords
  108. def get_level_text(self, record: LogRecord) -> Text:
  109. """Get the level name from the record.
  110. Args:
  111. record (LogRecord): LogRecord instance.
  112. Returns:
  113. Text: A tuple of the style and level name.
  114. """
  115. level_name = record.levelname
  116. level_text = Text.styled(
  117. level_name.ljust(8), f"logging.level.{level_name.lower()}"
  118. )
  119. return level_text
  120. def emit(self, record: LogRecord) -> None:
  121. """Invoked by logging."""
  122. message = self.format(record)
  123. traceback = None
  124. if (
  125. self.rich_tracebacks
  126. and record.exc_info
  127. and record.exc_info != (None, None, None)
  128. ):
  129. exc_type, exc_value, exc_traceback = record.exc_info
  130. assert exc_type is not None
  131. assert exc_value is not None
  132. traceback = Traceback.from_exception(
  133. exc_type,
  134. exc_value,
  135. exc_traceback,
  136. width=self.tracebacks_width,
  137. code_width=self.tracebacks_code_width,
  138. extra_lines=self.tracebacks_extra_lines,
  139. theme=self.tracebacks_theme,
  140. word_wrap=self.tracebacks_word_wrap,
  141. show_locals=self.tracebacks_show_locals,
  142. locals_max_length=self.locals_max_length,
  143. locals_max_string=self.locals_max_string,
  144. suppress=self.tracebacks_suppress,
  145. max_frames=self.tracebacks_max_frames,
  146. )
  147. message = record.getMessage()
  148. if self.formatter:
  149. record.message = record.getMessage()
  150. formatter = self.formatter
  151. if hasattr(formatter, "usesTime") and formatter.usesTime():
  152. record.asctime = formatter.formatTime(record, formatter.datefmt)
  153. message = formatter.formatMessage(record)
  154. message_renderable = self.render_message(record, message)
  155. log_renderable = self.render(
  156. record=record, traceback=traceback, message_renderable=message_renderable
  157. )
  158. if isinstance(self.console.file, NullFile):
  159. # Handles pythonw, where stdout/stderr are null, and we return NullFile
  160. # instance from Console.file. In this case, we still want to make a log record
  161. # even though we won't be writing anything to a file.
  162. self.handleError(record)
  163. else:
  164. try:
  165. self.console.print(log_renderable)
  166. except Exception:
  167. self.handleError(record)
  168. def render_message(self, record: LogRecord, message: str) -> "ConsoleRenderable":
  169. """Render message text in to Text.
  170. Args:
  171. record (LogRecord): logging Record.
  172. message (str): String containing log message.
  173. Returns:
  174. ConsoleRenderable: Renderable to display log message.
  175. """
  176. use_markup = getattr(record, "markup", self.markup)
  177. message_text = Text.from_markup(message) if use_markup else Text(message)
  178. highlighter = getattr(record, "highlighter", self.highlighter)
  179. if highlighter:
  180. message_text = highlighter(message_text)
  181. if self.keywords is None:
  182. self.keywords = self.KEYWORDS
  183. if self.keywords:
  184. message_text.highlight_words(self.keywords, "logging.keyword")
  185. return message_text
  186. def render(
  187. self,
  188. *,
  189. record: LogRecord,
  190. traceback: Optional[Traceback],
  191. message_renderable: "ConsoleRenderable",
  192. ) -> "ConsoleRenderable":
  193. """Render log for display.
  194. Args:
  195. record (LogRecord): logging Record.
  196. traceback (Optional[Traceback]): Traceback instance or None for no Traceback.
  197. message_renderable (ConsoleRenderable): Renderable (typically Text) containing log message contents.
  198. Returns:
  199. ConsoleRenderable: Renderable to display log.
  200. """
  201. path = Path(record.pathname).name
  202. level = self.get_level_text(record)
  203. time_format = None if self.formatter is None else self.formatter.datefmt
  204. log_time = datetime.fromtimestamp(record.created)
  205. log_renderable = self._log_render(
  206. self.console,
  207. [message_renderable] if not traceback else [message_renderable, traceback],
  208. log_time=log_time,
  209. time_format=time_format,
  210. level=level,
  211. path=path,
  212. line_no=record.lineno,
  213. link_path=record.pathname if self.enable_link_path else None,
  214. )
  215. return log_renderable
  216. if __name__ == "__main__": # pragma: no cover
  217. from time import sleep
  218. FORMAT = "%(message)s"
  219. # FORMAT = "%(asctime)-15s - %(levelname)s - %(message)s"
  220. logging.basicConfig(
  221. level="NOTSET",
  222. format=FORMAT,
  223. datefmt="[%X]",
  224. handlers=[RichHandler(rich_tracebacks=True, tracebacks_show_locals=True)],
  225. )
  226. log = logging.getLogger("rich")
  227. log.info("Server starting...")
  228. log.info("Listening on http://127.0.0.1:8080")
  229. sleep(1)
  230. log.info("GET /index.html 200 1298")
  231. log.info("GET /imgs/backgrounds/back1.jpg 200 54386")
  232. log.info("GET /css/styles.css 200 54386")
  233. log.warning("GET /favicon.ico 404 242")
  234. sleep(1)
  235. log.debug(
  236. "JSONRPC request\n--> %r\n<-- %r",
  237. {
  238. "version": "1.1",
  239. "method": "confirmFruitPurchase",
  240. "params": [["apple", "orange", "mangoes", "pomelo"], 1.123],
  241. "id": "194521489",
  242. },
  243. {"version": "1.1", "result": True, "error": None, "id": "194521489"},
  244. )
  245. log.debug(
  246. "Loading configuration file /adasd/asdasd/qeqwe/qwrqwrqwr/sdgsdgsdg/werwerwer/dfgerert/ertertert/ertetert/werwerwer"
  247. )
  248. log.error("Unable to find 'pomelo' in database!")
  249. log.info("POST /jsonrpc/ 200 65532")
  250. log.info("POST /admin/ 401 42234")
  251. log.warning("password was rejected for admin site.")
  252. def divide() -> None:
  253. number = 1
  254. divisor = 0
  255. foos = ["foo"] * 100
  256. log.debug("in divide")
  257. try:
  258. number / divisor
  259. except:
  260. log.exception("An error of some kind occurred!")
  261. divide()
  262. sleep(1)
  263. log.critical("Out of memory!")
  264. log.info("Server exited with code=-1")
  265. log.info("[bold]EXITING...[/bold]", extra=dict(markup=True))