pax_global_header00006660000000000000000000000064136134352220014513gustar00rootroot0000000000000052 comment=68f377c2ba591dd709cd6f28c4f0f5845a8af45b logging_tree-1.8.1/000077500000000000000000000000001361343522200141675ustar00rootroot00000000000000logging_tree-1.8.1/.gitignore000066400000000000000000000000361361343522200161560ustar00rootroot00000000000000/MANIFEST /dist/ /.tox/ *.pyc logging_tree-1.8.1/.travis.yml000066400000000000000000000003521361343522200163000ustar00rootroot00000000000000# Config file for automatic testing at travis-ci.org language: python python: - "2.7" - "3.4" - "3.5" - "3.6" - "3.7" - "3.8" script: - "python -m unittest logging_tree.tests.test_format logging_tree.tests.test_node" logging_tree-1.8.1/COPYRIGHT000066400000000000000000000024311361343522200154620ustar00rootroot00000000000000Copyright © 2012, Brandon Rhodes All rights reserved. (The BSD License) Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. logging_tree-1.8.1/MANIFEST.in000066400000000000000000000000221361343522200157170ustar00rootroot00000000000000include COPYRIGHT logging_tree-1.8.1/README.md000066400000000000000000000010511361343522200154430ustar00rootroot00000000000000Welcome to the `logging_tree` Python project repository! You can install this package and read its documentation at the project’s official entry on the Python Package Index: https://pypi.python.org/pypi/logging_tree On Debian Testing and Unstable, you can install the package for the system Python versions with any of the standard Debian package tools: $ sudo apt-get install python-logging-tree The documentation is also available as the package docstring, kept inside of the `logging_tree/__init__.py` file here in the project repository. logging_tree-1.8.1/logging_tree/000077500000000000000000000000001361343522200166345ustar00rootroot00000000000000logging_tree-1.8.1/logging_tree/__init__.py000066400000000000000000000110631361343522200207460ustar00rootroot00000000000000"""Introspection for the ``logging`` logger tree in the Standard Library. You can install this package with the standard ``pip`` command:: $ pip install logging_tree While you can write programs that call this package's ``tree()`` function and examine the hierarchy of logger objects that it finds inside of the Standard Library ``logging`` module, the simplest use of this package for debugging is to call ``printout()`` to print the loggers, filters, and handlers that your application has configured:: >>> logging.getLogger('a') >>> logging.getLogger('a.b').setLevel(logging.DEBUG) >>> logging.getLogger('x.c') >>> from logging_tree import printout >>> printout() <--"" Level WARNING | o<--"a" | Level NOTSET so inherits level WARNING | | | o<--"a.b" | Level DEBUG | o<--[x] | o<--"x.c" Level NOTSET so inherits level WARNING The logger tree should always print successfully, no matter how complicated. A node whose ``[name]`` is in square brackets is a "place holder" that has never actually been named in a ``getLogger()`` call, but was created automatically to serve as the parent of loggers further down the tree. There are several interfaces that ``logging_tree`` supports, depending on how much detail you need. ``logging_tree.printout(node=None)`` Prints the current logger tree, or the tree based at the given `node`, to the standard output. ``logging_tree.format.build_description(node=None)`` Builds and returns the multi-line description of the current logger tree, or the tree based at the given ``node``, as a single string with newlines inside and a newline at the end. ``logging_tree.format.describe(node)`` A generator that yields a series of lines that describe the tree based at the given ``node``. Note that the lines are returned without newline terminators attached. ``logging_tree.tree()`` Fetch the current tree of loggers from the ``logging`` module. Returns a node, that is simply a tuple with three fields: | ``[0]`` the logger name (``""`` for the root logger). | ``[1]`` the ``logging.Logger`` object itself. | ``[2]`` a list of zero or more child nodes. I welcome contributions and ideas as this package matures. You can find the bug tracker at the `repository page on github `_. Developers can run this package's tests with:: $ python -m unittest discover logging_tree On older versions of Python you will instead have to install ``unittest2`` and use its ``unit2`` command line tool to run the tests. Changelog --------- **Version 1.8.1** - 2020 January 26 Adjust one test to make it pass under Python 3.8, and update the distribution classifiers to declare compatibility with Python versions through 3.8. **Version 1.8** - 2018 August 5 Improve the output to better explain what happens if a "parent" attribute has been set to None. **Version 1.7** - 2016 January 23 Detect whether each logger has the correct "parent" attribute and, if not, print where its log messages are being sent instead. **Version 1.6** - 2015 January 8 Fixed a crash that would occur if a custom logging Formatter was missing its format string attributes. **Version 1.5** - 2014 December 24 Handlers now display their logging level if one has been set, and their custom logging formatter if one has been installed. **Version 1.4** - 2014 January 8 Thanks to a contribution from Dave Brondsema, disabled loggers are now actually marked as "Disabled" to make it less of a surprise that they fail to log anything. **Version 1.3** - 2013 October 29 Be explicit and display the logger level ``NOTSET`` along with the effective level inherited from the logger's ancestors; and display the list of ``.filters`` of a custom logging handler even though it might contain custom code that ignores them. **Version 1.2** - 2013 January 19 Compatible with Python 3.3 thanks to @ralphbean. **Version 1.1** - 2012 February 17 Now compatible with 2.3 <= Python <= 3.2. **Version 1.0** - 2012 February 13 Can display the handler inside a MemoryHandler; entire public interface documented; 100% test coverage. **Version 0.6** - 2012 February 10 Added a display format for every ``logging.handlers`` class. **Version 0.5** - 2012 February 8 Initial release. """ __version__ = '1.8.1' __all__ = ('tree', 'printout') from logging_tree.nodes import tree from logging_tree.format import printout logging_tree-1.8.1/logging_tree/format.py000066400000000000000000000137411361343522200205040ustar00rootroot00000000000000"""Routines that pretty-print a hierarchy of logging `Node` objects.""" import logging.handlers import sys if sys.version_info < (2, 6): next = lambda generator: generator.next() # supply a missing builtin def printout(node=None): """Print a tree of loggers, given a `Node` from `logging_tree.nodes`. If no `node` argument is provided, then the entire tree of currently active `logging` loggers is printed out. """ print(build_description(node)[:-1]) def build_description(node=None): """Return a multi-line string describing a `logging_tree.nodes.Node`. If no `node` argument is provided, then the entire tree of currently active `logging` loggers is printed out. """ if node is None: from logging_tree.nodes import tree node = tree() return '\n'.join([ line.rstrip() for line in describe(node) ]) + '\n' def describe(node): """Generate lines describing the given `node` tuple. The `node` should be a tuple returned by `logging_tree.nodes.tree()`. """ return _describe(node, None) def _describe(node, parent): """Generate lines describing the given `node` tuple. This is the recursive back-end that powers ``describe()``. With its extra ``parent`` parameter, this routine remembers the nearest non-placeholder ancestor so that it can compare it against the actual value of the ``.parent`` attribute of each node. """ name, logger, children = node is_placeholder = isinstance(logger, logging.PlaceHolder) if is_placeholder: yield '<--[%s]' % name else: parent_is_correct = (parent is None) or (logger.parent is parent) if not logger.propagate: arrow = ' ' elif parent_is_correct: arrow = '<--' else: arrow = ' !-' yield '%s"%s"' % (arrow, name) if not parent_is_correct: if logger.parent is None: yield (' Broken .parent is None, so messages stop here') else: yield (' Broken .parent redirects messages to %r instead' % (logger.parent.name,)) if logger.level == logging.NOTSET: yield ' Level NOTSET so inherits level ' + logging.getLevelName( logger.getEffectiveLevel()) else: yield ' Level ' + logging.getLevelName(logger.level) if not logger.propagate: yield ' Propagate OFF' if logger.disabled: yield ' Disabled' # In case someone has defined a custom logger that lacks a # `filters` or `handlers` attribute, we call getattr() and # provide an empty sequence as a fallback. for f in getattr(logger, 'filters', ()): yield ' Filter %s' % describe_filter(f) for h in getattr(logger, 'handlers', ()): g = describe_handler(h) yield ' Handler %s' % next(g) for line in g: yield ' ' + line if children: if not is_placeholder: parent = logger last_child = children[-1] for child in children: g = _describe(child, parent) yield ' |' yield ' o' + next(g) if child is last_child: prefix = ' ' else: prefix = ' |' for line in g: yield prefix + line # The functions below must avoid `isinstance()`, since a Filter or # Handler subclass might implement behavior that renders our tidy # description quite useless. def describe_filter(f): """Return text describing the logging filter `f`.""" if f.__class__ is logging.Filter: # using type() breaks in Python <= 2.6 return 'name=%r' % f.name return repr(f) handler_formats = { # Someday we will switch to .format() when Py2.6 is gone. logging.StreamHandler: 'Stream %(stream)r', logging.FileHandler: 'File %(baseFilename)r', logging.handlers.RotatingFileHandler: 'RotatingFile %(baseFilename)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', } if sys.version_info >= (2, 5): handler_formats.update({ logging.handlers.TimedRotatingFileHandler: 'TimedRotatingFile %(baseFilename)r when=%(when)r' ' interval=%(interval)r backupCount=%(backupCount)r', }) if sys.version_info >= (2, 6): handler_formats.update({ logging.handlers.WatchedFileHandler: 'WatchedFile %(baseFilename)r', }) def describe_handler(h): """Yield one or more lines describing the logging handler `h`.""" t = h.__class__ # using type() breaks in Python <= 2.6 format = handler_formats.get(t) if format is not None: yield format % h.__dict__ else: yield repr(h) level = getattr(h, 'level', logging.NOTSET) if level != logging.NOTSET: yield ' Level ' + logging.getLevelName(level) for f in getattr(h, 'filters', ()): yield ' Filter %s' % describe_filter(f) formatter = getattr(h, 'formatter', None) if formatter is not None: if type(formatter) is logging.Formatter: yield ' Formatter fmt=%r datefmt=%r' % ( getattr(formatter, '_fmt', None), getattr(formatter, 'datefmt', None)) else: yield ' Formatter %r' % (formatter,) if t is logging.handlers.MemoryHandler and h.target is not None: yield ' Flushes output to:' g = describe_handler(h.target) yield ' Handler ' + next(g) for line in g: yield ' ' + line logging_tree-1.8.1/logging_tree/nodes.py000066400000000000000000000014271361343522200203220ustar00rootroot00000000000000"""Routine that explores the `logging` hierarchy and builds a `Node` tree.""" import logging def tree(): """Return a tree of tuples representing the logger layout. Each tuple looks like ``('logger-name', , [...])`` where the third element is a list of zero or more child tuples that share the same layout. """ root = ('', logging.root, []) nodes = {} items = list(logging.root.manager.loggerDict.items()) # for Python 2 and 3 items.sort() for name, logger in items: nodes[name] = node = (name, logger, []) i = name.rfind('.', 0, len(name) - 1) # same formula used in `logging` if i == -1: parent = root else: parent = nodes[name[:i]] parent[2].append(node) return root logging_tree-1.8.1/logging_tree/tests/000077500000000000000000000000001361343522200177765ustar00rootroot00000000000000logging_tree-1.8.1/logging_tree/tests/__init__.py000066400000000000000000000000001361343522200220750ustar00rootroot00000000000000logging_tree-1.8.1/logging_tree/tests/case.py000066400000000000000000000007021361343522200212620ustar00rootroot00000000000000"""Common test class for `logging` tests.""" import logging.handlers import unittest class LoggingTestCase(unittest.TestCase): """Test case that knows the secret: how to reset the logging module.""" def tearDown(self): super(LoggingTestCase, self).tearDown() logging.root = logging.RootLogger(logging.WARNING) logging.Logger.root = logging.root logging.Logger.manager = logging.Manager(logging.Logger.root) logging_tree-1.8.1/logging_tree/tests/test_format.py000066400000000000000000000200331361343522200226750ustar00rootroot00000000000000"""Tests for the `logging_tree.format` module.""" import logging import logging.handlers import unittest import sys from logging_tree.format import build_description, printout from logging_tree.tests.case import LoggingTestCase if sys.version_info >= (3,): from io import StringIO else: from StringIO import StringIO class FakeFile(StringIO): def __init__(self, filename, mode, encoding=None): self.filename = filename StringIO.__init__(self) def __repr__(self): return '' % self.filename def fileno(self): return 0 class FormatTests(LoggingTestCase): maxDiff = 9999 def setUp(self): # Prevent logging file handlers from trying to open real files. # (The keyword delay=1, which defers any actual attempt to open # a file, did not appear until Python 2.6.) logging.open = FakeFile super(FormatTests, self).setUp() def tearDown(self): del logging.open super(FormatTests, self).tearDown() def test_printout(self): stdout, sys.stdout = sys.stdout, StringIO() printout() self.assertEqual(sys.stdout.getvalue(), '<--""\n Level WARNING\n') sys.stdout = stdout def test_simple_tree(self): logging.getLogger('a') logging.getLogger('a.b').setLevel(logging.DEBUG) logging.getLogger('x.c') self.assertEqual(build_description(), '''\ <--"" Level WARNING | o<--"a" | Level NOTSET so inherits level WARNING | | | o<--"a.b" | Level DEBUG | o<--[x] | o<--"x.c" Level NOTSET so inherits level WARNING ''') def test_fancy_tree(self): logging.getLogger('').setLevel(logging.DEBUG) log = logging.getLogger('db') log.setLevel(logging.INFO) log.propagate = False log.disabled = 1 log.addFilter(MyFilter()) handler = logging.StreamHandler() handler.setFormatter(logging.Formatter()) log.addHandler(handler) handler.addFilter(logging.Filter('db.errors')) logging.getLogger('db.errors') logging.getLogger('db.stats') log = logging.getLogger('www.status') log.setLevel(logging.DEBUG) log.addHandler(logging.FileHandler('/foo/log.txt')) log.addHandler(MyHandler()) self.assertEqual(build_description(), '''\ <--"" Level DEBUG | o "db" | Level INFO | Propagate OFF | Disabled | Filter | Handler Stream %r | Filter name='db.errors' | Formatter fmt='%%(message)s' datefmt=None | | | o<--"db.errors" | | Level NOTSET so inherits level INFO | | | o<--"db.stats" | Level NOTSET so inherits level INFO | o<--[www] | o<--"www.status" Level DEBUG Handler File '/foo/log.txt' Handler ''' % (sys.stderr,)) def test_most_handlers(self): ah = logging.getLogger('').addHandler ah(logging.handlers.RotatingFileHandler( '/bar/one.txt', maxBytes=10000, backupCount=3)) ah(logging.handlers.SocketHandler('server.example.com', 514)) ah(logging.handlers.DatagramHandler('server.example.com', 1958)) ah(logging.handlers.SysLogHandler()) ah(logging.handlers.SMTPHandler( 'mail.example.com', 'Server', 'Sysadmin', 'Logs!')) # ah(logging.handlers.NTEventLogHandler()) ah(logging.handlers.HTTPHandler('api.example.com', '/logs', 'POST')) ah(logging.handlers.BufferingHandler(20000)) sh = logging.StreamHandler() ah(logging.handlers.MemoryHandler(30000, target=sh)) self.assertEqual(build_description(), '''\ <--"" Level WARNING Handler RotatingFile '/bar/one.txt' maxBytes=10000 backupCount=3 Handler Socket server.example.com 514 Handler Datagram server.example.com 1958 Handler SysLog ('localhost', 514) facility=1 Handler SMTP via mail.example.com to ['Sysadmin'] Handler HTTP POST to http://api.example.com//logs Handler Buffering capacity=20000 Handler Memory capacity=30000 Flushes output to: Handler Stream %r ''' % (sh.stream,)) logging.getLogger('').handlers[3].socket.close() # or Python 3 warning def test_2_dot_5_handlers(self): if sys.version_info < (2, 5): return ah = logging.getLogger('').addHandler ah(logging.handlers.TimedRotatingFileHandler('/bar/two.txt')) expected = '''\ <--"" Level WARNING Handler TimedRotatingFile '/bar/two.txt' when='H' interval=3600 backupCount=0 ''' if sys.version_info >= (3, 8): # Apparently the design of the TimedRotatingFileHandler has # become a bit more ambitious as of Python 3.8. expected += '''\ | o<--"asyncio" | Level NOTSET so inherits level WARNING | o<--[concurrent] | o<--"concurrent.futures" Level NOTSET so inherits level WARNING ''' self.assertEqual(build_description(), expected) def test_2_dot_6_handlers(self): if sys.version_info < (2, 6): return ah = logging.getLogger('').addHandler ah(logging.handlers.WatchedFileHandler('/bar/three.txt')) self.assertEqual(build_description(), '''\ <--"" Level WARNING Handler WatchedFile '/bar/three.txt' ''') def test_nested_handlers(self): h1 = logging.StreamHandler() h2 = logging.handlers.MemoryHandler(30000, target=h1) h2.addFilter(logging.Filter('worse')) h2.setLevel(logging.ERROR) h3 = logging.handlers.MemoryHandler(30000, target=h2) h3.addFilter(logging.Filter('bad')) logging.getLogger('').addHandler(h3) self.assertEqual(build_description(), '''\ <--"" Level WARNING Handler Memory capacity=30000 Filter name='bad' Flushes output to: Handler Memory capacity=30000 Level ERROR Filter name='worse' Flushes output to: Handler Stream %r ''' % (h1.stream,)) def test_formatter_with_no_fmt_attributes(self): f = logging.Formatter() del f._fmt del f.datefmt h = logging.StreamHandler() h.setFormatter(f) logging.getLogger('').addHandler(h) self.assertEqual(build_description(), '''\ <--"" Level WARNING Handler Stream %r Formatter fmt=None datefmt=None ''' % (h.stream,)) def test_formatter_that_is_not_a_Formatter_instance(self): h = logging.StreamHandler() h.setFormatter("Ceci n'est pas une formatter") logging.getLogger('').addHandler(h) self.assertEqual(build_description(), '''\ <--"" Level WARNING Handler Stream %r Formatter "Ceci n'est pas une formatter" ''' % (h.stream,)) def test_handler_with_wrong_parent_attribute(self): logging.getLogger('celery') logging.getLogger('app.model') logging.getLogger('app.task').parent = logging.getLogger('celery.task') logging.getLogger('app.view') self.assertEqual(build_description(), '''\ <--"" Level WARNING | o<--[app] | | | o<--"app.model" | | Level NOTSET so inherits level WARNING | | | o !-"app.task" | | Broken .parent redirects messages to 'celery.task' instead | | Level NOTSET so inherits level WARNING | | | o<--"app.view" | Level NOTSET so inherits level WARNING | o<--"celery" Level NOTSET so inherits level WARNING | o<--"celery.task" Level NOTSET so inherits level WARNING ''') def test_handler_with_parent_attribute_that_is_none(self): logging.getLogger('app').parent = None self.assertEqual(build_description(), '''\ <--"" Level WARNING | o !-"app" Broken .parent is None, so messages stop here Level NOTSET so inherits level NOTSET ''') class MyFilter(object): def __repr__(self): return '' class MyHandler(object): def __repr__(self): return '' if __name__ == '__main__': # for Python <= 2.4 unittest.main() logging_tree-1.8.1/logging_tree/tests/test_node.py000066400000000000000000000026001361343522200223320ustar00rootroot00000000000000"""Tests for the `logging_tree.node` module.""" import logging.handlers import unittest from logging_tree.nodes import tree from logging_tree.tests.case import LoggingTestCase class AnyPlaceHolder(object): def __eq__(self, other): return isinstance(other, logging.PlaceHolder) any_placeholder = AnyPlaceHolder() class NodeTests(LoggingTestCase): def test_default_tree(self): self.assertEqual(tree(), ('', logging.root, [])) def test_one_level_tree(self): a = logging.getLogger('a') b = logging.getLogger('b') self.assertEqual(tree(), ( '', logging.root, [ ('a', a, []), ('b', b, []), ])) def test_two_level_tree(self): a = logging.getLogger('a') b = logging.getLogger('a.b') self.assertEqual(tree(), ( '', logging.root, [ ('a', a, [ ('a.b', b, []), ]), ])) def test_two_level_tree_with_placeholder(self): b = logging.getLogger('a.b') self.assertEqual(tree(), ( '', logging.root, [ ('a', any_placeholder, [ ('a.b', b, []), ]), ])) if __name__ == '__main__': # for Python <= 2.4 unittest.main() logging_tree-1.8.1/release.sh000077500000000000000000000003061361343522200161450ustar00rootroot00000000000000#!/bin/bash set -e python3 setup.py sdist upload || true sed -i '1iimport setuptools' setup.py python3 setup.py bdist_wheel upload || true git checkout setup.py rm -r build logging_tree.egg-info logging_tree-1.8.1/setup.cfg000066400000000000000000000000321361343522200160030ustar00rootroot00000000000000[bdist_wheel] universal=1 logging_tree-1.8.1/setup.py000066400000000000000000000023241361343522200157020ustar00rootroot00000000000000from distutils.core import setup import logging_tree setup(name='logging_tree', version=logging_tree.__version__, description='Introspect and display the logger tree inside "logging"', long_description=logging_tree.__doc__, author='Brandon Rhodes', author_email='brandon@rhodesmill.org', url='https://github.com/brandon-rhodes/logging_tree', classifiers=[ 'Development Status :: 6 - Mature', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 2.3', 'Programming Language :: Python :: 2.4', 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Topic :: System :: Logging', ], packages=['logging_tree', 'logging_tree.tests'], ) logging_tree-1.8.1/tox.ini000066400000000000000000000003371361343522200155050ustar00rootroot00000000000000[tox] envlist = py24, py25, py26, py27, py32, py33, py34, py35 [testenv] deps = py2{4,5,6}: unittest2 commands = py{27,32,33,34}: python -m unittest discover logging_tree py2{4,5,6}: unit2 discover logging_tree