You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

281 lines
11 KiB
Python

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__": # pragma: no cover
from time import sleep
FORMAT = "%(message)s"
# FORMAT = "%(asctime)-15s - %(levelname)s - %(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))