|
1 | | -""" |
2 | | -A sample logger (requires structlog, tested with structlog==25.1.0) |
3 | | -
|
4 | | -Usage |
5 | | ------ |
6 | | -
|
7 | | ->>> from $package_name.log import configure_file_and_print_logger, get_logger |
8 | | ->>> configure_file_and_print_logger("app.log") |
9 | | ->>> # OR |
10 | | ->>> configure_print_logger() |
11 | | ->>> logger = get_logger() |
12 | | ->>> logger.info("Hello, world!") |
13 | | -""" |
14 | | - |
15 | 1 | import logging |
16 | | -from typing import Any, TextIO |
17 | | -import os |
| 2 | +from typing import Any |
18 | 3 | from pathlib import Path |
19 | | - |
| 4 | +from copy import deepcopy |
20 | 5 |
|
21 | 6 | import structlog |
22 | 7 | from structlog import WriteLogger, PrintLogger |
23 | 8 |
|
24 | 9 |
|
25 | | -class CustomLogger: |
| 10 | +class MultiRendererLoggerFactory: |
| 11 | + def __init__( |
| 12 | + self, |
| 13 | + *, |
| 14 | + console_processors: list, |
| 15 | + json_processors: list, |
| 16 | + console_renderer: Any, |
| 17 | + json_renderer: Any, |
| 18 | + console_logger: Any, |
| 19 | + json_logger: Any, |
| 20 | + ): |
| 21 | + self._console_processors = console_processors |
| 22 | + self._json_processors = json_processors |
| 23 | + self._console_renderer = console_renderer |
| 24 | + self._json_renderer = json_renderer |
| 25 | + self._console_logger = console_logger |
| 26 | + self._json_logger = json_logger |
| 27 | + |
| 28 | + def __call__(self, *args: Any) -> "MultiRendererLogger": |
| 29 | + return MultiRendererLogger( |
| 30 | + console_processors=self._console_processors, |
| 31 | + json_processors=self._json_processors, |
| 32 | + console_renderer=self._console_renderer, |
| 33 | + json_renderer=self._json_renderer, |
| 34 | + console_logger=self._console_logger, |
| 35 | + json_logger=self._json_logger, |
| 36 | + ) |
| 37 | + |
| 38 | + |
| 39 | +class MultiRendererLogger: |
26 | 40 | """ |
27 | | - A custom logger that writes to a file and prints to the console |
| 41 | + A custom logger that writes JSON to a file and pretty prints to the console |
28 | 42 | """ |
29 | 43 |
|
30 | | - def __init__(self, file: TextIO | None = None): |
31 | | - self._file = file |
32 | | - self._write_logger = WriteLogger(self._file) |
33 | | - self._print_logger = PrintLogger() |
34 | | - |
35 | | - def msg(self, message: str) -> None: |
36 | | - self._write_logger.msg(message) |
37 | | - self._print_logger.msg(message) |
| 44 | + def __init__( |
| 45 | + self, |
| 46 | + *, |
| 47 | + console_processors: list, |
| 48 | + json_processors: list, |
| 49 | + console_renderer: Any, |
| 50 | + json_renderer: Any, |
| 51 | + console_logger: Any, |
| 52 | + json_logger: Any, |
| 53 | + ): |
| 54 | + self._console_processors = console_processors |
| 55 | + self._json_processors = json_processors |
| 56 | + self._console_renderer = console_renderer |
| 57 | + self._json_renderer = json_renderer |
| 58 | + self._console_logger = console_logger |
| 59 | + self._json_logger = json_logger |
| 60 | + |
| 61 | + def msg(self, **kwargs) -> None: |
| 62 | + kwargs_json = deepcopy(kwargs) |
| 63 | + kwargs_console = deepcopy(kwargs) |
| 64 | + |
| 65 | + for processor in self._console_processors: |
| 66 | + processor(self, "logger", kwargs_console) |
| 67 | + |
| 68 | + for processor in self._json_processors: |
| 69 | + processor(self, "logger", kwargs_json) |
| 70 | + |
| 71 | + message_console = self._console_renderer(self, "logger", kwargs_console) |
| 72 | + message_json = self._json_renderer(self, "logger", kwargs_json) |
| 73 | + |
| 74 | + self._console_logger.msg(message_console) |
| 75 | + self._json_logger.msg(message_json) |
38 | 76 |
|
39 | 77 | log = debug = info = warn = warning = msg |
40 | 78 | fatal = failure = err = error = critical = exception = msg |
41 | 79 |
|
42 | 80 |
|
43 | | -class CustomLoggerFactory: |
44 | | - def __init__(self, file: TextIO | None = None): |
45 | | - self._file = file |
46 | | - |
47 | | - def __call__(self, *args: Any) -> CustomLogger: |
48 | | - return CustomLogger(self._file) |
| 81 | +def configure_multi_renderer_logger( |
| 82 | + file_path: str = "app.log", |
| 83 | + level: int = logging.INFO, |
| 84 | + colors: bool = True, |
| 85 | +) -> None: |
| 86 | + """ |
| 87 | + Configure a logger that writes JSON formatted logs to a file while using |
| 88 | + the console renderer for terminal output. |
49 | 89 |
|
| 90 | + Parameters |
| 91 | + ---------- |
| 92 | + file_path : str, optional |
| 93 | + Path to the log file, by default "app.log" |
| 94 | + """ |
| 95 | + processors = [ |
| 96 | + structlog.contextvars.merge_contextvars, |
| 97 | + structlog.processors.add_log_level, |
| 98 | + structlog.processors.StackInfoRenderer(), |
| 99 | + # to add filename, function name, and line number to the log record |
| 100 | + structlog.processors.CallsiteParameterAdder( |
| 101 | + [ |
| 102 | + structlog.processors.CallsiteParameter.FILENAME, |
| 103 | + structlog.processors.CallsiteParameter.FUNC_NAME, |
| 104 | + structlog.processors.CallsiteParameter.LINENO, |
| 105 | + ], |
| 106 | + ), |
| 107 | + structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=False), |
| 108 | + # NOTE: we don't pass a renderer here, since we'll use a different |
| 109 | + # renderer for the console and the file |
| 110 | + ] |
| 111 | + |
| 112 | + json_processors = [ |
| 113 | + # dict tracebacks renders tracebacks as a dict |
| 114 | + # structlog.processors.dict_tracebacks, |
| 115 | + # format exc info renders tracebacks as a string |
| 116 | + structlog.processors.format_exc_info, |
| 117 | + ] |
| 118 | + |
| 119 | + console_processors = [] |
| 120 | + |
| 121 | + if colors: |
| 122 | + console_processors.append(structlog.dev.set_exc_info) |
| 123 | + else: |
| 124 | + console_processors.append(structlog.processors.format_exc_info) |
50 | 125 |
|
51 | | -def configure_file_and_print_logger(file_path: str = "app.log") -> None: |
52 | 126 | structlog.configure( |
53 | | - processors=[ |
54 | | - structlog.contextvars.merge_contextvars, |
55 | | - structlog.processors.add_log_level, |
56 | | - structlog.processors.StackInfoRenderer(), |
57 | | - structlog.dev.set_exc_info, |
58 | | - # to add filename, function name, and line number to the log record |
59 | | - structlog.processors.CallsiteParameterAdder( |
60 | | - [ |
61 | | - structlog.processors.CallsiteParameter.FILENAME, |
62 | | - structlog.processors.CallsiteParameter.FUNC_NAME, |
63 | | - structlog.processors.CallsiteParameter.LINENO, |
64 | | - ], |
65 | | - ), |
66 | | - structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=False), |
67 | | - structlog.processors.JSONRenderer(), |
68 | | - ], |
69 | | - wrapper_class=structlog.make_filtering_bound_logger(logging.NOTSET), |
| 127 | + wrapper_class=structlog.make_filtering_bound_logger(level), |
70 | 128 | context_class=dict, |
71 | | - logger_factory=CustomLoggerFactory(open(file_path, "at")), |
72 | | - cache_logger_on_first_use=False, |
73 | | - ) |
74 | | - |
75 | | - |
76 | | -def configure_file_logger(file_path: str = "app.log") -> None: |
77 | | - structlog.configure( |
78 | | - processors=[ |
79 | | - structlog.contextvars.merge_contextvars, |
80 | | - structlog.processors.add_log_level, |
81 | | - structlog.processors.StackInfoRenderer(), |
82 | | - structlog.dev.set_exc_info, |
83 | | - structlog.processors.CallsiteParameterAdder( |
84 | | - [ |
85 | | - structlog.processors.CallsiteParameter.FILENAME, |
86 | | - structlog.processors.CallsiteParameter.FUNC_NAME, |
87 | | - structlog.processors.CallsiteParameter.LINENO, |
88 | | - ], |
| 129 | + logger_factory=MultiRendererLoggerFactory( |
| 130 | + console_processors=console_processors, |
| 131 | + json_processors=json_processors, |
| 132 | + console_renderer=structlog.dev.ConsoleRenderer( |
| 133 | + colors=colors, |
| 134 | + exception_formatter=structlog.dev.RichTracebackFormatter( |
| 135 | + show_locals=False, |
| 136 | + max_frames=3, |
| 137 | + width=80, |
| 138 | + ), |
89 | 139 | ), |
90 | | - structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=False), |
91 | | - structlog.processors.JSONRenderer(), |
92 | | - ], |
93 | | - wrapper_class=structlog.make_filtering_bound_logger(logging.NOTSET), |
94 | | - context_class=dict, |
95 | | - logger_factory=structlog.WriteLoggerFactory(file=Path(file_path).open("at")), |
96 | | - cache_logger_on_first_use=False, |
97 | | - ) |
98 | | - |
99 | | - |
100 | | -def configure_print_logger() -> None: |
101 | | - structlog.configure( |
102 | | - processors=[ |
103 | | - structlog.contextvars.merge_contextvars, |
104 | | - structlog.processors.add_log_level, |
105 | | - structlog.processors.StackInfoRenderer(), |
106 | | - structlog.dev.set_exc_info, |
107 | | - structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=False), |
108 | | - structlog.dev.ConsoleRenderer(), |
109 | | - ], |
110 | | - wrapper_class=structlog.make_filtering_bound_logger(logging.NOTSET), |
111 | | - context_class=dict, |
112 | | - logger_factory=structlog.PrintLoggerFactory(), |
| 140 | + json_renderer=structlog.processors.JSONRenderer(), |
| 141 | + json_logger=WriteLogger(Path(file_path).open("at")), |
| 142 | + console_logger=PrintLogger(), |
| 143 | + ), |
113 | 144 | cache_logger_on_first_use=False, |
114 | | - ) |
115 | | - |
116 | | - |
117 | | -def configure_no_logging() -> None: |
118 | | - structlog.configure( |
119 | | - logger_factory=structlog.PrintLoggerFactory(open(os.devnull, "w")), |
| 145 | + processors=processors, |
120 | 146 | ) |
121 | 147 |
|
122 | 148 |
|
|
0 commit comments