You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
243 lines
8.0 KiB
243 lines
8.0 KiB
from collections import namedtuple
|
|
import inspect
|
|
import logging
|
|
import logging.handlers
|
|
import __main__
|
|
import os
|
|
import socket
|
|
import sys
|
|
import textwrap
|
|
from types import MethodType, ModuleType
|
|
|
|
from stackclimber import stackclimber
|
|
|
|
|
|
try:
|
|
basestring
|
|
except NameError:
|
|
basestring = str
|
|
|
|
|
|
def logger(ref=0):
|
|
"""Finds a module logger.
|
|
|
|
If the argument passed is a module, find the logger for that module using
|
|
the modules' name; if it's a string, finds a logger of that name; if an
|
|
integer, walks the stack to the module at that height.
|
|
|
|
The returned is always extended with a ``.configure()`` method allowing
|
|
its log levels for syslog and stderr to be adjusted or automatically
|
|
initialized as per the documentation for `configure()` below.
|
|
"""
|
|
if inspect.ismodule(ref):
|
|
return extend(logging.getLogger(ref.__name__))
|
|
if isinstance(ref, basestring):
|
|
return extend(logging.getLogger(ref))
|
|
return extend(logging.getLogger(stackclimber(ref+1)))
|
|
|
|
|
|
def configure(logger=None, **kwargs):
|
|
"""Configures a logger, according to the following rules:
|
|
|
|
* With no options set, enables ``INFO`` level on stderr if stderr is a TTY;
|
|
otherwise enables ``INFO`` level to Syslog.
|
|
|
|
* With ``syslog=`` and/or ``stderr=``, configures as specified.
|
|
|
|
* With ``level=``, enables either Syslog or stderr logging according to
|
|
whether or not stderr is a TTY, but uses the level specified.
|
|
"""
|
|
configuration = Configuration.auto(**kwargs)
|
|
if logger is None:
|
|
logger = logging.getLogger()
|
|
configuration(logger)
|
|
|
|
|
|
class Configuration(namedtuple('Configuration',
|
|
'syslog stderr extended server')):
|
|
def __call__(self, logger):
|
|
log.info('Applying config %s to logger: %s', self, logger.name)
|
|
if isinstance(logger, basestring):
|
|
logger = logging.getLogger(logger)
|
|
syslog, stderr = norm_level(self.syslog), norm_level(self.stderr)
|
|
configure_handlers(logger, syslog=syslog, stderr=stderr,
|
|
extended=self.extended, server=self.server)
|
|
set_normed_level(logger, min(_ for _ in [syslog, stderr] if _))
|
|
|
|
@classmethod
|
|
def auto(cls, syslog=None, stderr=None, level=None, extended=None,
|
|
server=None):
|
|
"""Tries to guess a sound logging configuration.
|
|
"""
|
|
level = norm_level(level) or logging.INFO
|
|
if syslog is None and stderr is None:
|
|
if sys.stderr.isatty() or syslog_path() is None:
|
|
log.info('Defaulting to STDERR logging.')
|
|
syslog, stderr = None, level
|
|
if extended is None:
|
|
extended = (stderr or 0) <= logging.DEBUG
|
|
else:
|
|
log.info('Defaulting to logging with Syslog.')
|
|
syslog, stderr = level, None
|
|
return cls(syslog=syslog, stderr=stderr, extended=extended,
|
|
server=server)
|
|
|
|
|
|
def configure_handlers(logger, syslog=None, stderr=None, extended=False,
|
|
server=None):
|
|
stderr_handler, syslog_handler = None, None
|
|
if stderr is not None:
|
|
stderr_handler = logging.StreamHandler()
|
|
configure_stderr_format(stderr_handler, extended)
|
|
if stderr != logging.NOTSET:
|
|
stderr_handler.level = stderr
|
|
if syslog is not None:
|
|
cmd = os.path.basename(sys.argv[0])
|
|
app = __main__.__name__
|
|
app = cmd if app in ['__main__'] else app
|
|
fmt = app + '[%(process)d]: %(name)s %(funcName)s %(message)s'
|
|
if server is not None:
|
|
address = server
|
|
fmt = ' '.join([socket.getfqdn(), fmt])
|
|
else:
|
|
address = syslog_path()
|
|
syslog_handler = logging.handlers.SysLogHandler(address=address)
|
|
syslog_handler.setFormatter(logging.Formatter(fmt=fmt))
|
|
if syslog != logging.NOTSET:
|
|
syslog_handler.level = syslog
|
|
clear_handlers(logger)
|
|
logger.handlers = [h for h in [stderr_handler, syslog_handler] if h]
|
|
|
|
|
|
syslog_paths = ['/dev/log', '/var/run/syslog']
|
|
|
|
|
|
def syslog_path():
|
|
for path in syslog_paths:
|
|
if os.path.exists(path):
|
|
return path
|
|
|
|
|
|
def set_normed_level(logger, level):
|
|
level = norm_level(level)
|
|
if level is not None:
|
|
logger.level = level
|
|
|
|
|
|
def configure_stderr_format(stderr_handler, extended=False):
|
|
if extended:
|
|
fmt = Formatter(datefmt='%H:%M:%S')
|
|
else:
|
|
fmt = logging.Formatter(fmt='%(asctime)s.%(msecs)03d %(message)s',
|
|
datefmt='%H:%M:%S')
|
|
stderr_handler.setFormatter(fmt)
|
|
|
|
|
|
def extend(logger):
|
|
logger.configure = MethodType(configure, logger)
|
|
logger.handlers += [_null_handler]
|
|
return logger
|
|
|
|
|
|
try:
|
|
_levels = dict((k.lower(), v) for k, v in logging._levelNames.items()
|
|
if isinstance(k, basestring))
|
|
except AttributeError:
|
|
_levels = dict((k.lower(), v) for k, v in logging._nameToLevel.items()
|
|
if isinstance(k, basestring))
|
|
|
|
|
|
def norm_level(level):
|
|
if level is None:
|
|
return level
|
|
if isinstance(level, basestring):
|
|
return _levels[level.lower()]
|
|
else:
|
|
assert level in _levels.values()
|
|
return level
|
|
|
|
|
|
def levels():
|
|
return set(_levels.keys())
|
|
|
|
|
|
def clear_handlers(root_of_loggers):
|
|
loggers = [root_of_loggers]
|
|
if isinstance(root_of_loggers, logging.RootLogger):
|
|
loggers = logging.Logger.manager.loggerDict.values()
|
|
else:
|
|
for name, logger in logging.Logger.manager.loggerDict.items():
|
|
if name.startswith(root_of_loggers.name + '.'):
|
|
loggers += [logger]
|
|
for logger in loggers:
|
|
logger.handlers = []
|
|
|
|
|
|
try:
|
|
_null_handler = logging.NullHandler()
|
|
except:
|
|
# Python 2.6 compatibility
|
|
class NullHandler(logging.Handler):
|
|
def emit(self, record):
|
|
pass
|
|
_null_handler = NullHandler()
|
|
|
|
|
|
class Formatter(logging.Formatter):
|
|
wrapper = textwrap.TextWrapper(drop_whitespace=True,
|
|
initial_indent=' ',
|
|
subsequent_indent=' ',
|
|
break_long_words=False,
|
|
break_on_hyphens=False,
|
|
width=76)
|
|
|
|
def format(self, rec):
|
|
"""
|
|
:type rec: logging.LogRecord
|
|
"""
|
|
t = self.formatTime(rec, self.datefmt)
|
|
func = '' if rec.funcName == '<module>' else ' %s()' % rec.funcName
|
|
left_header_data = (t, rec.msecs, rec.name, func, rec.lineno)
|
|
left_header = '%s.%03d %s%s @ %d' % left_header_data
|
|
right_header = rec.levelname.lower()
|
|
spacer = 79 - 4 - len(left_header) - len(right_header)
|
|
top_line = left_header + ' -' + spacer * '-' + '- ' + right_header
|
|
lines = [_ for __ in textwrap.dedent(rec.getMessage()).splitlines()
|
|
for _ in self.wrapper.wrap(__)]
|
|
|
|
# This is more or less the logic in logging.Formatter.format() for
|
|
# exception logging, though greatly condensed.
|
|
if rec.exc_info:
|
|
exc_text = super(Formatter, self).formatException(rec.exc_info)
|
|
exc_lines = exc_text.splitlines()
|
|
# if len(exc_lines) > 4:
|
|
# exc_lines = exc_lines[:2] + ['...'] + exc_lines[-2:]
|
|
lines += [''] + [' ' + l for l in exc_lines]
|
|
|
|
return top_line + '\n' + '\n'.join(l for l in lines)
|
|
|
|
|
|
# This is how we overload `import`. Modelled on Andrew Moffat's `sh`.
|
|
class ImportWrapper(ModuleType):
|
|
def __init__(self, module):
|
|
self._module = module
|
|
|
|
# From the original -- these attributes are special.
|
|
for attr in ['__builtins__', '__doc__', '__name__', '__package__']:
|
|
setattr(self, attr, getattr(module, attr, None))
|
|
|
|
# Path settings per original -- seemingly obligatory.
|
|
self.__path__ = []
|
|
|
|
def __getattr__(self, name):
|
|
if name == 'log':
|
|
return logger(1)
|
|
return getattr(self._module, name)
|
|
|
|
|
|
log = logger(__name__)
|
|
|
|
|
|
self = sys.modules[__name__]
|
|
sys.modules[__name__] = ImportWrapper(self)
|