diff -r d232cff25bbd -r 64df448c183d Doc/library/logging.rst --- a/Doc/library/logging.rst Sat Apr 27 00:20:04 2013 +0200 +++ b/Doc/library/logging.rst Sat Apr 27 14:31:53 2013 +0100 @@ -1134,6 +1134,86 @@ :kwargs: Additional keyword arguments. +.. function:: snapshot(config=None) + + Provide a snapshot of the logger hierarchy (loggers, handlers, filters and + formatters). The return value is a dictionary. The keys of that dictionary + are logger names, with ``''`` being the key for the root logger. The + corresponding values are instances of :class:`namedtuple` subclasses; they + are snapshots of the loggers, handlers, formatters and filters. + + You can customise the return value by passing in a configuration dictionary. + If none is provided, the default value at ``logging.Snapper.DEFAULT_CONFIG`` + is used. You can make a copy of that, add or change elements in your copy, + and then pass that in to :func:`snapshot`. + + The configuration dictionary currently has two keys - ``attrmap`` and + ``format`` -- whose values are dictionaries. The dictionary is used when + converting each logging object to its snapshot representation. Each object, + and each attribute of that object, is passed recursively through the + conversion process. This process works roughly as follows: + + #. A ``dict`` instance is converted to another dict instance whose keys and + values have passed through the conversion process. + #. Instances of ``list`` and ``tuple`` are converted to a ``tuple`` instance + whose elements are the converted source elements. + #. Instances of ``int``, ``float``, and ``str`` are passed through unchanged. + ``None``, ``True`` and ``False`` are passed through unchanged. (If needed, + this could also be applied to ``complex`` and ``decimal.Decimal`` types, + but it seems unlikely these would be used in a logging configuration.) + #. For instances, a qualified class name is computed from + ``instance.__class__.__name__`` and ``instance.__module__`` (if present). + This is used as the key to the attrmap and format configuration + sub-dictionaries. The format dictionary is checked first -- if the key is + present, the value should be a callable which takes a single argument -- + the instance -- and returns the representation. This is used, for example, + to convert internal implementation class instances such as + ``PercentStyle``, ``StrFormatStyle`` and ``StringTemplateStyle`` to the + strings ``'%'``, ``'{'`` and ``'$'``. (The value for + ``config['attrmap']['logging.PercentStyle']`` is ``lambda x: '%'``.) If + the key is not present in ``format``, then ``attrmap`` is checked for the + key. If present, the value should be a space-separated list of attribute + names to bring into the snapshot; if an attribute name starts with an + underscore, you can represent this as e.g. _fmt:format, where the instance + attribute in the logging object will be ``_fmt`` but the field name in the + corresponding namedtuple will be ``format``. (Field names in named tuples + can’t start with an underscore.) All named tuple classes have the field + name ``class_`` included, which holds the class name of the type + represented by the named tuple. + + This arrangement allows user-defined handler classes, for example, to be + easily accommodated by just adding corresponding entries in the configuration + dictionary. A part of the default configuration dictionary is shown below:: + + DEFAULT_CONFIG = { + 'attrmap': { + 'TextIOWrapper': 'name mode encoding', + 'logging.Formatter': 'datefmt _fmt:format _style:style', + 'logging.Logger': 'disabled filters handlers level name ' + 'propagate children', + 'logging.StreamHandler': 'filters formatter level stream', + 'logging.FileHandler': 'filters formatter level ' + 'baseFilename:filename mode encoding delay', + 'logging.handlers.SMTPHandler': 'filters formatter level mailhost ' + 'mailport fromaddr toaddrs username password subject secure ' + 'timeout', + }, + 'format': { + 'logging.PercentStyle': lambda x : '%', + 'logging.StrFormatStyle': lambda x : '{', + 'logging.StringTemplateStyle': lambda x : '$', + }, + } + + Note that the logging hierarchy can change at any time in a multi-threaded + environment. Hence, the snapshot represents the state of the hierarchy at + the point where :func:`snapshot` is called, and the returned values are + immutable and so cannot be used to effect changes to the logging + configuration. Use other APIs provided for that: this function is intended + to be used for diagnosing problems and troubleshooting logging + configurations. + + Module-Level Attributes ----------------------- @@ -1188,3 +1268,5 @@ and 2.2.x, which do not include the :mod:`logging` package in the standard library. + + diff -r d232cff25bbd -r 64df448c183d Lib/logging/__init__.py --- a/Lib/logging/__init__.py Sat Apr 27 00:20:04 2013 +0200 +++ b/Lib/logging/__init__.py Sat Apr 27 14:31:53 2013 +0100 @@ -23,7 +23,8 @@ To use, simply 'import logging' and log away! """ -import sys, os, time, io, traceback, warnings, weakref +import sys, os, time, io, traceback, warnings, weakref, re +from collections import namedtuple from string import Template __all__ = ['BASIC_FORMAT', 'BufferingFormatter', 'CRITICAL', 'DEBUG', 'ERROR', @@ -33,7 +34,8 @@ 'captureWarnings', 'critical', 'debug', 'disable', 'error', 'exception', 'fatal', 'getLevelName', 'getLogger', 'getLoggerClass', 'info', 'log', 'makeLogRecord', 'setLoggerClass', 'warn', 'warning', - 'getLogRecordFactory', 'setLogRecordFactory', 'lastResort'] + 'getLogRecordFactory', 'setLogRecordFactory', 'lastResort', + 'Snapper', 'snapshot'] try: import threading @@ -1914,3 +1916,176 @@ if _warnings_showwarning is not None: warnings.showwarning = _warnings_showwarning _warnings_showwarning = None + +# Snapshot functionality + +class Snapper(object): + + DEFAULT_CONFIG = { + 'attrmap': { + 'TextIOWrapper': 'name mode encoding', + 'logging.PlaceHolder': 'children', + 'logging.Formatter': 'datefmt _fmt:format _style:style', + 'logging.Filter': 'name', + 'logging.RootLogger': 'filters handlers level propagate children', + 'logging.Logger': 'disabled filters handlers level name ' + 'propagate children', + 'logging.Handler': 'filters formatter level', + 'logging.StreamHandler': 'filters formatter level stream', + 'logging.FileHandler': 'filters formatter level ' + 'baseFilename:filename mode encoding delay', + 'logging.handlers.WatchedFileHandler': 'filters formatter level ' + 'baseFilename:filename mode encoding delay', + 'logging.handlers.RotatingFileHandler': 'filters formatter level ' + 'baseFilename:filename mode encoding maxBytes backupCount ' + 'delay', + 'logging.handlers.TimedRotatingFileHandler': 'filters formatter ' + 'level baseFilename:filename mode encoding when interval ' + 'backupCount delay utc', + 'logging.handlers.SocketHandler': 'filters formatter level host ' + 'port closeOnError retryStart retryMax retryFactor', + 'logging.handlers.DatagramHandler': 'filters formatter level host ' + 'port closeOnError', + 'logging.handlers.SysLogHandler': 'filters formatter level address ' + 'facility socktype', + 'logging.handlers.SMTPHandler': 'filters formatter level mailhost ' + 'mailport fromaddr toaddrs username password subject secure ' + 'timeout', + 'logging.handlers.NTEventLogHandler': 'filters formatter level ' + 'appname dllname logtype', + 'logging.handlers.HTTPHandler': 'filters formatter level host ' + 'url method secure credentials', + 'logging.handlers.BufferingHandler': 'filters formatter level ' + 'capacity', + 'logging.handlers.MemoryHandler': 'filters formatter level ' + 'capacity flushLevel target', + 'logging.handlers.QueueHandler': 'filters formatter level', + }, + 'format': { + 'logging.PercentStyle': lambda x : '%', + 'logging.StrFormatStyle': lambda x : '{', + 'logging.StringTemplateStyle': lambda x : '$', + }, + } + + LEADING_UNDERSCORE = re.compile('^_') + + class Context(object): + def __init__(self, config): + self.converted = {} + self.typemap = dict(config.get('typemap', {})) + self.attrmap = attrmap = dict(config.get('attrmap', {})) + self.format = dict(config.get('format', {})) + for k in attrmap: + v = attrmap[k] + if not isinstance(v, dict): + d = {} + attrs = v.split() + for attr in attrs: + if ':' in attr: + attr = attr.split(':', 1) + d[attr[0]] = attr[1] + else: + d[attr] = attr + attrmap[k] = d + + def __init__(self, config=None): + self.config = config or self.DEFAULT_CONFIG + + def snapshot(self): + convert = self.convert + ctx = self.Context(self.config) + _acquireLock() + self.root = Logger.manager.root + ld = dict(Logger.manager.loggerDict) + self.logger_items = sorted(ld.items()) + try: + result = { '': convert(ctx, self.root) } + for k, v in ld.items(): + result[k] = convert(ctx, v) + finally: + _releaseLock() + return result + + def get_children(self, logger_or_placeholder): + if logger_or_placeholder is self.root: + result = [(k, v) for k, v in self.logger_items if '.' not in k] + else: + index = -1 + for i, item in enumerate(self.logger_items): + k, v = item + if v is logger_or_placeholder: + index = i + name = k + nlen = len(k) + break + result = [] + if index >= 0: + for k, v in self.logger_items[index + 1:]: + if (k.startswith(name) and k[nlen:nlen + 1] == '.' and + '.' not in k[nlen+2:]): + result.append((k, v)) + return result + + def convert(self, context, value): + cnv = self.convert + if isinstance(value, dict): + result = dict([cnv(context, x) for x in value.items()]) + elif isinstance(value, (list, tuple)): + result = tuple([cnv(context, x) for x in value]) + elif value is None or isinstance(value, (int, float, str)): + result = value + else: + if not hasattr(value, '__module__'): + key = value.__class__.__name__ + else: + key = '%s.%s' % (value.__module__, value.__class__.__name__) + if key in context.format: + result = context.format[key](value) + elif value in context.converted: + result = context.converted[value] + else: + if key in context.attrmap: + attrs = context.attrmap[key] + elif (isinstance(value, Handler) and + 'logging.Handler' in context.attrmap): + attrs = context.attrmap['logging.Handler'] + elif (isinstance(value, Filter) and + 'logging.Filter' in context.attrmap): + attrs = context.attrmap['logging.Filter'] + elif (isinstance(value, Logger) and + 'logging.Logger' in context.attrmap): + attrs = context.attrmap['logging.Logger'] + elif (isinstance(value, Formatter) and + 'logging.Formatter' in context.attrmap): + attrs = context.attrmap['logging.Formatter'] + else: + attrs = {} + for k in value.__dict__: + attrs[k] = self.LEADING_UNDERSCORE.sub('', k) + context.attrmap[key] = attrs + if key in context.typemap: + cls = context.typemap[key] + else: + fields = ' '.join(sorted(attrs.values())) + cn = '%sInfo' % key.replace('.', '_') + prefix = 'class_ ' + cls = namedtuple(cn, prefix + fields) + context.typemap[key] = cls + d = {} + did_children = False + if isinstance(value, (Logger, PlaceHolder)): + d['children'] = cnv(context, self.get_children(value)) + did_children = True + for k, v in attrs.items(): + if k == 'children' and did_children: + continue + d[v] = cnv(context, getattr(value, k, None)) + result = cls(class_=key, **d) + context.converted[value] = result + return result + +snapper = Snapper + +def snapshot(config=None): + return snapper(config).snapshot() diff -r d232cff25bbd -r 64df448c183d Lib/test/test_logging.py --- a/Lib/test/test_logging.py Sat Apr 27 00:20:04 2013 +0200 +++ b/Lib/test/test_logging.py Sat Apr 27 14:31:53 2013 +0100 @@ -4078,6 +4078,199 @@ msg = 'Record not found in event log, went back %d records' % GO_BACK self.assertTrue(found, msg=msg) + +class SnapshotTest(BaseTest): + def test_basic(self): + logging_config = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, + 'filters': { + 'special': { + '()': 'logging.Filter', + 'name': 'foobar', + } + }, + 'handlers': { + 'null': { + 'level':'DEBUG', + 'class':'logging.NullHandler', + }, + 'console':{ + 'level':'DEBUG', + 'class':'logging.StreamHandler', + 'formatter': 'simple' + }, + 'mail_admins': { + 'level': 'ERROR', + 'class': 'logging.handlers.SMTPHandler', + 'mailhost': 'mymailhost', + 'fromaddr': 'me@example.com', + 'toaddrs': ['you@example.com'], + 'subject': 'Pay attention to this', + 'filters': ['special'] + } + }, + 'loggers': { + 'django': { + 'handlers':['null'], + 'propagate': True, + 'level':'INFO', + }, + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + }, + 'myproject.custom.subproject': { + 'handlers': ['console', 'mail_admins'], + 'level': 'INFO', + 'filters': ['special'] + } + } + } + + def describe_filter(f): + if f.class_ == 'logging.Filter': + return 'name=%r' % f.name + return repr(f) + + handler_formats = { + 'logging.StreamHandler': 'Stream %(stream)r', + 'logging.FileHandler': 'File %(filename)r', + 'logging.handlers.RotatingFileHandler': 'RotatingFile %(filename)r' + ' maxBytes=%(maxBytes)r backupCount=%(backupCount)r', + 'logging.handlers.SocketHandler': 'Socket %(host)s %(port)r', + 'logging.handlers.DatagramHandler': 'Datagram %(host)s %(port)r', + 'logging.handlers.SysLogHandler': + 'SysLog %(address)r facility=%(facility)r', + 'logging.handlers.SMTPHandler': + 'SMTP via %(mailhost)s to %(toaddrs)s', + 'logging.handlers.HTTPHandler': + 'HTTP %(method)s to http://%(host)s/%(url)s', + 'logging.handlers.BufferingHandler': + 'Buffering capacity=%(capacity)r', + 'logging.handlers.MemoryHandler': + 'Memory capacity=%(capacity)r dumping to:', + 'logging.handlers.TimedRotatingFileHandler': + 'TimedRotatingFile %(filename)r when=%(when)r' + ' interval=%(interval)r backupCount=%(backupCount)r', + 'logging.handlers.WatchedFileHandler': 'WatchedFile %(filename)r', + 'logging.NullHandler': 'Null', + } + + + def describe_handler(h): + result = [] + t = h.class_ + format = handler_formats.get(h.class_) + if format is None: + result.append(repr(h)) + else: + result.append(format % h._asdict()) + for f in getattr(h, 'filters', ()): + result.append(' Filter %s' % describe_filter(f)) + if t == 'logging.handlers.MemoryHandler' and h.target is not None: + g = describe_handler(h.target) + result.append(' Handler %s' % g[0]) + for line in g[1:]: + result.append(' ' + line) + return result + + def hierarchy_level(name): + if name == '': + result = 0 + else: + result = len(name.split('.')) + return result + + def describe(name, logger): + result = [] + is_placeholder = logger.class_ == 'logging.PlaceHolder' + if is_placeholder or logger.propagate: + arrow = '<--' + else: + arrow = ' ' + if is_placeholder: + name = '[%s]' % name + else: + name = '"%s"' % name + result.append(arrow + name) + if not is_placeholder: + if logger.level: + result.append(' Level ' + logging.getLevelName(logger.level)) + if not logger.propagate: + result.append(' Propagate OFF') + for f in getattr(logger, 'filters', ()): + result.append(' Filter %s' % describe_filter(f)) + for h in getattr(logger, 'handlers', ()): + g = describe_handler(h) + result.append(' Handler %s' % g[0]) + for line in g[1:]: + result.append(' ' + line) + children = logger.children + if children: + last_child = children[-1] + for child in children: + name, logger = child + g = describe(name, logger) + result.append(' |') + result.append(' o' + g[0]) + if child is last_child: + prefix = ' ' + else: + prefix = ' |' + for line in g[1:]: + result.append(prefix + line) + return result + + def format_snapshot(d): + return describe('', d['']) + + logging.config.dictConfig(logging_config) + lines = format_snapshot(logging.snapshot()) + expected = [ + '<--""', + ' Level DEBUG', + " Handler Stream StringIOInfo(class_='StringIO')", + ' |', + ' o<--"django"', + ' | Level INFO', + ' | Handler Null', + ' | |', + ' | o "django.request"', + ' | Level ERROR', + ' | Propagate OFF', + " | Handler SMTP via mymailhost to ('you@example.com',)", + " | Filter name='foobar'", + ' |', + ' o<--[myproject]', + ' | |', + ' | o<--[myproject.custom]', + ' | |', + ' | o<--"myproject.custom.subproject"', + ' | Level INFO', + " | Filter name='foobar'", + " | Handler Stream TextIOWrapperInfo(class_=" + "'TextIOWrapper', encoding='UTF-8', mode='w', name='')", + " | Handler SMTP via mymailhost to " + "('you@example.com',)", + " | Filter name='foobar'", + ' |', + ' o<--"«×»"', + ' |', + ' o<--"ĿÖG"', + ] + self.maxDiff = None + self.assertEqual(lines, expected) + # Set the locale to the platform-dependent default. I have no idea # why the test does this, but in any case we save the current locale # first and restore it at the end. @@ -4094,7 +4287,7 @@ SMTPHandlerTest, FileHandlerTest, RotatingFileHandlerTest, LastResortTest, LogRecordTest, ExceptionTest, SysLogHandlerTest, HTTPHandlerTest, NTEventLogHandlerTest, - TimedRotatingFileHandlerTest + TimedRotatingFileHandlerTest, SnapshotTest ) if __name__ == "__main__":