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)