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)