Mercurial > libervia-backend
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) |