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