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