forked from github/pelican
198 lines
5.7 KiB
Python
198 lines
5.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
from __future__ import unicode_literals, print_function
|
|
|
|
__all__ = [
|
|
'init'
|
|
]
|
|
|
|
import os
|
|
import sys
|
|
import logging
|
|
import locale
|
|
|
|
from collections import defaultdict, Mapping
|
|
|
|
import six
|
|
|
|
class BaseFormatter(logging.Formatter):
|
|
def __init__(self, fmt=None, datefmt=None):
|
|
FORMAT = '%(customlevelname)s %(message)s'
|
|
super(BaseFormatter, self).__init__(fmt=FORMAT, datefmt=datefmt)
|
|
|
|
def format(self, record):
|
|
record.__dict__['customlevelname'] = self._get_levelname(record.levelname)
|
|
# format multiline messages 'nicely' to make it clear they are together
|
|
record.msg = record.msg.replace('\n', '\n | ')
|
|
return super(BaseFormatter, self).format(record)
|
|
|
|
def formatException(self, ei):
|
|
''' prefix traceback info for better representation '''
|
|
# .formatException returns a bytestring in py2 and unicode in py3
|
|
# since .format will handle unicode conversion,
|
|
# str() calls are used to normalize formatting string
|
|
s = super(BaseFormatter, self).formatException(ei)
|
|
# fancy format traceback
|
|
s = str('\n').join(str(' | ') + line for line in s.splitlines())
|
|
# separate the traceback from the preceding lines
|
|
s = str(' |___\n{}').format(s)
|
|
return s
|
|
|
|
def _get_levelname(self, name):
|
|
''' NOOP: overridden by subclasses '''
|
|
return name
|
|
|
|
|
|
class ANSIFormatter(BaseFormatter):
|
|
ANSI_CODES = {
|
|
'red': '\033[1;31m',
|
|
'yellow': '\033[1;33m',
|
|
'cyan': '\033[1;36m',
|
|
'white': '\033[1;37m',
|
|
'bgred': '\033[1;41m',
|
|
'bggrey': '\033[1;100m',
|
|
'reset': '\033[0;m'}
|
|
|
|
LEVEL_COLORS = {
|
|
'INFO': 'cyan',
|
|
'WARNING': 'yellow',
|
|
'ERROR': 'red',
|
|
'CRITICAL': 'bgred',
|
|
'DEBUG': 'bggrey'}
|
|
|
|
def _get_levelname(self, name):
|
|
color = self.ANSI_CODES[self.LEVEL_COLORS.get(name, 'white')]
|
|
if name == 'INFO':
|
|
fmt = '{0}->{2}'
|
|
else:
|
|
fmt = '{0}{1}{2}:'
|
|
return fmt.format(color, name, self.ANSI_CODES['reset'])
|
|
|
|
|
|
class TextFormatter(BaseFormatter):
|
|
"""
|
|
Convert a `logging.LogRecord' object into text.
|
|
"""
|
|
|
|
def _get_levelname(self, name):
|
|
if name == 'INFO':
|
|
return '->'
|
|
else:
|
|
return name + ':'
|
|
|
|
|
|
class LimitFilter(logging.Filter):
|
|
"""
|
|
Remove duplicates records, and limit the number of records in the same
|
|
group.
|
|
|
|
Groups are specified by the message to use when the number of records in
|
|
the same group hit the limit.
|
|
E.g.: log.warning(('43 is not the answer', 'More erroneous answers'))
|
|
"""
|
|
|
|
_ignore = set()
|
|
_threshold = 5
|
|
_group_count = defaultdict(int)
|
|
|
|
def filter(self, record):
|
|
# don't limit log messages for anything above "warning"
|
|
if record.levelno > logging.WARN:
|
|
return True
|
|
|
|
# extract group
|
|
group = record.__dict__.get('limit_msg', None)
|
|
group_args = record.__dict__.get('limit_args', ())
|
|
|
|
# ignore record if it was already raised
|
|
# use .getMessage() and not .msg for string formatting
|
|
ignore_key = (record.levelno, record.getMessage())
|
|
if ignore_key in self._ignore:
|
|
return False
|
|
else:
|
|
self._ignore.add(ignore_key)
|
|
|
|
# check if we went over threshold
|
|
if group:
|
|
key = (record.levelno, group)
|
|
self._group_count[key] += 1
|
|
if self._group_count[key] == self._threshold:
|
|
record.msg = group
|
|
record.args = group_args
|
|
elif self._group_count[key] > self._threshold:
|
|
return False
|
|
return True
|
|
|
|
|
|
class SafeLogger(logging.Logger):
|
|
"""
|
|
Base Logger which properly encodes Exceptions in Py2
|
|
"""
|
|
_exc_encoding = locale.getpreferredencoding()
|
|
|
|
def _log(self, level, msg, args, exc_info=None, extra=None):
|
|
# if the only argument is a Mapping, Logger uses that for formatting
|
|
# format values for that case
|
|
if args and len(args)==1 and isinstance(args[0], Mapping):
|
|
args = ({k: self._decode_arg(v) for k, v in args[0].items()},)
|
|
# otherwise, format each arg
|
|
else:
|
|
args = tuple(self._decode_arg(arg) for arg in args)
|
|
super(SafeLogger, self)._log(level, msg, args,
|
|
exc_info=exc_info, extra=extra)
|
|
|
|
def _decode_arg(self, arg):
|
|
'''
|
|
properly decode an arg for Py2 if it's Exception
|
|
|
|
|
|
localized systems have errors in native language if locale is set
|
|
so convert the message to unicode with the correct encoding
|
|
'''
|
|
if isinstance(arg, Exception):
|
|
text = str(arg)
|
|
if six.PY2:
|
|
text = text.decode(self._exc_encoding)
|
|
return text
|
|
else:
|
|
return arg
|
|
|
|
|
|
class LimitLogger(SafeLogger):
|
|
"""
|
|
A logger which adds LimitFilter automatically
|
|
"""
|
|
|
|
limit_filter = LimitFilter()
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(LimitLogger, self).__init__(*args, **kwargs)
|
|
self.addFilter(LimitLogger.limit_filter)
|
|
|
|
logging.setLoggerClass(LimitLogger)
|
|
|
|
|
|
def init(level=None, handler=logging.StreamHandler()):
|
|
|
|
logger = logging.getLogger()
|
|
|
|
if (os.isatty(sys.stdout.fileno())
|
|
and not sys.platform.startswith('win')):
|
|
fmt = ANSIFormatter()
|
|
else:
|
|
fmt = TextFormatter()
|
|
handler.setFormatter(fmt)
|
|
logger.addHandler(handler)
|
|
|
|
if level:
|
|
logger.setLevel(level)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
init(level=logging.DEBUG)
|
|
|
|
root_logger = logging.getLogger()
|
|
root_logger.debug('debug')
|
|
root_logger.info('info')
|
|
root_logger.warning('warning')
|
|
root_logger.error('error')
|
|
root_logger.critical('critical')
|