Mercurial > libervia-backend
comparison sat/core/log_config.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | src/core/log_config.py@0046283a285d |
children | 56f94936df1e |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SàT: a XMPP client | |
5 # Copyright (C) 2009-2018 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 sat.core.constants import Const as C | |
24 from sat.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(TwistedLogger, self).__init__(*args, **kwargs) | |
33 from twisted.python import log as twisted_log | |
34 self.twisted_log = twisted_log | |
35 | |
36 def out(self, message, level=None): | |
37 """Actually log the message | |
38 | |
39 @param message: formatted message | |
40 """ | |
41 self.twisted_log.msg(message.encode('utf-8', 'ignore'), sat_logged=True, level=level) | |
42 | |
43 | |
44 class ConfigureBasic(log.ConfigureBase): | |
45 | |
46 def configureColors(self, colors, force_colors, levels_taints_dict): | |
47 super(ConfigureBasic, self).configureColors(colors, force_colors, levels_taints_dict) | |
48 if colors: | |
49 import sys | |
50 try: | |
51 isatty = sys.stdout.isatty() | |
52 except AttributeError: | |
53 isatty = False | |
54 if force_colors or isatty: # FIXME: isatty should be tested on each handler, not globaly | |
55 # we need colors | |
56 log.Logger.post_treat = lambda logger, level, message: self.ansiColors(level, message) | |
57 elif force_colors: | |
58 raise ValueError("force_colors can't be used if colors is False") | |
59 | |
60 @staticmethod | |
61 def getProfile(): | |
62 """Try to find profile value using introspection""" | |
63 import inspect | |
64 stack = inspect.stack() | |
65 current_path = stack[0][1] | |
66 for frame_data in stack[:-1]: | |
67 if frame_data[1] != current_path: | |
68 if log.backend == C.LOG_BACKEND_STANDARD and "/logging/__init__.py" in frame_data[1]: | |
69 continue | |
70 break | |
71 | |
72 frame = frame_data[0] | |
73 args = inspect.getargvalues(frame) | |
74 try: | |
75 profile = args.locals.get('profile') or args.locals['profile_key'] | |
76 except (TypeError, KeyError): | |
77 try: | |
78 try: | |
79 profile = args.locals['self'].profile | |
80 except AttributeError: | |
81 try: | |
82 profile = args.locals['self'].parent.profile | |
83 except AttributeError: | |
84 profile = args.locals['self'].host.profile # used in quick_frontend for single profile configuration | |
85 except Exception: | |
86 # we can't find profile, we return an empty value | |
87 profile = '' | |
88 return profile | |
89 | |
90 | |
91 class ConfigureTwisted(ConfigureBasic): | |
92 LOGGER_CLASS = TwistedLogger | |
93 | |
94 def changeObserver(self, observer, can_colors=False): | |
95 """Install a hook on observer to manage SàT specificities | |
96 | |
97 @param observer: original observer to hook | |
98 @param can_colors: True if observer can display ansi colors | |
99 """ | |
100 def observer_hook(event): | |
101 """redirect non SàT log to twisted_logger, and add colors when possible""" | |
102 if 'sat_logged' in event: # we only want our own logs, other are managed by twistedObserver | |
103 # we add colors if possible | |
104 if (can_colors and self.LOGGER_CLASS.colors) or self.LOGGER_CLASS.force_colors: | |
105 message = event.get('message', tuple()) | |
106 level = event.get('level', C.LOG_LVL_INFO) | |
107 if message: | |
108 event['message'] = (self.ansiColors(level, ''.join(message)),) # must be a tuple | |
109 observer(event) # we can now call the original observer | |
110 | |
111 return observer_hook | |
112 | |
113 def changeFileLogObserver(self, observer): | |
114 """Install SàT hook for FileLogObserver | |
115 | |
116 if the output is a tty, we allow colors, else we don't | |
117 @param observer: original observer to hook | |
118 """ | |
119 log_obs = observer.__self__ | |
120 log_file = log_obs.write.__self__ | |
121 try: | |
122 can_colors = log_file.isatty() | |
123 except AttributeError: | |
124 can_colors = False | |
125 return self.changeObserver(observer, can_colors=can_colors) | |
126 | |
127 def installObserverHook(self, observer): | |
128 """Check observer type and install SàT hook when possible | |
129 | |
130 @param observer: observer to hook | |
131 @return: hooked observer or original one | |
132 """ | |
133 if hasattr(observer, '__self__'): | |
134 ori = observer | |
135 if isinstance(observer.__self__, self.twisted_log.FileLogObserver): | |
136 observer = self.changeFileLogObserver(observer) | |
137 elif isinstance(observer.__self__, self.twisted_log.DefaultObserver): | |
138 observer = self.changeObserver(observer, can_colors=True) | |
139 else: | |
140 # we use print because log system is not fully initialized | |
141 print("Unmanaged observer [%s]" % observer) | |
142 return observer | |
143 self.observers[ori] = observer | |
144 return observer | |
145 | |
146 def preTreatment(self): | |
147 """initialise needed attributes, and install observers hooks""" | |
148 self.observers = {} | |
149 from twisted.python import log as twisted_log | |
150 self.twisted_log = twisted_log | |
151 self.log_publisher = twisted_log.msg.__self__ | |
152 def addObserverObserver(self_logpub, other): | |
153 """Install hook so we know when a new observer is added""" | |
154 other = self.installObserverHook(other) | |
155 return self_logpub._originalAddObserver(other) | |
156 def removeObserverObserver(self_logpub, ori): | |
157 """removeObserver hook fix | |
158 | |
159 As we wrap the original observer, the original removeObserver may want to remove the original object instead of the wrapper, this method fix this | |
160 """ | |
161 if ori in self.observers: | |
162 self_logpub._originalRemoveObserver(self.observers[ori]) | |
163 else: | |
164 try: | |
165 self_logpub._originalRemoveObserver(ori) | |
166 except ValueError: | |
167 try: | |
168 ori in self.cleared_observers | |
169 except AttributeError: | |
170 raise ValueError("Unknown observer") | |
171 | |
172 # we replace addObserver/removeObserver by our own | |
173 twisted_log.LogPublisher._originalAddObserver = twisted_log.LogPublisher.addObserver | |
174 twisted_log.LogPublisher._originalRemoveObserver = twisted_log.LogPublisher.removeObserver | |
175 import types # see https://stackoverflow.com/a/4267590 (thx Chris Morgan/aaronasterling) | |
176 twisted_log.addObserver = types.MethodType(addObserverObserver, self.log_publisher, twisted_log.LogPublisher) | |
177 twisted_log.removeObserver = types.MethodType(removeObserverObserver, self.log_publisher, twisted_log.LogPublisher) | |
178 | |
179 # we now change existing observers | |
180 for idx, observer in enumerate(self.log_publisher.observers): | |
181 self.log_publisher.observers[idx] = self.installObserverHook(observer) | |
182 | |
183 def configureLevel(self, level): | |
184 self.LOGGER_CLASS.level = level | |
185 super(ConfigureTwisted, self).configureLevel(level) | |
186 | |
187 def configureOutput(self, output): | |
188 import sys | |
189 if output is None: | |
190 output = C.LOG_OPT_OUTPUT_SEP + C.LOG_OPT_OUTPUT_DEFAULT | |
191 self.manageOutputs(output) | |
192 addObserver = self.twisted_log.addObserver | |
193 | |
194 if C.LOG_OPT_OUTPUT_DEFAULT in log.handlers: | |
195 # default output is already managed, we just add output to stdout if we are in debug or nodaemon mode | |
196 if self.backend_data is None: | |
197 raise ValueError("You must pass options as backend_data with Twisted backend") | |
198 options = self.backend_data | |
199 if options.get('nodaemon', False) or options.get('debug', False): | |
200 addObserver(self.twisted_log.FileLogObserver(sys.stdout).emit) | |
201 else: | |
202 # \\default is not in the output, so we remove current observers | |
203 self.cleared_observers = self.log_publisher.observers | |
204 self.observers.clear() | |
205 del self.log_publisher.observers[:] | |
206 # and we forbid twistd to add any observer | |
207 self.twisted_log.addObserver = lambda other: None | |
208 | |
209 if C.LOG_OPT_OUTPUT_FILE in log.handlers: | |
210 from twisted.python import logfile | |
211 for path in log.handlers[C.LOG_OPT_OUTPUT_FILE]: | |
212 log_file = sys.stdout if path == '-' else logfile.LogFile.fromFullPath(path) | |
213 addObserver(self.twisted_log.FileLogObserver(log_file).emit) | |
214 | |
215 if C.LOG_OPT_OUTPUT_MEMORY in log.handlers: | |
216 raise NotImplementedError("Memory observer is not implemented in Twisted backend") | |
217 | |
218 def configureColors(self, colors, force_colors, levels_taints_dict): | |
219 super(ConfigureTwisted, self).configureColors(colors, force_colors, levels_taints_dict) | |
220 self.LOGGER_CLASS.colors = colors | |
221 self.LOGGER_CLASS.force_colors = force_colors | |
222 if force_colors and not colors: | |
223 raise ValueError('colors must be True if force_colors is True') | |
224 | |
225 def postTreatment(self): | |
226 """Install twistedObserver which manage non SàT logs""" | |
227 def twistedObserver(event): | |
228 """Observer which redirect log message not produced by SàT to SàT logging system""" | |
229 if not 'sat_logged' in event: | |
230 # this log was not produced by SàT | |
231 from twisted.python import log as twisted_log | |
232 text = twisted_log.textFromEventDict(event) | |
233 if text is None: | |
234 return | |
235 twisted_logger = log.getLogger(C.LOG_TWISTED_LOGGER) | |
236 log_method = twisted_logger.error if event.get('isError', False) else twisted_logger.info | |
237 log_method(text.decode('utf-8')) | |
238 | |
239 self.log_publisher._originalAddObserver(twistedObserver) | |
240 | |
241 | |
242 class ConfigureStandard(ConfigureBasic): | |
243 | |
244 def __init__(self, level=None, fmt=None, output=None, logger=None, colors=False, levels_taints_dict=None, force_colors=False, backend_data=None): | |
245 if fmt is None: | |
246 fmt = C.LOG_OPT_FORMAT[1] | |
247 if output is None: | |
248 output = C.LOG_OPT_OUTPUT[1] | |
249 super(ConfigureStandard, self).__init__(level, fmt, output, logger, colors, levels_taints_dict, force_colors, backend_data) | |
250 | |
251 def preTreatment(self): | |
252 """We use logging methods directly, instead of using Logger""" | |
253 import logging | |
254 log.getLogger = logging.getLogger | |
255 log.debug = logging.debug | |
256 log.info = logging.info | |
257 log.warning = logging.warning | |
258 log.error = logging.error | |
259 log.critical = logging.critical | |
260 | |
261 def configureLevel(self, level): | |
262 if level is None: | |
263 level = C.LOG_LVL_DEBUG | |
264 self.level = level | |
265 | |
266 def configureFormat(self, fmt): | |
267 super(ConfigureStandard, self).configureFormat(fmt) | |
268 import logging | |
269 import sys | |
270 | |
271 class SatFormatter(logging.Formatter): | |
272 u"""Formatter which manage SàT specificities""" | |
273 _format = fmt | |
274 _with_profile = '%(profile)s' in fmt | |
275 | |
276 def __init__(self, can_colors=False): | |
277 super(SatFormatter, self).__init__(self._format) | |
278 self.can_colors = can_colors | |
279 | |
280 def format(self, record): | |
281 if self._with_profile: | |
282 record.profile = ConfigureStandard.getProfile() | |
283 do_color = self.with_colors and (self.can_colors or self.force_colors) | |
284 if ConfigureStandard._color_location: | |
285 # we copy raw formatting strings for color_* | |
286 # as formatting is handled in ansiColors in this case | |
287 if do_color: | |
288 record.color_start = log.COLOR_START | |
289 record.color_end = log.COLOR_END | |
290 else: | |
291 record.color_start = record.color_end = '' | |
292 s = super(SatFormatter, self).format(record) | |
293 if do_color: | |
294 s = ConfigureStandard.ansiColors(record.levelname, s) | |
295 if sys.platform == "android": | |
296 # FIXME: dirty hack to workaround android encoding issue on log | |
297 # need to be fixed properly | |
298 return s.encode('ascii', 'ignore') | |
299 else: | |
300 return s | |
301 | |
302 self.formatterClass = SatFormatter | |
303 | |
304 def configureOutput(self, output): | |
305 self.manageOutputs(output) | |
306 | |
307 def configureLogger(self, logger): | |
308 self.name_filter = log.FilterName(logger) if logger else None | |
309 | |
310 def configureColors(self, colors, force_colors, levels_taints_dict): | |
311 super(ConfigureStandard, self).configureColors(colors, force_colors, levels_taints_dict) | |
312 self.formatterClass.with_colors = colors | |
313 self.formatterClass.force_colors = force_colors | |
314 if not colors and force_colors: | |
315 raise ValueError("force_colors can't be used if colors is False") | |
316 | |
317 def _addHandler(self, root_logger, hdlr, can_colors=False): | |
318 hdlr.setFormatter(self.formatterClass(can_colors)) | |
319 root_logger.addHandler(hdlr) | |
320 root_logger.setLevel(self.level) | |
321 if self.name_filter is not None: | |
322 hdlr.addFilter(self.name_filter) | |
323 | |
324 def postTreatment(self): | |
325 import logging | |
326 root_logger = logging.getLogger() | |
327 if len(root_logger.handlers) == 0: | |
328 for handler, options in log.handlers.items(): | |
329 if handler == C.LOG_OPT_OUTPUT_DEFAULT: | |
330 hdlr = logging.StreamHandler() | |
331 try: | |
332 can_colors = hdlr.stream.isatty() | |
333 except AttributeError: | |
334 can_colors = False | |
335 self._addHandler(root_logger, hdlr, can_colors=can_colors) | |
336 elif handler == C.LOG_OPT_OUTPUT_MEMORY: | |
337 from logging.handlers import BufferingHandler | |
338 class SatMemoryHandler(BufferingHandler): | |
339 def emit(self, record): | |
340 super(SatMemoryHandler, self).emit(self.format(record)) | |
341 hdlr = SatMemoryHandler(options) | |
342 log.handlers[handler] = hdlr # we keep a reference to the handler to read the buffer later | |
343 self._addHandler(root_logger, hdlr, can_colors=False) | |
344 elif handler == C.LOG_OPT_OUTPUT_FILE: | |
345 import os.path | |
346 for path in options: | |
347 hdlr = logging.FileHandler(os.path.expanduser(path)) | |
348 self._addHandler(root_logger, hdlr, can_colors=False) | |
349 else: | |
350 raise ValueError("Unknown handler type") | |
351 else: | |
352 root_logger.warning(u"Handlers already set on root logger") | |
353 | |
354 @staticmethod | |
355 def memoryGet(size=None): | |
356 """Return buffered logs | |
357 | |
358 @param size: number of logs to return | |
359 """ | |
360 mem_handler = log.handlers[C.LOG_OPT_OUTPUT_MEMORY] | |
361 return (log_msg for log_msg in mem_handler.buffer[size if size is None else -size:]) | |
362 | |
363 | |
364 log.configure_cls[C.LOG_BACKEND_BASIC] = ConfigureBasic | |
365 log.configure_cls[C.LOG_BACKEND_TWISTED] = ConfigureTwisted | |
366 log.configure_cls[C.LOG_BACKEND_STANDARD] = ConfigureStandard | |
367 | |
368 def configure(backend, **options): | |
369 """Configure logging behaviour | |
370 @param backend: can be: | |
371 C.LOG_BACKEND_STANDARD: use standard logging module | |
372 C.LOG_BACKEND_TWISTED: use twisted logging module (with standard logging observer) | |
373 C.LOG_BACKEND_BASIC: use a basic print based logging | |
374 C.LOG_BACKEND_CUSTOM: use a given Logger subclass | |
375 """ | |
376 return log.configure(backend, **options) | |
377 | |
378 def _parseOptions(options): | |
379 """Parse string options as given in conf or environment variable, and return expected python value | |
380 | |
381 @param options (dict): options with (key: name, value: string value) | |
382 """ | |
383 COLORS = C.LOG_OPT_COLORS[0] | |
384 LEVEL = C.LOG_OPT_LEVEL[0] | |
385 | |
386 if COLORS in options: | |
387 if options[COLORS].lower() in ('1', 'true'): | |
388 options[COLORS] = True | |
389 elif options[COLORS] == 'force': | |
390 options[COLORS] = True | |
391 options['force_colors'] = True | |
392 else: | |
393 options[COLORS] = False | |
394 if LEVEL in options: | |
395 level = options[LEVEL].upper() | |
396 if level not in C.LOG_LEVELS: | |
397 level = C.LOG_LVL_INFO | |
398 options[LEVEL] = level | |
399 | |
400 def satConfigure(backend=C.LOG_BACKEND_STANDARD, const=None, backend_data=None): | |
401 """Configure logging system for SàT, can be used by frontends | |
402 | |
403 logs conf is read in SàT conf, then in environment variables. It must be done before Memory init | |
404 @param backend: backend to use, it can be: | |
405 - C.LOG_BACKEND_BASIC: print based backend | |
406 - C.LOG_BACKEND_TWISTED: Twisted logging backend | |
407 - C.LOG_BACKEND_STANDARD: standard logging backend | |
408 @param const: Const class to use instead of sat.core.constants.Const (mainly used to change default values) | |
409 """ | |
410 if const is not None: | |
411 global C | |
412 C = const | |
413 log.C = const | |
414 from sat.tools import config | |
415 import os | |
416 log_conf = {} | |
417 sat_conf = config.parseMainConf() | |
418 for opt_name, opt_default in C.LOG_OPTIONS(): | |
419 try: | |
420 log_conf[opt_name] = os.environ[''.join((C.ENV_PREFIX, C.LOG_OPT_PREFIX.upper(), opt_name.upper()))] | |
421 except KeyError: | |
422 log_conf[opt_name] = config.getConfig(sat_conf, C.LOG_OPT_SECTION, C.LOG_OPT_PREFIX + opt_name, opt_default) | |
423 | |
424 _parseOptions(log_conf) | |
425 configure(backend, backend_data=backend_data, **log_conf) |