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)