comparison sat/core/log.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.py@0046283a285d
children 0fa217fafabf
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 # TODO: change formatting from "%s" style to "{}" when moved to Python 3
23
24 from sat.core.constants import Const as C
25 from sat.tools.common.ansi import ANSI as A
26 from sat.core import exceptions
27
28 backend = None
29 _loggers = {}
30 handlers = {}
31 COLOR_START = '%(color_start)s'
32 COLOR_END = '%(color_end)s'
33
34
35 class Filtered(Exception):
36 pass
37
38
39 class Logger(object):
40 """High level logging class"""
41 fmt = None # format option as given by user (e.g. SAT_LOG_LOGGER)
42 filter_name = None # filter to call
43 post_treat = None
44
45 def __init__(self, name):
46 if isinstance(name, Logger):
47 self.copy(name)
48 else:
49 self._name = name
50
51 def copy(self, other):
52 """Copy values from other Logger"""
53 self.fmt = other.fmt
54 self.Filter_name = other.fmt
55 self.post_treat = other.post_treat
56 self._name = other._name
57
58 def out(self, message, level=None):
59 """Actually log the message
60
61 @param message: formatted message
62 """
63 print message
64
65 def log(self, level, message):
66 """Print message
67
68 @param level: one of C.LOG_LEVELS
69 @param message: message to format and print
70 """
71 try:
72 formatted = self.format(level, message)
73 if self.post_treat is None:
74 self.out(formatted, level)
75 else:
76 self.out(self.post_treat(level, formatted), level)
77 except Filtered:
78 pass
79
80 def format(self, level, message):
81 """Format message according to Logger.fmt
82
83 @param level: one of C.LOG_LEVELS
84 @param message: message to format
85 @return: formatted message
86
87 @raise: Filtered when the message must not be logged
88 """
89 if self.fmt is None and self.filter_name is None:
90 return message
91 record = {'name': self._name,
92 'message': message,
93 'levelname': level,
94 }
95 try:
96 if not self.filter_name.dictFilter(record):
97 raise Filtered
98 except (AttributeError, TypeError): # XXX: TypeError is here because of a pyjamas bug which need to be fixed (TypeError is raised instead of AttributeError)
99 if self.filter_name is not None:
100 raise ValueError("Bad filter: filters must have a .filter method")
101 try:
102 return self.fmt % record
103 except TypeError:
104 return message
105 except KeyError as e:
106 if e.args[0] == 'profile':
107 # XXX: %(profile)s use some magic with introspection, for debugging purpose only *DO NOT* use in production
108 record['profile'] = configure_cls[backend].getProfile()
109 return self.fmt % record
110 else:
111 raise e
112
113 def debug(self, msg):
114 self.log(C.LOG_LVL_DEBUG, msg)
115
116 def info(self, msg):
117 self.log(C.LOG_LVL_INFO, msg)
118
119 def warning(self, msg):
120 self.log(C.LOG_LVL_WARNING, msg)
121
122 def error(self, msg):
123 self.log(C.LOG_LVL_ERROR, msg)
124
125 def critical(self, msg):
126 self.log(C.LOG_LVL_CRITICAL, msg)
127
128
129 class FilterName(object):
130 """Filter on logger name according to a regex"""
131
132 def __init__(self, name_re):
133 """Initialise name filter
134
135 @param name_re: regular expression used to filter names (using search and not match)
136 """
137 assert name_re
138 import re
139 self.name_re = re.compile(name_re)
140
141 def filter(self, record):
142 if self.name_re.search(record.name) is not None:
143 return 1
144 return 0
145
146 def dictFilter(self, dict_record):
147 """Filter using a dictionary record
148
149 @param dict_record: dictionary with at list a key "name" with logger name
150 @return: True if message should be logged
151 """
152 class LogRecord(object):
153 pass
154 log_record = LogRecord()
155 log_record.name = dict_record['name']
156 return self.filter(log_record) == 1
157
158
159 class ConfigureBase(object):
160 LOGGER_CLASS = Logger
161 _color_location = False # True if color location is specified in fmt (with COLOR_START)
162
163 def __init__(self, level=None, fmt=None, output=None, logger=None, colors=False, levels_taints_dict=None, force_colors=False, backend_data=None):
164 """Configure a backend
165
166 @param level: one of C.LOG_LEVELS
167 @param fmt: format string, pretty much as in std logging. Accept the following keywords (maybe more depending on backend):
168 - "message"
169 - "levelname"
170 - "name" (logger name)
171 @param logger: if set, use it as a regular expression to filter on logger name.
172 Use search to match expression, so ^ or $ can be necessary.
173 @param colors: if True use ANSI colors to show log levels
174 @param force_colors: if True ANSI colors are used even if stdout is not a tty
175 """
176 self.backend_data = backend_data
177 self.preTreatment()
178 self.configureLevel(level)
179 self.configureFormat(fmt)
180 self.configureOutput(output)
181 self.configureLogger(logger)
182 self.configureColors(colors, force_colors, levels_taints_dict)
183 self.postTreatment()
184 self.updateCurrentLogger()
185
186 def updateCurrentLogger(self):
187 """update existing logger to the class needed for this backend"""
188 if self.LOGGER_CLASS is None:
189 return
190 for name, logger in _loggers.items():
191 _loggers[name] = self.LOGGER_CLASS(logger)
192
193 def preTreatment(self):
194 pass
195
196 def configureLevel(self, level):
197 if level is not None:
198 # we deactivate methods below level
199 level_idx = C.LOG_LEVELS.index(level)
200 def dev_null(self, msg):
201 pass
202 for _level in C.LOG_LEVELS[:level_idx]:
203 setattr(Logger, _level.lower(), dev_null)
204
205 def configureFormat(self, fmt):
206 if fmt is not None:
207 if fmt != '%(message)s': # %(message)s is the same as None
208 Logger.fmt = fmt
209 if COLOR_START in fmt:
210 ConfigureBase._color_location = True
211 if fmt.find(COLOR_END,fmt.rfind(COLOR_START))<0:
212 # color_start not followed by an end, we add it
213 Logger.fmt += COLOR_END
214
215 def configureOutput(self, output):
216 if output is not None:
217 if output != C.LOG_OPT_OUTPUT_SEP + C.LOG_OPT_OUTPUT_DEFAULT:
218 # TODO: manage other outputs
219 raise NotImplementedError("Basic backend only manage default output yet")
220
221 def configureLogger(self, logger):
222 if logger:
223 Logger.filter_name = FilterName(logger)
224
225 def configureColors(self, colors, force_colors, levels_taints_dict):
226 if colors:
227 # if color are used, we need to handle levels_taints_dict
228 for level in levels_taints_dict.keys():
229 # we wants levels in uppercase to correspond to contstants
230 levels_taints_dict[level.upper()] = levels_taints_dict[level]
231 taints = self.__class__.taints = {}
232 for level in C.LOG_LEVELS:
233 # we want use values and use constant value as default
234 taint_list = levels_taints_dict.get(level, C.LOG_OPT_TAINTS_DICT[1][level])
235 ansi_list = []
236 for elt in taint_list:
237 elt = elt.upper()
238 try:
239 ansi = getattr(A, 'FG_{}'.format(elt))
240 except AttributeError:
241 try:
242 ansi = getattr(A, elt)
243 except AttributeError:
244 # we use raw string if element is unknown
245 ansi = elt
246 ansi_list.append(ansi)
247 taints[level] = ''.join(ansi_list)
248
249 def postTreatment(self):
250 pass
251
252 def manageOutputs(self, outputs_raw):
253 """ Parse output option in a backend agnostic way, and fill handlers consequently
254
255 @param outputs_raw: output option as enterred in environment variable or in configuration
256 """
257 if not outputs_raw:
258 return
259 outputs = outputs_raw.split(C.LOG_OPT_OUTPUT_SEP)
260 global handlers
261 if len(outputs) == 1:
262 handlers[C.LOG_OPT_OUTPUT_FILE] = [outputs.pop()]
263
264 for output in outputs:
265 if not output:
266 continue
267 if output[-1] == ')':
268 # we have options
269 opt_begin = output.rfind('(')
270 options = output[opt_begin+1:-1]
271 output = output[:opt_begin]
272 else:
273 options = None
274
275 if output not in (C.LOG_OPT_OUTPUT_DEFAULT, C.LOG_OPT_OUTPUT_FILE, C.LOG_OPT_OUTPUT_MEMORY):
276 raise ValueError(u"Invalid output [%s]" % output)
277
278 if output == C.LOG_OPT_OUTPUT_DEFAULT:
279 # no option for defaut handler
280 handlers[output] = None
281 elif output == C.LOG_OPT_OUTPUT_FILE:
282 if not options:
283 ValueError("{handler} output need a path as option" .format(handle=output))
284 handlers.setdefault(output, []).append(options)
285 options = None # option are parsed, we can empty them
286 elif output == C.LOG_OPT_OUTPUT_MEMORY:
287 # we have memory handler, option can be the len limit or None
288 try:
289 limit = int(options)
290 options = None # option are parsed, we can empty them
291 except (TypeError, ValueError):
292 limit = C.LOG_OPT_OUTPUT_MEMORY_LIMIT
293 handlers[output] = limit
294
295 if options: # we should not have unparsed options
296 raise ValueError(u"options [{options}] are not supported for {handler} output".format(options=options, handler=output))
297
298 @staticmethod
299 def memoryGet(size=None):
300 """Return buffered logs
301
302 @param size: number of logs to return
303 """
304 raise NotImplementedError
305
306 @classmethod
307 def ansiColors(cls, level, message):
308 """Colorise message depending on level for terminals
309
310 @param level: one of C.LOG_LEVELS
311 @param message: formatted message to log
312 @return: message with ANSI escape codes for coloration
313 """
314
315 try:
316 start = cls.taints[level]
317 except KeyError:
318 start = ''
319
320 if cls._color_location:
321 return message % {'color_start': start,
322 'color_end': A.RESET}
323 else:
324 return '%s%s%s' % (start, message, A.RESET)
325
326 @staticmethod
327 def getProfile():
328 """Try to find profile value using introspection"""
329 raise NotImplementedError
330
331
332 class ConfigureCustom(ConfigureBase):
333 LOGGER_CLASS = None
334
335 def __init__(self, logger_class, *args, **kwargs):
336 ConfigureCustom.LOGGER_CLASS = logger_class
337
338
339 configure_cls = { None: ConfigureBase,
340 C.LOG_BACKEND_CUSTOM: ConfigureCustom
341 } # XXX: (key: backend, value: Configure subclass) must be filled when new backend are added
342
343
344 def configure(backend_, **options):
345 """Configure logging behaviour
346 @param backend: can be:
347 C.LOG_BACKEND_BASIC: use a basic print based logging
348 C.LOG_BACKEND_CUSTOM: use a given Logger subclass
349 """
350 global backend
351 if backend is not None:
352 raise exceptions.InternalError("Logging can only be configured once")
353 backend = backend_
354
355 try:
356 configure_class = configure_cls[backend]
357 except KeyError:
358 raise ValueError("unknown backend [{}]".format(backend))
359 if backend == C.LOG_BACKEND_CUSTOM:
360 logger_class = options.pop('logger_class')
361 configure_class(logger_class, **options)
362 else:
363 configure_class(**options)
364
365 def memoryGet(size=None):
366 if not C.LOG_OPT_OUTPUT_MEMORY in handlers:
367 raise ValueError('memory output is not used')
368 return configure_cls[backend].memoryGet(size)
369
370 def getLogger(name=C.LOG_BASE_LOGGER):
371 try:
372 logger_class = configure_cls[backend].LOGGER_CLASS
373 except KeyError:
374 raise ValueError("This method should not be called with backend [{}]".format(backend))
375 return _loggers.setdefault(name, logger_class(name))
376
377 _root_logger = getLogger()
378
379 def debug(msg):
380 _root_logger.debug(msg)
381
382 def info(msg):
383 _root_logger.info(msg)
384
385 def warning(msg):
386 _root_logger.warning(msg)
387
388 def error(msg):
389 _root_logger.error(msg)
390
391 def critical(msg):
392 _root_logger.critical(msg)