logging.py 11 KB

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