1 # The software in this package is distributed under the GNU General
2 # Public License version 2 (with a special exception described below).
4 # A copy of GNU General Public License (GPL) is included in this distribution,
5 # in the file COPYING.GPL.
7 # As a special exception, if other files instantiate templates or use macros
8 # or inline functions from this file, or you compile this file and link it
9 # with other works to produce a work based on this file, this file
10 # does not by itself cause the resulting work to be covered
11 # by the GNU General Public License.
13 # However the source code for this file must still be made available
14 # in accordance with section (3) of the GNU General Public License.
16 # This exception does not invalidate any other reasons why a work based
17 # on this file might be covered by the GNU General Public License.
19 # Copyright (c) 2016-2018 Intra2net AG <info@intra2net.com>
21 """ Helpers for logging; featuring:
23 ShortLevelFormatter: provide a 4-char-sized field "shortlevel" for message
24 urgency (dbug/info/warn/err /crit)
26 I2nLogger: logger that provides a notice(), allows omission for str.format
27 and is quite convenient in general
29 get_logger: factor for creating I2nLoggers
32 * allow milliseconds in dateformat field (see test_short_level_format)
33 * create own basicConfig-like function that uses our classes as default
34 --> started using I2nLogger constructor and get_logger
35 * try to find out module that calls I2nLogger constructor to provide a good
36 default value for name (finding out the current module is a pain in the a..)
38 .. todo:: create unittests from test_* functions at bottom
39 .. todo:: think about how to allow different levels per handler
40 .. todo:: do not limit logs by line numbers but by disc size? Warn when at 50%,
41 75%, 90%, 99% of limit?
45 from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL, NOTSET
46 from math import log10, floor
49 from .type_helpers import isstr
51 #: log level half-way between INFO and WARNING
52 NOTICE = (INFO + WARNING)//2
53 logging.addLevelName(NOTICE, 'NOTICE')
55 #: default formatting string for ShortLevelFormatter
56 DEFAULT_SHORT_LEVEL_FORMAT = '%(asctime)s:%(msecs)03d %(shortlevel)s| %(msg)s'
58 #: default formatting string for date/time in ShortLevelFormatter
59 DEFAULT_SHORT_LEVEL_DATE_FORMAT = '%H:%M:%S'
61 #: mapping from level name to level int for I2nLogger's set_level
62 LEVEL_DICT = dict(notset=NOTSET, debug=DEBUG, info=INFO, notice=NOTICE,
63 warning=WARNING, error=ERROR, critical=CRITICAL)
65 #: min level allowed in I2nLogger
68 #: max level allowed in I2nLogger
71 #: constant for I2nLogger streams argument: stream to stdout
75 class ShortLevelFormatter(logging.Formatter):
77 Formatter for logging handlers that allows use of format field "shortlevel"
79 using this formatter, you can specify in the log message format string the
80 field "shortlevel" which will introduce in your log messages a 4-char field
81 for the log record urgency: "DEBUG" --> "dbug", "INFO" --> "info",
82 "WARNING" --> "warn", "ERROR" --> "err ", "CRITICAL" --> "crit"
84 All other functionality (like other format fields) is inherited from base
87 Most easily used indirectly through :py:class:`I2nLogger`
88 (uses this by default)
90 Explicit usage example::
92 logger = logging.getLogger(__name__)
93 logger.setLevel(logging.DEBUG)
94 handler = logging.StreamHandler()
95 handler.setLevel(logging.DEBUG)
96 formatter = ShortLevelFormatter('%(shortlevel)s| %(msg)s')
97 handler.setFormatter(formatter)
98 logger.addHandler(handler)
100 You can even add new levels::
102 notice = (logging.INFO + logging.WARNING)/2
103 formatter.add_level(notice, 'note')
104 logger.log(notice, 'more important than info but no warning nor error')
106 .. seealso:: testing funcion :py:func:`test_short_level_format`
109 def __init__(self, fmt=DEFAULT_SHORT_LEVEL_FORMAT,
110 datefmt=DEFAULT_SHORT_LEVEL_DATE_FORMAT, *args, **kwargs):
111 """ creates a new ShortLevelFormatter
113 forwards all args to super class Formatter and initializes dict with
114 levelno and short string representations
116 self.parent = super(ShortLevelFormatter, self)
117 self.parent.__init__(fmt=fmt, datefmt=datefmt, *args, **kwargs)
118 self._shortlevel_dict = {DEBUG: 'dbug', INFO: 'info', WARNING: 'warn',
119 ERROR: 'err ', CRITICAL: 'crit',
122 def format(self, record):
123 """ create string representation of given log record """
125 record.shortlevel = self._shortlevel_dict[record.levelno]
127 record.shortlevel = '????'
129 return self.parent.format(record)
131 def add_level(self, levelno, shortlevel_str):
132 """ add a new message urgency level
134 :param int levelno: numeric urgency level
135 :param str shortlevel_str: string representation of message urgency;
136 should be of length 4
140 self._shortlevel_dict[levelno] = shortlevel_str
143 #: memory for all I2nLogger instances in current session to avoid overwrite
144 #: used in :py:func:`get_logger` and :py:class:`I2nLogger` constructor
148 def get_logger(name, *args, **kwargs):
149 """ factory for :py:class:`I2nLogger`: create new or return existing object
151 all args as in :py:class:`I2nLogger` constructor; args will be ignored if
152 logger with given name exists already!
155 if name in _i2n_loggers:
156 return _i2n_loggers[name] # ignore all args
158 return I2nLogger(name, *args, **kwargs)
162 """ a more convenient logger
165 * can be used without ".format()" as follows::
167 logger.info('the result is {0}', result) # more convenient than...
168 logger.info('the result is {0}'.format(result)) # ... the original
170 * Has a :py:meth:`Logger.notice` function
171 * Has by default as only handler a :py:class:`logging.StreamHandler` to
172 stdout with a :py:class:`ShortLevelFormatter`
173 * Simplifies setting of logger's logging level using
174 :py:meth:`Logger.set_level`. The level is kept in sync between logger and
175 handlers and can be queried using :py:meth:`get_level`
176 (does not work accross logger hierarchies!)
177 * can specify name and level and [date]fmt all in constructor
178 * can limit the number of lines this logger will produce to prevent filling
179 hard drive with log file
180 (assumes one line per call to log/debug/info/notice/...,
181 only counts calls with priority above this logger's level)
182 * provides shorter method names: dbg, note, warn, err
184 ..note:: Creating multiple instances with the same name is not allowed and
185 will result in an error. Use :py:func:`get_logger` instead of this
186 constructor to avoid such situtations
188 ..note:: Do not change or use the underlying logger or functionality here
189 (in particular, get_level) is no longer reliable!
192 def __init__(self, name, level=INFO, fmt=DEFAULT_SHORT_LEVEL_FORMAT,
193 datefmt=DEFAULT_SHORT_LEVEL_DATE_FORMAT,
194 streams=STDOUT, files=None):
195 """ creates a I2nLogger; forwards args to logging.getLogger
198 :param str name: name of this logger, best practice is module name
199 :param int level: best use one of the constants DEBUG, INFO NOTICE,
201 :param str fmt: format of log messages, see
202 :py:class:`ShortLevelFormatter` for more help
203 :param str datefmt: format of date added to log messages, see
204 :py:class:`ShortLevelFormatter` for more help
205 :param streams: list/tuple of or a single stream to log to, default is
207 :param files: list/tuple or single file name to log to
208 :raise: ValueError if an I2nLogger with the same name exists already
211 # check if an instance with the same name has been created earlier
212 # to prevent conflicts
214 if name in _i2n_loggers:
215 raise ValueError("An I2nLogger with that exact name ('{0}') exists"
216 " already -- use get_logger instead!"
219 self._log = logging.getLogger(name)
220 if isinstance(level, str):
221 level = LEVEL_DICT[level]
222 self._level = min(MAX_LEVEL, max(MIN_LEVEL, level))
223 self._log.setLevel(self._level)
225 # remove handlers (sometimes there are mutliple by default)
226 for handler in self._log.handlers:
227 self._log.removeHandler(handler)
229 # create new handlers and formatter
232 elif not isinstance(streams, (list, tuple)):
233 streams = (streams, )
234 for stream in streams:
235 formatter = ShortLevelFormatter(fmt=fmt, datefmt=datefmt)
236 formatter.add_level(NOTICE, 'note')
237 new_handler = logging.StreamHandler(stream)
238 new_handler.setFormatter(formatter)
239 new_handler.setLevel(self._level)
240 self._log.addHandler(new_handler)
244 elif not isinstance(files, (list, tuple)):
246 for file_name in files:
247 formatter = ShortLevelFormatter(fmt=fmt, datefmt=datefmt)
248 formatter.add_level(NOTICE, 'note')
249 new_handler = logging.FileHandler(file_name)
250 new_handler.setFormatter(formatter)
251 new_handler.setLevel(self._level)
252 self._log.addHandler(new_handler)
254 # remember that this logger is a I2nLogger
255 _i2n_loggers[name] = self
257 def dbg(self, message, *args, **kwargs):
258 self._log_no_test(DEBUG, message, *args, **kwargs)
260 def debug(self, message, *args, **kwargs):
261 self._log_no_test(DEBUG, message, *args, **kwargs)
263 def info(self, message, *args, **kwargs):
264 self._log_no_test(INFO, message, *args, **kwargs)
266 def note(self, message, *args, **kwargs):
267 self._log_no_test(NOTICE, message, *args, **kwargs)
269 def notice(self, message, *args, **kwargs):
270 self._log_no_test(NOTICE, message, *args, **kwargs)
272 def warn(self, message, *args, **kwargs):
273 self._log_no_test(WARNING, message, *args, **kwargs)
275 def warning(self, message, *args, **kwargs):
276 self._log_no_test(WARNING, message, *args, **kwargs)
278 def err(self, message, *args, **kwargs):
279 self._log_no_test(ERROR, message, *args, **kwargs)
281 def error(self, message, *args, **kwargs):
282 self._log_no_test(ERROR, message, *args, **kwargs)
284 def exception(self, message, *args, **kwargs):
285 if 'exc_info' in kwargs:
286 self._log_no_test(ERROR, message, *args, **kwargs)
288 self._log_no_test(ERROR, message, *args, exc_info=True, **kwargs)
290 def critical(self, message, *args, **kwargs):
291 self._log_no_test(CRITICAL, message, *args, **kwargs)
293 def log(self, level, message, *args, **kwargs):
294 if isinstance(level, str):
295 level = LEVEL_DICT[level.lower()]
296 if level >= self._level:
298 message_formatted = message.format(*args)
299 except (KeyError, IndexError):
300 message_formatted = '[LOG ARGS!] ' + message
301 self._log.log(level, message_formatted, **kwargs)
303 def _log_no_test(self, level, message, *args, **kwargs):
304 """ same as log() but without the isinstance test for internal use"""
305 if level >= self._level:
307 message_formatted = message.format(*args)
308 except (KeyError, IndexError):
309 message_formatted = '[LOG ARGS!] ' + message
310 self._log.log(level, message_formatted, **kwargs)
312 def log_count_if_interesting(self, count, level=INFO, counter_name=None):
313 """ Log value of a counter in gradually coarser intervals
315 see :py:func:`is_interesting_count` for definition of "interesting"
317 if is_interesting_count(count):
319 self.log(level, '{0} counter is at {1}', counter_name, count)
321 self.log(level, 'Counter is at {0}', count)
324 """ return int level of this logger """
327 def get_level_str(self):
328 """ returns :py:func:`logging.getLevelName` on :py:meth:`get_level` """
329 return logging.getLevelName(self._level)
331 def set_level(self, new_level):
332 """ set level given an int or a str
334 :arg new_level: int or str (str is converted to lower case)
335 :raises: KeyError if new_level is a string that is not in
336 :py:data:`LEVEL_DICT`
339 self._level = LEVEL_DICT[new_level.lower()]
341 self._level = min(MAX_LEVEL, max(MIN_LEVEL, new_level))
342 self._log.setLevel(self._level)
343 for handler in self._log.handlers:
344 handler.setLevel(self._level)
347 def n_digits(number):
348 """ returns the number of digits a number has in decimal format
350 :returns: 1 for 1...9, 2 for 10...99, 3 for 100...999, ...
351 0 for 0 (and everything else beween -1 and 1)
352 1 for -1...-9, 2 for -10...-99, ...
357 return floor(log10(abs(number)))+1
359 def is_interesting_count(counter):
360 """ return True if counter has reached an "interesting" value
362 For the counter to be "interesting" becomes ever harder. At first it is
363 easy (returns True for 1,2,3,6,10), then becomes harder (True only for
364 10,20,30,60,100) and harder (True for 100,200,300,600,1000) and this scheme
365 continues on a logartihmic scale.
367 An example that will print lots in the beginning and then less and less::
370 while not end_reached():
372 if is_interesting_count(counter):
373 log('reached iteration {0}'.format(counter))
376 Or implicitly using I2nLogger::log_count_if_interesting(counter)
378 :returns: True for a few values of counter, False most of the time
381 return float(counter) / 10.**(n_digits(counter)-1.) in (1., 2., 3., 6.)
383 def test_short_level_format():
384 """ quick test of :py:class:`ShortLevelFormatter` """
386 logger = logging.getLogger('logtest')
387 logger.setLevel(DEBUG)
388 handler = logging.StreamHandler()
389 handler.setLevel(DEBUG)
390 formatter = ShortLevelFormatter(
391 '%(asctime)s:%(msecs)03d %(shortlevel)s| %(msg)s'
392 ' [regular levelname=%(levelname)s]',
394 handler.setFormatter(formatter)
395 logger.addHandler(handler)
398 logger.debug('debug message')
399 logger.info('info message')
400 logger.warn('warn message')
401 logger.error('error message')
402 logger.critical('critical message')
403 logger.log(15, 'unknown level')
404 logger.log(NOTSET, 'level not set')
407 notice = (logging.INFO + logging.WARNING)/2
408 formatter.add_level(notice, 'note')
409 logger.log(notice, 'more important than info but no warning nor error')
411 # try if exception formatting still works:
413 logger.info('this is what an exception looks like:')
414 impossible_result = 1/0
415 logger.critical('just divided 1/0! The result is {0}'
416 .format(impossible_result))
417 except ZeroDivisionError:
418 logger.exception('1/0 still does not work!', exc_info=True)
421 logger.info('done testing')
423 def test_get_logger():
424 log = get_logger('logger_test')
425 log2 = get_logger('logger_test')
429 def test_line_counter():
430 log = get_logger('logger_test', max_lines=10)
431 for idx in range(20):
433 log.debug('should not show nor count')
434 print('calling log for idx {0}'.format(idx))
435 log.info('logging with idx {0}', idx)
436 log.log_count_if_interesting(idx)
439 def test_error_in_formatting():
440 log = get_logger('logger_test')
441 log.warn('forgot to add argument {} and {1} and {cnf} to format string')
442 log.warn('wrong number {} of arguments {}', 1)
444 if __name__ == '__main__':
445 #test_short_level_format()
448 test_error_in_formatting()