codekingpro/portable-devtools
114k
1import logging2from datetime import datetime3from logging import Handler, LogRecord4from pathlib import Path5from types import ModuleType6from typing import ClassVar, Iterable, List, Optional, Type, Union7 8from rich._null_file import NullFile9 10from . import get_console11from ._log_render import FormatTimeCallable, LogRender12from .console import Console, ConsoleRenderable13from .highlighter import Highlighter, ReprHighlighter14from .text import Text15from .traceback import Traceback16 17 18class 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 not24 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]] = ReprHighlighter62 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_path99 self.markup = markup100 self.rich_tracebacks = rich_tracebacks101 self.tracebacks_width = tracebacks_width102 self.tracebacks_extra_lines = tracebacks_extra_lines103 self.tracebacks_theme = tracebacks_theme104 self.tracebacks_word_wrap = tracebacks_word_wrap105 self.tracebacks_show_locals = tracebacks_show_locals106 self.tracebacks_suppress = tracebacks_suppress107 self.locals_max_length = locals_max_length108 self.locals_max_string = locals_max_string109 self.keywords = keywords110 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.levelname121 level_text = Text.styled(122 level_name.ljust(8), f"logging.level.{level_name.lower()}"123 )124 return level_text125 126 def emit(self, record: LogRecord) -> None:127 """Invoked by logging."""128 message = self.format(record)129 traceback = None130 if (131 self.rich_tracebacks132 and record.exc_info133 and record.exc_info != (None, None, None)134 ):135 exc_type, exc_value, exc_traceback = record.exc_info136 assert exc_type is not None137 assert exc_value is not None138 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.formatter155 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_renderable162 )163 if isinstance(self.console.file, NullFile):164 # Handles pythonw, where stdout/stderr are null, and we return NullFile165 # instance from Console.file. In this case, we still want to make a log record166 # 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.KEYWORDS193 194 if self.keywords:195 message_text.highlight_words(self.keywords, "logging.keyword")196 197 return message_text198 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).name217 level = self.get_level_text(record)218 time_format = None if self.formatter is None else self.formatter.datefmt219 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_renderable232 233 234if __name__ == "__main__": # pragma: no cover235 from time import sleep236 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 = 1277 divisor = 0278 foos = ["foo"] * 100279 log.debug("in divide")280 try:281 number / divisor282 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))290 