Mercurial > libervia-backend
comparison libervia/backend/core/log_config.py @ 4071:4b842c1fb686
refactoring: renamed `sat` package to `libervia.backend`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 11:49:51 +0200 |
parents | sat/core/log_config.py@524856bd7b19 |
children | 15055a00162c |
comparison
equal
deleted
inserted
replaced
4070:d10748475025 | 4071:4b842c1fb686 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 | |
4 # Libervia: an XMPP client | |
5 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 """High level logging functions""" | |
21 # XXX: this module use standard logging module when possible, but as SàT can work in different cases where logging is not the best choice (twisted, pyjamas, etc), it is necessary to have a dedicated module. Additional feature like environment variables and colors are also managed. | |
22 | |
23 from libervia.backend.core.constants import Const as C | |
24 from libervia.backend.core import log | |
25 | |
26 | |
27 class TwistedLogger(log.Logger): | |
28 colors = True | |
29 force_colors = False | |
30 | |
31 def __init__(self, *args, **kwargs): | |
32 super().__init__(*args, **kwargs) | |
33 from twisted.logger import Logger | |
34 self.twisted_log = Logger() | |
35 | |
36 def out(self, message, level=None, **kwargs): | |
37 """Actually log the message | |
38 | |
39 @param message: formatted message | |
40 """ | |
41 self.twisted_log.emit( | |
42 level=self.level_map[level], | |
43 format=message, | |
44 sat_logged=True, | |
45 **kwargs, | |
46 ) | |
47 | |
48 | |
49 class ConfigureBasic(log.ConfigureBase): | |
50 def configure_colors(self, colors, force_colors, levels_taints_dict): | |
51 super(ConfigureBasic, self).configure_colors( | |
52 colors, force_colors, levels_taints_dict | |
53 ) | |
54 if colors: | |
55 import sys | |
56 | |
57 try: | |
58 isatty = sys.stdout.isatty() | |
59 except AttributeError: | |
60 isatty = False | |
61 # FIXME: isatty should be tested on each handler, not globaly | |
62 if (force_colors or isatty): | |
63 # we need colors | |
64 log.Logger.post_treat = lambda logger, level, message: self.ansi_colors( | |
65 level, message | |
66 ) | |
67 elif force_colors: | |
68 raise ValueError("force_colors can't be used if colors is False") | |
69 | |
70 @staticmethod | |
71 def get_profile(): | |
72 """Try to find profile value using introspection""" | |
73 import inspect | |
74 | |
75 stack = inspect.stack() | |
76 current_path = stack[0][1] | |
77 for frame_data in stack[:-1]: | |
78 if frame_data[1] != current_path: | |
79 if ( | |
80 log.backend == C.LOG_BACKEND_STANDARD | |
81 and "/logging/__init__.py" in frame_data[1] | |
82 ): | |
83 continue | |
84 break | |
85 | |
86 frame = frame_data[0] | |
87 args = inspect.getargvalues(frame) | |
88 try: | |
89 profile = args.locals.get("profile") or args.locals["profile_key"] | |
90 except (TypeError, KeyError): | |
91 try: | |
92 try: | |
93 profile = args.locals["self"].profile | |
94 except AttributeError: | |
95 try: | |
96 profile = args.locals["self"].parent.profile | |
97 except AttributeError: | |
98 profile = args.locals[ | |
99 "self" | |
100 ].host.profile # used in quick_frontend for single profile configuration | |
101 except Exception: | |
102 # we can't find profile, we return an empty value | |
103 profile = "" | |
104 return profile | |
105 | |
106 | |
107 class ConfigureTwisted(ConfigureBasic): | |
108 LOGGER_CLASS = TwistedLogger | |
109 | |
110 def pre_treatment(self): | |
111 from twisted import logger | |
112 global logger | |
113 self.level_map = { | |
114 C.LOG_LVL_DEBUG: logger.LogLevel.debug, | |
115 C.LOG_LVL_INFO: logger.LogLevel.info, | |
116 C.LOG_LVL_WARNING: logger.LogLevel.warn, | |
117 C.LOG_LVL_ERROR: logger.LogLevel.error, | |
118 C.LOG_LVL_CRITICAL: logger.LogLevel.critical, | |
119 } | |
120 self.LOGGER_CLASS.level_map = self.level_map | |
121 | |
122 def configure_level(self, level): | |
123 self.level = self.level_map[level] | |
124 | |
125 def configure_output(self, output): | |
126 import sys | |
127 from twisted.python import logfile | |
128 self.log_publisher = logger.LogPublisher() | |
129 | |
130 if output is None: | |
131 output = C.LOG_OPT_OUTPUT_SEP + C.LOG_OPT_OUTPUT_DEFAULT | |
132 self.manage_outputs(output) | |
133 | |
134 if C.LOG_OPT_OUTPUT_DEFAULT in log.handlers: | |
135 if self.backend_data is None: | |
136 raise ValueError( | |
137 "You must pass options as backend_data with Twisted backend" | |
138 ) | |
139 options = self.backend_data | |
140 log_file = logfile.LogFile.fromFullPath(options['logfile']) | |
141 self.log_publisher.addObserver( | |
142 logger.FileLogObserver(log_file, self.text_formatter)) | |
143 # we also want output to stdout if we are in debug or nodaemon mode | |
144 if options.get("nodaemon", False) or options.get("debug", False): | |
145 self.log_publisher.addObserver( | |
146 logger.FileLogObserver(sys.stdout, self.text_formatter)) | |
147 | |
148 if C.LOG_OPT_OUTPUT_FILE in log.handlers: | |
149 | |
150 for path in log.handlers[C.LOG_OPT_OUTPUT_FILE]: | |
151 log_file = ( | |
152 sys.stdout if path == "-" else logfile.LogFile.fromFullPath(path) | |
153 ) | |
154 self.log_publisher.addObserver( | |
155 logger.FileLogObserver(log_file, self.text_formatter)) | |
156 | |
157 if C.LOG_OPT_OUTPUT_MEMORY in log.handlers: | |
158 raise NotImplementedError( | |
159 "Memory observer is not implemented in Twisted backend" | |
160 ) | |
161 | |
162 def configure_colors(self, colors, force_colors, levels_taints_dict): | |
163 super(ConfigureTwisted, self).configure_colors( | |
164 colors, force_colors, levels_taints_dict | |
165 ) | |
166 self.LOGGER_CLASS.colors = colors | |
167 self.LOGGER_CLASS.force_colors = force_colors | |
168 if force_colors and not colors: | |
169 raise ValueError("colors must be True if force_colors is True") | |
170 | |
171 def post_treatment(self): | |
172 """Install twistedObserver which manage non SàT logs""" | |
173 # from twisted import logger | |
174 import sys | |
175 filtering_obs = logger.FilteringLogObserver( | |
176 observer=self.log_publisher, | |
177 predicates=[ | |
178 logger.LogLevelFilterPredicate(self.level), | |
179 ] | |
180 ) | |
181 logger.globalLogBeginner.beginLoggingTo([filtering_obs]) | |
182 | |
183 def text_formatter(self, event): | |
184 if event.get('sat_logged', False): | |
185 timestamp = ''.join([logger.formatTime(event.get("log_time", None)), " "]) | |
186 return f"{timestamp}{event.get('log_format', '')}\n" | |
187 else: | |
188 eventText = logger.eventAsText( | |
189 event, includeSystem=True) | |
190 if not eventText: | |
191 return None | |
192 return eventText.replace("\n", "\n\t") + "\n" | |
193 | |
194 | |
195 class ConfigureStandard(ConfigureBasic): | |
196 def __init__( | |
197 self, | |
198 level=None, | |
199 fmt=None, | |
200 output=None, | |
201 logger=None, | |
202 colors=False, | |
203 levels_taints_dict=None, | |
204 force_colors=False, | |
205 backend_data=None, | |
206 ): | |
207 if fmt is None: | |
208 fmt = C.LOG_OPT_FORMAT[1] | |
209 if output is None: | |
210 output = C.LOG_OPT_OUTPUT[1] | |
211 super(ConfigureStandard, self).__init__( | |
212 level, | |
213 fmt, | |
214 output, | |
215 logger, | |
216 colors, | |
217 levels_taints_dict, | |
218 force_colors, | |
219 backend_data, | |
220 ) | |
221 | |
222 def pre_treatment(self): | |
223 """We use logging methods directly, instead of using Logger""" | |
224 import logging | |
225 | |
226 log.getLogger = logging.getLogger | |
227 log.debug = logging.debug | |
228 log.info = logging.info | |
229 log.warning = logging.warning | |
230 log.error = logging.error | |
231 log.critical = logging.critical | |
232 | |
233 def configure_level(self, level): | |
234 if level is None: | |
235 level = C.LOG_LVL_DEBUG | |
236 self.level = level | |
237 | |
238 def configure_format(self, fmt): | |
239 super(ConfigureStandard, self).configure_format(fmt) | |
240 import logging | |
241 | |
242 class SatFormatter(logging.Formatter): | |
243 """Formatter which manage SàT specificities""" | |
244 _format = fmt | |
245 _with_profile = "%(profile)s" in fmt | |
246 | |
247 def __init__(self, can_colors=False): | |
248 super(SatFormatter, self).__init__(self._format) | |
249 self.can_colors = can_colors | |
250 | |
251 def format(self, record): | |
252 if self._with_profile: | |
253 record.profile = ConfigureStandard.get_profile() | |
254 do_color = self.with_colors and (self.can_colors or self.force_colors) | |
255 if ConfigureStandard._color_location: | |
256 # we copy raw formatting strings for color_* | |
257 # as formatting is handled in ansi_colors in this case | |
258 if do_color: | |
259 record.color_start = log.COLOR_START | |
260 record.color_end = log.COLOR_END | |
261 else: | |
262 record.color_start = record.color_end = "" | |
263 s = super(SatFormatter, self).format(record) | |
264 if do_color: | |
265 s = ConfigureStandard.ansi_colors(record.levelname, s) | |
266 return s | |
267 | |
268 self.formatterClass = SatFormatter | |
269 | |
270 def configure_output(self, output): | |
271 self.manage_outputs(output) | |
272 | |
273 def configure_logger(self, logger): | |
274 self.name_filter = log.FilterName(logger) if logger else None | |
275 | |
276 def configure_colors(self, colors, force_colors, levels_taints_dict): | |
277 super(ConfigureStandard, self).configure_colors( | |
278 colors, force_colors, levels_taints_dict | |
279 ) | |
280 self.formatterClass.with_colors = colors | |
281 self.formatterClass.force_colors = force_colors | |
282 if not colors and force_colors: | |
283 raise ValueError("force_colors can't be used if colors is False") | |
284 | |
285 def _add_handler(self, root_logger, hdlr, can_colors=False): | |
286 hdlr.setFormatter(self.formatterClass(can_colors)) | |
287 root_logger.addHandler(hdlr) | |
288 root_logger.setLevel(self.level) | |
289 if self.name_filter is not None: | |
290 hdlr.addFilter(self.name_filter) | |
291 | |
292 def post_treatment(self): | |
293 import logging | |
294 | |
295 root_logger = logging.getLogger() | |
296 if len(root_logger.handlers) == 0: | |
297 for handler, options in list(log.handlers.items()): | |
298 if handler == C.LOG_OPT_OUTPUT_DEFAULT: | |
299 hdlr = logging.StreamHandler() | |
300 try: | |
301 can_colors = hdlr.stream.isatty() | |
302 except AttributeError: | |
303 can_colors = False | |
304 self._add_handler(root_logger, hdlr, can_colors=can_colors) | |
305 elif handler == C.LOG_OPT_OUTPUT_MEMORY: | |
306 from logging.handlers import BufferingHandler | |
307 | |
308 class SatMemoryHandler(BufferingHandler): | |
309 def emit(self, record): | |
310 super(SatMemoryHandler, self).emit(self.format(record)) | |
311 | |
312 hdlr = SatMemoryHandler(options) | |
313 log.handlers[ | |
314 handler | |
315 ] = ( | |
316 hdlr | |
317 ) # we keep a reference to the handler to read the buffer later | |
318 self._add_handler(root_logger, hdlr, can_colors=False) | |
319 elif handler == C.LOG_OPT_OUTPUT_FILE: | |
320 import os.path | |
321 | |
322 for path in options: | |
323 hdlr = logging.FileHandler(os.path.expanduser(path)) | |
324 self._add_handler(root_logger, hdlr, can_colors=False) | |
325 else: | |
326 raise ValueError("Unknown handler type") | |
327 else: | |
328 root_logger.warning("Handlers already set on root logger") | |
329 | |
330 @staticmethod | |
331 def memory_get(size=None): | |
332 """Return buffered logs | |
333 | |
334 @param size: number of logs to return | |
335 """ | |
336 mem_handler = log.handlers[C.LOG_OPT_OUTPUT_MEMORY] | |
337 return ( | |
338 log_msg for log_msg in mem_handler.buffer[size if size is None else -size :] | |
339 ) | |
340 | |
341 | |
342 log.configure_cls[C.LOG_BACKEND_BASIC] = ConfigureBasic | |
343 log.configure_cls[C.LOG_BACKEND_TWISTED] = ConfigureTwisted | |
344 log.configure_cls[C.LOG_BACKEND_STANDARD] = ConfigureStandard | |
345 | |
346 | |
347 def configure(backend, **options): | |
348 """Configure logging behaviour | |
349 @param backend: can be: | |
350 C.LOG_BACKEND_STANDARD: use standard logging module | |
351 C.LOG_BACKEND_TWISTED: use twisted logging module (with standard logging observer) | |
352 C.LOG_BACKEND_BASIC: use a basic print based logging | |
353 C.LOG_BACKEND_CUSTOM: use a given Logger subclass | |
354 """ | |
355 return log.configure(backend, **options) | |
356 | |
357 | |
358 def _parse_options(options): | |
359 """Parse string options as given in conf or environment variable, and return expected python value | |
360 | |
361 @param options (dict): options with (key: name, value: string value) | |
362 """ | |
363 COLORS = C.LOG_OPT_COLORS[0] | |
364 LEVEL = C.LOG_OPT_LEVEL[0] | |
365 | |
366 if COLORS in options: | |
367 if options[COLORS].lower() in ("1", "true"): | |
368 options[COLORS] = True | |
369 elif options[COLORS] == "force": | |
370 options[COLORS] = True | |
371 options["force_colors"] = True | |
372 else: | |
373 options[COLORS] = False | |
374 if LEVEL in options: | |
375 level = options[LEVEL].upper() | |
376 if level not in C.LOG_LEVELS: | |
377 level = C.LOG_LVL_INFO | |
378 options[LEVEL] = level | |
379 | |
380 | |
381 def sat_configure(backend=C.LOG_BACKEND_STANDARD, const=None, backend_data=None): | |
382 """Configure logging system for SàT, can be used by frontends | |
383 | |
384 logs conf is read in SàT conf, then in environment variables. It must be done before Memory init | |
385 @param backend: backend to use, it can be: | |
386 - C.LOG_BACKEND_BASIC: print based backend | |
387 - C.LOG_BACKEND_TWISTED: Twisted logging backend | |
388 - C.LOG_BACKEND_STANDARD: standard logging backend | |
389 @param const: Const class to use instead of sat.core.constants.Const (mainly used to change default values) | |
390 """ | |
391 if const is not None: | |
392 global C | |
393 C = const | |
394 log.C = const | |
395 from libervia.backend.tools import config | |
396 import os | |
397 | |
398 log_conf = {} | |
399 sat_conf = config.parse_main_conf() | |
400 for opt_name, opt_default in C.LOG_OPTIONS(): | |
401 try: | |
402 log_conf[opt_name] = os.environ[ | |
403 "".join((C.ENV_PREFIX, C.LOG_OPT_PREFIX.upper(), opt_name.upper())) | |
404 ] | |
405 except KeyError: | |
406 log_conf[opt_name] = config.config_get( | |
407 sat_conf, C.LOG_OPT_SECTION, C.LOG_OPT_PREFIX + opt_name, opt_default | |
408 ) | |
409 | |
410 _parse_options(log_conf) | |
411 configure(backend, backend_data=backend_data, **log_conf) |