|
import logging |
|
from datetime import datetime |
|
from logging import Handler, LogRecord |
|
from pathlib import Path |
|
from types import ModuleType |
|
from typing import ClassVar, List, Optional, Iterable, Type, Union |
|
|
|
from . import get_console |
|
from ._log_render import LogRender, FormatTimeCallable |
|
from .console import Console, ConsoleRenderable |
|
from .highlighter import Highlighter, ReprHighlighter |
|
from .text import Text |
|
from .traceback import Traceback |
|
|
|
|
|
class RichHandler(Handler): |
|
"""A logging handler that renders output with Rich. The time / level / message and file are displayed in columns. |
|
The level is color coded, and the message is syntax highlighted. |
|
|
|
Note: |
|
Be careful when enabling console markup in log messages if you have configured logging for libraries not |
|
under your control. If a dependency writes messages containing square brackets, it may not produce the intended output. |
|
|
|
Args: |
|
level (Union[int, str], optional): Log level. Defaults to logging.NOTSET. |
|
console (:class:`~rich.console.Console`, optional): Optional console instance to write logs. |
|
Default will use a global console instance writing to stdout. |
|
show_time (bool, optional): Show a column for the time. Defaults to True. |
|
omit_repeated_times (bool, optional): Omit repetition of the same time. Defaults to True. |
|
show_level (bool, optional): Show a column for the level. Defaults to True. |
|
show_path (bool, optional): Show the path to the original log call. Defaults to True. |
|
enable_link_path (bool, optional): Enable terminal link of path column to file. Defaults to True. |
|
highlighter (Highlighter, optional): Highlighter to style log messages, or None to use ReprHighlighter. Defaults to None. |
|
markup (bool, optional): Enable console markup in log messages. Defaults to False. |
|
rich_tracebacks (bool, optional): Enable rich tracebacks with syntax highlighting and formatting. Defaults to False. |
|
tracebacks_width (Optional[int], optional): Number of characters used to render tracebacks, or None for full width. Defaults to None. |
|
tracebacks_extra_lines (int, optional): Additional lines of code to render tracebacks, or None for full width. Defaults to None. |
|
tracebacks_theme (str, optional): Override pygments theme used in traceback. |
|
tracebacks_word_wrap (bool, optional): Enable word wrapping of long tracebacks lines. Defaults to True. |
|
tracebacks_show_locals (bool, optional): Enable display of locals in tracebacks. Defaults to False. |
|
tracebacks_suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback. |
|
locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. |
|
Defaults to 10. |
|
locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80. |
|
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] ". |
|
keywords (List[str], optional): List of words to highlight instead of ``RichHandler.KEYWORDS``. |
|
""" |
|
|
|
KEYWORDS: ClassVar[Optional[List[str]]] = [ |
|
"GET", |
|
"POST", |
|
"HEAD", |
|
"PUT", |
|
"DELETE", |
|
"OPTIONS", |
|
"TRACE", |
|
"PATCH", |
|
] |
|
HIGHLIGHTER_CLASS: ClassVar[Type[Highlighter]] = ReprHighlighter |
|
|
|
def __init__( |
|
self, |
|
level: Union[int, str] = logging.NOTSET, |
|
console: Optional[Console] = None, |
|
*, |
|
show_time: bool = True, |
|
omit_repeated_times: bool = True, |
|
show_level: bool = True, |
|
show_path: bool = True, |
|
enable_link_path: bool = True, |
|
highlighter: Optional[Highlighter] = None, |
|
markup: bool = False, |
|
rich_tracebacks: bool = False, |
|
tracebacks_width: Optional[int] = None, |
|
tracebacks_extra_lines: int = 3, |
|
tracebacks_theme: Optional[str] = None, |
|
tracebacks_word_wrap: bool = True, |
|
tracebacks_show_locals: bool = False, |
|
tracebacks_suppress: Iterable[Union[str, ModuleType]] = (), |
|
locals_max_length: int = 10, |
|
locals_max_string: int = 80, |
|
log_time_format: Union[str, FormatTimeCallable] = "[%x %X]", |
|
keywords: Optional[List[str]] = None, |
|
) -> None: |
|
super().__init__(level=level) |
|
self.console = console or get_console() |
|
self.highlighter = highlighter or self.HIGHLIGHTER_CLASS() |
|
self._log_render = LogRender( |
|
show_time=show_time, |
|
show_level=show_level, |
|
show_path=show_path, |
|
time_format=log_time_format, |
|
omit_repeated_times=omit_repeated_times, |
|
level_width=None, |
|
) |
|
self.enable_link_path = enable_link_path |
|
self.markup = markup |
|
self.rich_tracebacks = rich_tracebacks |
|
self.tracebacks_width = tracebacks_width |
|
self.tracebacks_extra_lines = tracebacks_extra_lines |
|
self.tracebacks_theme = tracebacks_theme |
|
self.tracebacks_word_wrap = tracebacks_word_wrap |
|
self.tracebacks_show_locals = tracebacks_show_locals |
|
self.tracebacks_suppress = tracebacks_suppress |
|
self.locals_max_length = locals_max_length |
|
self.locals_max_string = locals_max_string |
|
self.keywords = keywords |
|
|
|
def get_level_text(self, record: LogRecord) -> Text: |
|
"""Get the level name from the record. |
|
|
|
Args: |
|
record (LogRecord): LogRecord instance. |
|
|
|
Returns: |
|
Text: A tuple of the style and level name. |
|
""" |
|
level_name = record.levelname |
|
level_text = Text.styled( |
|
level_name.ljust(8), f"logging.level.{level_name.lower()}" |
|
) |
|
return level_text |
|
|
|
def emit(self, record: LogRecord) -> None: |
|
"""Invoked by logging.""" |
|
message = self.format(record) |
|
traceback = None |
|
if ( |
|
self.rich_tracebacks |
|
and record.exc_info |
|
and record.exc_info != (None, None, None) |
|
): |
|
exc_type, exc_value, exc_traceback = record.exc_info |
|
assert exc_type is not None |
|
assert exc_value is not None |
|
traceback = Traceback.from_exception( |
|
exc_type, |
|
exc_value, |
|
exc_traceback, |
|
width=self.tracebacks_width, |
|
extra_lines=self.tracebacks_extra_lines, |
|
theme=self.tracebacks_theme, |
|
word_wrap=self.tracebacks_word_wrap, |
|
show_locals=self.tracebacks_show_locals, |
|
locals_max_length=self.locals_max_length, |
|
locals_max_string=self.locals_max_string, |
|
suppress=self.tracebacks_suppress, |
|
) |
|
message = record.getMessage() |
|
if self.formatter: |
|
record.message = record.getMessage() |
|
formatter = self.formatter |
|
if hasattr(formatter, "usesTime") and formatter.usesTime(): |
|
record.asctime = formatter.formatTime(record, formatter.datefmt) |
|
message = formatter.formatMessage(record) |
|
|
|
message_renderable = self.render_message(record, message) |
|
log_renderable = self.render( |
|
record=record, traceback=traceback, message_renderable=message_renderable |
|
) |
|
try: |
|
self.console.print(log_renderable) |
|
except Exception: |
|
self.handleError(record) |
|
|
|
def render_message(self, record: LogRecord, message: str) -> "ConsoleRenderable": |
|
"""Render message text in to Text. |
|
|
|
record (LogRecord): logging Record. |
|
message (str): String containing log message. |
|
|
|
Returns: |
|
ConsoleRenderable: Renderable to display log message. |
|
""" |
|
use_markup = getattr(record, "markup", self.markup) |
|
message_text = Text.from_markup(message) if use_markup else Text(message) |
|
|
|
highlighter = getattr(record, "highlighter", self.highlighter) |
|
if highlighter: |
|
message_text = highlighter(message_text) |
|
|
|
if self.keywords is None: |
|
self.keywords = self.KEYWORDS |
|
|
|
if self.keywords: |
|
message_text.highlight_words(self.keywords, "logging.keyword") |
|
|
|
return message_text |
|
|
|
def render( |
|
self, |
|
*, |
|
record: LogRecord, |
|
traceback: Optional[Traceback], |
|
message_renderable: "ConsoleRenderable", |
|
) -> "ConsoleRenderable": |
|
"""Render log for display. |
|
|
|
Args: |
|
record (LogRecord): logging Record. |
|
traceback (Optional[Traceback]): Traceback instance or None for no Traceback. |
|
message_renderable (ConsoleRenderable): Renderable (typically Text) containing log message contents. |
|
|
|
Returns: |
|
ConsoleRenderable: Renderable to display log. |
|
""" |
|
path = Path(record.pathname).name |
|
level = self.get_level_text(record) |
|
time_format = None if self.formatter is None else self.formatter.datefmt |
|
log_time = datetime.fromtimestamp(record.created) |
|
|
|
log_renderable = self._log_render( |
|
self.console, |
|
[message_renderable] if not traceback else [message_renderable, traceback], |
|
log_time=log_time, |
|
time_format=time_format, |
|
level=level, |
|
path=path, |
|
line_no=record.lineno, |
|
link_path=record.pathname if self.enable_link_path else None, |
|
) |
|
return log_renderable |
|
|
|
|
|
if __name__ == "__main__": |
|
from time import sleep |
|
|
|
FORMAT = "%(message)s" |
|
|
|
logging.basicConfig( |
|
level="NOTSET", |
|
format=FORMAT, |
|
datefmt="[%X]", |
|
handlers=[RichHandler(rich_tracebacks=True, tracebacks_show_locals=True)], |
|
) |
|
log = logging.getLogger("rich") |
|
|
|
log.info("Server starting...") |
|
log.info("Listening on http://127.0.0.1:8080") |
|
sleep(1) |
|
|
|
log.info("GET /index.html 200 1298") |
|
log.info("GET /imgs/backgrounds/back1.jpg 200 54386") |
|
log.info("GET /css/styles.css 200 54386") |
|
log.warning("GET /favicon.ico 404 242") |
|
sleep(1) |
|
|
|
log.debug( |
|
"JSONRPC request\n--> %r\n<-- %r", |
|
{ |
|
"version": "1.1", |
|
"method": "confirmFruitPurchase", |
|
"params": [["apple", "orange", "mangoes", "pomelo"], 1.123], |
|
"id": "194521489", |
|
}, |
|
{"version": "1.1", "result": True, "error": None, "id": "194521489"}, |
|
) |
|
log.debug( |
|
"Loading configuration file /adasd/asdasd/qeqwe/qwrqwrqwr/sdgsdgsdg/werwerwer/dfgerert/ertertert/ertetert/werwerwer" |
|
) |
|
log.error("Unable to find 'pomelo' in database!") |
|
log.info("POST /jsonrpc/ 200 65532") |
|
log.info("POST /admin/ 401 42234") |
|
log.warning("password was rejected for admin site.") |
|
|
|
def divide() -> None: |
|
number = 1 |
|
divisor = 0 |
|
foos = ["foo"] * 100 |
|
log.debug("in divide") |
|
try: |
|
number / divisor |
|
except: |
|
log.exception("An error of some kind occurred!") |
|
|
|
divide() |
|
sleep(1) |
|
log.critical("Out of memory!") |
|
log.info("Server exited with code=-1") |
|
log.info("[bold]EXITING...[/bold]", extra=dict(markup=True)) |
|
|