├── MANIFEST.in ├── logging_tree ├── tests │ ├── __init__.py │ ├── case.py │ ├── test_node.py │ └── test_format.py ├── nodes.py ├── format.py └── __init__.py ├── FUNDING.yml ├── setup.cfg ├── .gitignore ├── release.sh ├── tox.ini ├── .github └── workflows │ └── logging-tree-tests.yml ├── README.md ├── test.sh ├── COPYRIGHT └── setup.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include COPYRIGHT 2 | -------------------------------------------------------------------------------- /logging_tree/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: brandon-rhodes 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /MANIFEST 2 | /dist/ 3 | /.tox/ 4 | *.pyc 5 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | PYTHONDONTWRITEBYTECODE= python -m build . 6 | rm -rf logging_tree.egg-info 7 | echo 8 | echo Now run: twine upload ... for both the new files in build/ 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # To test against as many versions of Python as feasible, I run: 2 | # 3 | # tox --discover ~/.pyenv/versions/*/bin/python 4 | # 5 | # Unfortunately tox has lost its ability to detect older versions of 6 | # Python like 2.6 (much less 2.3 or 2.4); see the accompanying `test.sh` 7 | # script for an alternative. 8 | 9 | [tox] 10 | envlist = py27,py36,py37,py38,py39 11 | [testenv] 12 | commands = 13 | python -m unittest discover logging_tree 14 | -------------------------------------------------------------------------------- /.github/workflows/logging-tree-tests.yml: -------------------------------------------------------------------------------- 1 | name: logging_tree tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-20.04 12 | strategy: 13 | matrix: 14 | python: [python2, python3] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Print Python version 19 | run: ${{matrix.python}} --version 20 | - name: Test 21 | run: ${{matrix.python}} -m unittest discover -v logging_tree 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Welcome to the `logging_tree` Python project repository! 2 | 3 | You can install this package and read its documentation 4 | at the project’s official entry on the Python Package Index: 5 | 6 | https://pypi.python.org/pypi/logging_tree 7 | 8 | On Debian Testing and Unstable, you can install the package for the 9 | system Python versions with any of the standard Debian package tools: 10 | 11 | $ sudo apt-get install python-logging-tree 12 | 13 | The documentation is also available as the package docstring, 14 | kept inside of the `logging_tree/__init__.py` file here in the 15 | project repository. 16 | -------------------------------------------------------------------------------- /logging_tree/tests/case.py: -------------------------------------------------------------------------------- 1 | """Common test class for `logging` tests.""" 2 | 3 | import logging.handlers 4 | import unittest 5 | 6 | class LoggingTestCase(unittest.TestCase): 7 | """Test case that knows the secret: how to reset the logging module.""" 8 | 9 | def setUp(self): 10 | reset_logging() 11 | super(LoggingTestCase, self).setUp() 12 | 13 | def tearDown(self): 14 | reset_logging() 15 | super(LoggingTestCase, self).tearDown() 16 | 17 | def reset_logging(): 18 | logging.root = logging.RootLogger(logging.WARNING) 19 | logging.Logger.root = logging.root 20 | logging.Logger.manager = logging.Manager(logging.Logger.root) 21 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This merely tests that `logging_tree` works when run directly from 4 | # source (and, of course, only tests the versions of Python you happen 5 | # to have installed with pyenv). For a comprehensive test of whether it 6 | # will actually work if installed from its distribution, see `tox.ini`. 7 | 8 | errors=0 9 | 10 | for python in $(ls ~/.pyenv/versions/*/bin/python | sort -t. -k 2,2 -k 3n) 11 | do 12 | echo 13 | echo ====================================================================== 14 | echo $python 15 | echo ====================================================================== 16 | for test in logging_tree/tests/test_*.py 17 | do 18 | if ! PYTHONPATH=. $python $test 19 | then 20 | let "errors=errors+1" 21 | fi 22 | done 23 | done 24 | 25 | echo 26 | echo "Failure count: $errors" 27 | -------------------------------------------------------------------------------- /logging_tree/nodes.py: -------------------------------------------------------------------------------- 1 | """Routine that explores the `logging` hierarchy and builds a `Node` tree.""" 2 | 3 | import logging 4 | 5 | def tree(): 6 | """Return a tree of tuples representing the logger layout. 7 | 8 | Each tuple looks like ``('logger-name', , [...])`` where the 9 | third element is a list of zero or more child tuples that share the 10 | same layout. 11 | 12 | """ 13 | root = ('', logging.root, []) 14 | nodes = {} 15 | items = list(logging.root.manager.loggerDict.items()) # for Python 2 and 3 16 | items.sort() 17 | for name, logger in items: 18 | nodes[name] = node = (name, logger, []) 19 | i = name.rfind('.', 0, len(name) - 1) # same formula used in `logging` 20 | if i == -1: 21 | parent = root 22 | else: 23 | parent = nodes[name[:i]] 24 | parent[2].append(node) 25 | return root 26 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright © 2012, Brandon Rhodes 2 | All rights reserved. 3 | 4 | (The BSD License) 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | 13 | Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 18 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 19 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 20 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 23 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | import logging_tree 3 | 4 | setup(name='logging_tree', 5 | version=logging_tree.__version__, 6 | description='Introspect and display the logger tree inside "logging"', 7 | long_description=logging_tree.__doc__, 8 | author='Brandon Rhodes', 9 | author_email='brandon@rhodesmill.org', 10 | url='https://github.com/brandon-rhodes/logging_tree', 11 | classifiers=[ 12 | 'Development Status :: 6 - Mature', 13 | 'Intended Audience :: Developers', 14 | 'License :: OSI Approved :: BSD License', 15 | 'Programming Language :: Python :: 2.3', 16 | 'Programming Language :: Python :: 2.4', 17 | 'Programming Language :: Python :: 2.5', 18 | 'Programming Language :: Python :: 2.6', 19 | 'Programming Language :: Python :: 2.7', 20 | 'Programming Language :: Python :: 3.2', 21 | 'Programming Language :: Python :: 3.3', 22 | 'Programming Language :: Python :: 3.4', 23 | 'Programming Language :: Python :: 3.5', 24 | 'Programming Language :: Python :: 3.6', 25 | 'Programming Language :: Python :: 3.7', 26 | 'Programming Language :: Python :: 3.8', 27 | 'Programming Language :: Python :: 3.9', 28 | 'Programming Language :: Python :: 3.10', 29 | 'Programming Language :: Python :: 3.11', 30 | 'Programming Language :: Python :: 3.12', 31 | 'Topic :: System :: Logging', 32 | ], 33 | packages=['logging_tree', 'logging_tree.tests'], 34 | ) 35 | -------------------------------------------------------------------------------- /logging_tree/tests/test_node.py: -------------------------------------------------------------------------------- 1 | """Tests for the `logging_tree.node` module.""" 2 | 3 | import logging.handlers 4 | import unittest 5 | from logging_tree.nodes import tree 6 | from logging_tree.tests.case import LoggingTestCase 7 | 8 | class AnyPlaceHolder(object): 9 | def __eq__(self, other): 10 | return isinstance(other, logging.PlaceHolder) 11 | 12 | any_placeholder = AnyPlaceHolder() 13 | 14 | class NodeTests(LoggingTestCase): 15 | 16 | def test_default_tree(self): 17 | self.assertEqual(tree(), ('', logging.root, [])) 18 | 19 | def test_one_level_tree(self): 20 | a = logging.getLogger('a') 21 | b = logging.getLogger('b') 22 | self.assertEqual(tree(), ( 23 | '', logging.root, [ 24 | ('a', a, []), 25 | ('b', b, []), 26 | ])) 27 | 28 | def test_two_level_tree(self): 29 | a = logging.getLogger('a') 30 | b = logging.getLogger('a.b') 31 | self.assertEqual(tree(), ( 32 | '', logging.root, [ 33 | ('a', a, [ 34 | ('a.b', b, []), 35 | ]), 36 | ])) 37 | 38 | def test_two_level_tree_with_placeholder(self): 39 | b = logging.getLogger('a.b') 40 | self.assertEqual(tree(), ( 41 | '', logging.root, [ 42 | ('a', any_placeholder, [ 43 | ('a.b', b, []), 44 | ]), 45 | ])) 46 | 47 | 48 | if __name__ == '__main__': # for Python <= 2.4 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /logging_tree/format.py: -------------------------------------------------------------------------------- 1 | """Routines that pretty-print a hierarchy of logging `Node` objects.""" 2 | 3 | import logging.handlers 4 | import sys 5 | 6 | if sys.version_info < (2, 6): 7 | def next(generator): 8 | return generator.next() 9 | 10 | def printout(node=None): 11 | """Print a tree of loggers, given a `Node` from `logging_tree.nodes`. 12 | 13 | If no `node` argument is provided, then the entire tree of currently 14 | active `logging` loggers is printed out. 15 | 16 | """ 17 | print(build_description(node)[:-1]) 18 | 19 | 20 | def build_description(node=None): 21 | """Return a multi-line string describing a `logging_tree.nodes.Node`. 22 | 23 | If no `node` argument is provided, then the entire tree of currently 24 | active `logging` loggers is printed out. 25 | 26 | """ 27 | if node is None: 28 | from logging_tree.nodes import tree 29 | node = tree() 30 | return '\n'.join([ line.rstrip() for line in describe(node) ]) + '\n' 31 | 32 | 33 | def describe(node): 34 | """Generate lines describing the given `node` tuple. 35 | 36 | The `node` should be a tuple returned by `logging_tree.nodes.tree()`. 37 | 38 | """ 39 | return _describe(node, None) 40 | 41 | def _describe(node, parent): 42 | """Generate lines describing the given `node` tuple. 43 | 44 | This is the recursive back-end that powers ``describe()``. With its 45 | extra ``parent`` parameter, this routine remembers the nearest 46 | non-placeholder ancestor so that it can compare it against the 47 | actual value of the ``.parent`` attribute of each node. 48 | 49 | """ 50 | name, logger, children = node 51 | is_placeholder = isinstance(logger, logging.PlaceHolder) 52 | if is_placeholder: 53 | yield '<--[%s]' % name 54 | else: 55 | parent_is_correct = (parent is None) or (logger.parent is parent) 56 | if not logger.propagate: 57 | arrow = ' ' 58 | elif parent_is_correct: 59 | arrow = '<--' 60 | else: 61 | arrow = ' !-' 62 | yield '%s"%s"' % (arrow, name) 63 | if not parent_is_correct: 64 | if logger.parent is None: 65 | yield (' Broken .parent is None, so messages stop here') 66 | else: 67 | yield (' Broken .parent redirects messages to %r instead' 68 | % (logger.parent.name,)) 69 | if logger.level == logging.NOTSET: 70 | yield ' Level NOTSET so inherits level ' + logging.getLevelName( 71 | logger.getEffectiveLevel()) 72 | else: 73 | yield ' Level ' + logging.getLevelName(logger.level) 74 | if not logger.propagate: 75 | yield ' Propagate OFF' 76 | if logger.disabled: 77 | yield ' Disabled' 78 | 79 | # In case someone has defined a custom logger that lacks a 80 | # `filters` or `handlers` attribute, we call getattr() and 81 | # provide an empty sequence as a fallback. 82 | 83 | for f in getattr(logger, 'filters', ()): 84 | yield ' Filter %s' % describe_filter(f) 85 | for h in getattr(logger, 'handlers', ()): 86 | g = describe_handler(h) 87 | yield ' Handler %s' % next(g) 88 | for line in g: 89 | yield ' ' + line 90 | 91 | if children: 92 | if not is_placeholder: 93 | parent = logger 94 | last_child = children[-1] 95 | for child in children: 96 | g = _describe(child, parent) 97 | yield ' |' 98 | yield ' o' + next(g) 99 | if child is last_child: 100 | prefix = ' ' 101 | else: 102 | prefix = ' |' 103 | for line in g: 104 | yield prefix + line 105 | 106 | 107 | # The functions below must avoid `isinstance()`, since a Filter or 108 | # Handler subclass might implement behavior that renders our tidy 109 | # description quite useless. 110 | 111 | 112 | def describe_filter(f): 113 | """Return text describing the logging filter `f`.""" 114 | if f.__class__ is logging.Filter: # using type() breaks in Python <= 2.6 115 | return 'name=%r' % f.name 116 | return repr(f) 117 | 118 | 119 | handler_formats = { # Someday we will switch to .format() when Py2.6 is gone. 120 | logging.StreamHandler: 'Stream %(stream)r', 121 | logging.FileHandler: 'File %(baseFilename)r', 122 | logging.handlers.RotatingFileHandler: 'RotatingFile %(baseFilename)r' 123 | ' maxBytes=%(maxBytes)r backupCount=%(backupCount)r', 124 | logging.handlers.SocketHandler: 'Socket %(host)s %(port)r', 125 | logging.handlers.DatagramHandler: 'Datagram %(host)s %(port)r', 126 | logging.handlers.SysLogHandler: 'SysLog %(address)r facility=%(facility)r', 127 | logging.handlers.SMTPHandler: 'SMTP via %(mailhost)s to %(toaddrs)s', 128 | logging.handlers.HTTPHandler: 'HTTP %(method)s to http://%(host)s/%(url)s', 129 | logging.handlers.BufferingHandler: 'Buffering capacity=%(capacity)r', 130 | logging.handlers.MemoryHandler: 'Memory capacity=%(capacity)r', 131 | } 132 | 133 | if sys.version_info >= (2, 5): handler_formats.update({ 134 | logging.handlers.TimedRotatingFileHandler: 135 | 'TimedRotatingFile %(baseFilename)r when=%(when)r' 136 | ' interval=%(interval)r backupCount=%(backupCount)r', 137 | }) 138 | 139 | if sys.version_info >= (2, 6): handler_formats.update({ 140 | logging.handlers.WatchedFileHandler: 'WatchedFile %(baseFilename)r', 141 | }) 142 | 143 | 144 | def describe_handler(h): 145 | """Yield one or more lines describing the logging handler `h`.""" 146 | t = h.__class__ # using type() breaks in Python <= 2.6 147 | format = handler_formats.get(t) 148 | if format is not None: 149 | yield format % h.__dict__ 150 | else: 151 | yield repr(h) 152 | level = getattr(h, 'level', logging.NOTSET) 153 | if level != logging.NOTSET: 154 | yield ' Level ' + logging.getLevelName(level) 155 | for f in getattr(h, 'filters', ()): 156 | yield ' Filter %s' % describe_filter(f) 157 | formatter = getattr(h, 'formatter', None) 158 | if formatter is not None: 159 | if class_of(formatter) is logging.Formatter: 160 | yield ' Formatter fmt=%r datefmt=%r' % ( 161 | getattr(formatter, '_fmt', None), 162 | getattr(formatter, 'datefmt', None)) 163 | else: 164 | yield ' Formatter %r' % (formatter,) 165 | if t is logging.handlers.MemoryHandler and h.target is not None: 166 | yield ' Flushes output to:' 167 | g = describe_handler(h.target) 168 | yield ' Handler ' + next(g) 169 | for line in g: 170 | yield ' ' + line 171 | 172 | 173 | def class_of(obj): 174 | """Try to learn the class of `obj`. 175 | 176 | We perform the operation gingerly, as `obj` could be any kind of 177 | user-supplied object: an old-style class, a new-style class, or a 178 | built-in type that doesn't follow normal rules. 179 | 180 | """ 181 | cls = getattr(obj, '__class__', None) 182 | if cls is None: 183 | cls = type(obj) 184 | return cls 185 | -------------------------------------------------------------------------------- /logging_tree/__init__.py: -------------------------------------------------------------------------------- 1 | """Introspection for the ``logging`` logger tree in the Standard Library. 2 | 3 | You can install this package with the standard ``pip`` command:: 4 | 5 | $ pip install logging_tree 6 | 7 | The simplest way to use this package is to call ``printout()`` to see 8 | the loggers, filters, and handlers that your application has configured: 9 | 10 | >>> import logging 11 | >>> a = logging.getLogger('a') 12 | >>> b = logging.getLogger('a.b').setLevel(logging.DEBUG) 13 | >>> c = logging.getLogger('x.c') 14 | 15 | >>> import sys 16 | >>> h = logging.StreamHandler(sys.stdout) 17 | >>> logging.getLogger().addHandler(h) 18 | 19 | >>> from logging_tree import printout 20 | >>> printout() 21 | <--"" 22 | Level WARNING 23 | Handler Stream 24 | | 25 | o<--"a" 26 | | Level NOTSET so inherits level WARNING 27 | | | 28 | | o<--"a.b" 29 | | Level DEBUG 30 | | 31 | o<--[x] 32 | | 33 | o<--"x.c" 34 | Level NOTSET so inherits level WARNING 35 | 36 | If you instead want to write the tree diagram to a file, stream, or 37 | other file-like object, use:: 38 | 39 | file_object.write(logging_tree.format.build_description()) 40 | 41 | The logging tree should always print successfully, no matter how 42 | complicated. A node whose name is in square brackets, like the ``[x]`` 43 | node above, is a "place holder" that has never itself been named in a 44 | ``getLogger()`` call, but which was created automatically to serve as 45 | the parent of loggers further down the tree. 46 | 47 | Propagation 48 | ----------- 49 | 50 | A quick reminder about how ``logging`` works: by default, a node will 51 | not only submit a message to its own handlers (if any), but will also 52 | "propagate" each message up to its parent. For example, a ``Stream`` 53 | handler attached to the root logger will not only receive messages sent 54 | directly to the root, but also messages that propagate up from a child 55 | like ``a.b``. 56 | 57 | >>> logging.getLogger().warning('message sent to the root') 58 | message sent to the root 59 | >>> logging.getLogger('a.b').warning('message sent to a.b') 60 | message sent to a.b 61 | 62 | But messages are *not* subjected to filtering as they propagate. So a 63 | debug-level message, which our root node will discard because the root's 64 | level is set to ``WARNING``, will be accepted by the ``a.b`` node and 65 | will be allowed to propagate up to the root handler. 66 | 67 | >>> logging.getLogger().debug('this message is ignored') 68 | >>> logging.getLogger('a.b').debug('but this message prints!') 69 | but this message prints! 70 | 71 | If both the root node and ``a.b`` have a handler attached, then a 72 | message accepted by ``a.b`` will be printed twice, once by its own node, 73 | and then a second time when the message propagates up to the root. 74 | 75 | >>> logging.getLogger('a.b').addHandler(h) 76 | >>> logging.getLogger('a.b').warning('this message prints twice') 77 | this message prints twice 78 | this message prints twice 79 | 80 | But you can stop a node from propagating messages to its parent by 81 | setting its ``propagate`` attribute to ``False``. 82 | 83 | >>> logging.getLogger('a.b').propagate = False 84 | >>> logging.getLogger('a.b').warning('does not propagate') 85 | does not propagate 86 | 87 | The logging tree will indicate that propagate is turned off by no longer 88 | drawing the arrow ``<--`` that points from the node to its parent: 89 | 90 | >>> printout() 91 | <--"" 92 | Level WARNING 93 | Handler Stream 94 | | 95 | o<--"a" 96 | | Level NOTSET so inherits level WARNING 97 | | | 98 | | o "a.b" 99 | | Level DEBUG 100 | | Propagate OFF 101 | | Handler Stream 102 | | 103 | o<--[x] 104 | | 105 | o<--"x.c" 106 | Level NOTSET so inherits level WARNING 107 | 108 | You can turn propagate back on again by setting the attribute ``True``. 109 | 110 | API 111 | --- 112 | 113 | Even though most users will simply call the top-level ``printout()`` 114 | routine, this package also offers a few lower-level calls. Here's the 115 | complete list: 116 | 117 | ``logging_tree.printout(node=None)`` 118 | 119 | Prints the current logging tree, or the tree based at the given 120 | `node`, to the standard output. 121 | 122 | ``logging_tree.format.build_description(node=None)`` 123 | 124 | Builds and returns the multi-line description of the current logger 125 | tree, or the tree based at the given ``node``, as a single string 126 | with newlines inside and a newline at the end. 127 | 128 | ``logging_tree.format.describe(node)`` 129 | 130 | A generator that yields a series of lines that describe the tree 131 | based at the given ``node``. Note that the lines are returned 132 | without newline terminators attached. 133 | 134 | ``logging_tree.tree()`` 135 | 136 | Fetch the current tree of loggers from the ``logging`` module. 137 | Returns a node, that is simply a tuple with three fields: 138 | 139 | | ``[0]`` the logger name (``""`` for the root logger). 140 | | ``[1]`` the ``logging.Logger`` object itself. 141 | | ``[2]`` a list of zero or more child nodes. 142 | 143 | You can find this package's issue tracker `on GitHub 144 | `_. You can run this 145 | package's test suite with:: 146 | 147 | $ python -m unittest discover logging_tree 148 | 149 | On older versions of Python you will instead have to install 150 | ``unittest2`` and use its ``unit2`` command line tool to run the tests. 151 | 152 | Changelog 153 | --------- 154 | 155 | **Version 1.10** - 2024 May 3 156 | Declare compatibility with Python 3.12, and expand the documentation 157 | to describe the basics of log message propagation. 158 | 159 | **Version 1.9** - 2021 April 10 160 | Declare compatibility with Python 3.9. Improve how the logging 161 | module's built-in ``Formatter`` class is displayed under old Python 162 | versions where the ``logging`` module uses old-style classes. 163 | 164 | **Version 1.8.1** - 2020 January 26 165 | Adjust one test to make it pass under Python 3.8, and update the 166 | distribution classifiers to declare compatibility with Python 167 | versions through 3.8. 168 | 169 | **Version 1.8** - 2018 August 5 170 | Improve the output to better explain what happens if a "parent" 171 | attribute has been set to None. 172 | 173 | **Version 1.7** - 2016 January 23 174 | Detect whether each logger has the correct "parent" attribute and, 175 | if not, print where its log messages are being sent instead. 176 | 177 | **Version 1.6** - 2015 January 8 178 | Fixed a crash that would occur if a custom logging Formatter was 179 | missing its format string attributes. 180 | 181 | **Version 1.5** - 2014 December 24 182 | Handlers now display their logging level if one has been set, and 183 | their custom logging formatter if one has been installed. 184 | 185 | **Version 1.4** - 2014 January 8 186 | Thanks to a contribution from Dave Brondsema, disabled loggers are 187 | now actually marked as "Disabled" to make it less of a surprise that 188 | they fail to log anything. 189 | 190 | **Version 1.3** - 2013 October 29 191 | Be explicit and display the logger level ``NOTSET`` along with the 192 | effective level inherited from the logger's ancestors; and display 193 | the list of ``.filters`` of a custom logging handler even though it 194 | might contain custom code that ignores them. 195 | 196 | **Version 1.2** - 2013 January 19 197 | Compatible with Python 3.3 thanks to @ralphbean. 198 | 199 | **Version 1.1** - 2012 February 17 200 | Now compatible with 2.3 <= Python <= 3.2. 201 | 202 | **Version 1.0** - 2012 February 13 203 | Can display the handler inside a MemoryHandler; entire public 204 | interface documented; 100% test coverage. 205 | 206 | **Version 0.6** - 2012 February 10 207 | Added a display format for every ``logging.handlers`` class. 208 | 209 | **Version 0.5** - 2012 February 8 210 | Initial release. 211 | 212 | """ 213 | __version__ = '1.10' 214 | __all__ = ('tree', 'printout') 215 | 216 | from logging_tree.nodes import tree 217 | from logging_tree.format import printout 218 | -------------------------------------------------------------------------------- /logging_tree/tests/test_format.py: -------------------------------------------------------------------------------- 1 | """Tests for the `logging_tree.format` module.""" 2 | 3 | import doctest 4 | import logging 5 | import logging.handlers 6 | import unittest 7 | import sys 8 | 9 | import logging_tree 10 | from logging_tree.format import build_description, printout 11 | from logging_tree.tests.case import LoggingTestCase 12 | if sys.version_info >= (3,): 13 | from io import StringIO 14 | else: 15 | from StringIO import StringIO 16 | 17 | 18 | class FakeFile(StringIO): 19 | def __init__(self, filename, *args, **kwargs): 20 | self.filename = filename 21 | StringIO.__init__(self) 22 | 23 | def __repr__(self): 24 | return '' % self.filename 25 | 26 | def fileno(self): 27 | return 0 28 | 29 | 30 | class FormatTests(LoggingTestCase): 31 | 32 | maxDiff = 9999 33 | 34 | def setUp(self): 35 | # Prevent logging file handlers from trying to open real files. 36 | # (The keyword delay=1, which defers any actual attempt to open 37 | # a file, did not appear until Python 2.6.) 38 | logging.open = FakeFile 39 | super(FormatTests, self).setUp() 40 | 41 | def tearDown(self): 42 | del logging.open 43 | super(FormatTests, self).tearDown() 44 | 45 | def test_printout(self): 46 | stdout, sys.stdout = sys.stdout, StringIO() 47 | printout() 48 | self.assertEqual(sys.stdout.getvalue(), '<--""\n Level WARNING\n') 49 | sys.stdout = stdout 50 | 51 | def test_simple_tree(self): 52 | logging.getLogger('a') 53 | logging.getLogger('a.b').setLevel(logging.DEBUG) 54 | logging.getLogger('x.c') 55 | self.assertEqual(build_description(), '''\ 56 | <--"" 57 | Level WARNING 58 | | 59 | o<--"a" 60 | | Level NOTSET so inherits level WARNING 61 | | | 62 | | o<--"a.b" 63 | | Level DEBUG 64 | | 65 | o<--[x] 66 | | 67 | o<--"x.c" 68 | Level NOTSET so inherits level WARNING 69 | ''') 70 | 71 | def test_fancy_tree(self): 72 | logging.getLogger('').setLevel(logging.DEBUG) 73 | 74 | log = logging.getLogger('db') 75 | log.setLevel(logging.INFO) 76 | log.propagate = False 77 | log.disabled = 1 78 | log.addFilter(MyFilter()) 79 | 80 | handler = logging.StreamHandler() 81 | handler.setFormatter(logging.Formatter()) 82 | log.addHandler(handler) 83 | handler.addFilter(logging.Filter('db.errors')) 84 | 85 | logging.getLogger('db.errors') 86 | logging.getLogger('db.stats') 87 | 88 | log = logging.getLogger('www.status') 89 | log.setLevel(logging.DEBUG) 90 | log.addHandler(logging.FileHandler('/foo/log.txt')) 91 | log.addHandler(MyHandler()) 92 | 93 | self.assertEqual(build_description(), '''\ 94 | <--"" 95 | Level DEBUG 96 | | 97 | o "db" 98 | | Level INFO 99 | | Propagate OFF 100 | | Disabled 101 | | Filter 102 | | Handler Stream %r 103 | | Filter name='db.errors' 104 | | Formatter fmt='%%(message)s' datefmt=None 105 | | | 106 | | o<--"db.errors" 107 | | | Level NOTSET so inherits level INFO 108 | | | 109 | | o<--"db.stats" 110 | | Level NOTSET so inherits level INFO 111 | | 112 | o<--[www] 113 | | 114 | o<--"www.status" 115 | Level DEBUG 116 | Handler File '/foo/log.txt' 117 | Handler 118 | ''' % (sys.stderr,)) 119 | 120 | def test_most_handlers(self): 121 | ah = logging.getLogger('').addHandler 122 | ah(logging.handlers.RotatingFileHandler( 123 | '/bar/one.txt', maxBytes=10000, backupCount=3)) 124 | ah(logging.handlers.SocketHandler('server.example.com', 514)) 125 | ah(logging.handlers.DatagramHandler('server.example.com', 1958)) 126 | ah(logging.handlers.SysLogHandler()) 127 | ah(logging.handlers.SMTPHandler( 128 | 'mail.example.com', 'Server', 'Sysadmin', 'Logs!')) 129 | # ah(logging.handlers.NTEventLogHandler()) 130 | ah(logging.handlers.HTTPHandler('api.example.com', '/logs', 'POST')) 131 | ah(logging.handlers.BufferingHandler(20000)) 132 | sh = logging.StreamHandler() 133 | ah(logging.handlers.MemoryHandler(30000, target=sh)) 134 | self.assertEqual(build_description(), '''\ 135 | <--"" 136 | Level WARNING 137 | Handler RotatingFile '/bar/one.txt' maxBytes=10000 backupCount=3 138 | Handler Socket server.example.com 514 139 | Handler Datagram server.example.com 1958 140 | Handler SysLog ('localhost', 514) facility=1 141 | Handler SMTP via mail.example.com to ['Sysadmin'] 142 | Handler HTTP POST to http://api.example.com//logs 143 | Handler Buffering capacity=20000 144 | Handler Memory capacity=30000 145 | Flushes output to: 146 | Handler Stream %r 147 | ''' % (sh.stream,)) 148 | logging.getLogger('').handlers[3].socket.close() # or Python 3 warning 149 | 150 | def test_2_dot_5_handlers(self): 151 | if sys.version_info < (2, 5): 152 | return 153 | ah = logging.getLogger('').addHandler 154 | ah(logging.handlers.TimedRotatingFileHandler('/bar/two.txt')) 155 | expected = '''\ 156 | <--"" 157 | Level WARNING 158 | Handler TimedRotatingFile '/bar/two.txt' when='H' interval=3600 backupCount=0 159 | ''' 160 | self.assertEqual(build_description(), expected) 161 | 162 | def test_2_dot_6_handlers(self): 163 | if sys.version_info < (2, 6): 164 | return 165 | ah = logging.getLogger('').addHandler 166 | ah(logging.handlers.WatchedFileHandler('/bar/three.txt')) 167 | self.assertEqual(build_description(), '''\ 168 | <--"" 169 | Level WARNING 170 | Handler WatchedFile '/bar/three.txt' 171 | ''') 172 | 173 | def test_nested_handlers(self): 174 | h1 = logging.StreamHandler() 175 | 176 | h2 = logging.handlers.MemoryHandler(30000, target=h1) 177 | h2.addFilter(logging.Filter('worse')) 178 | h2.setLevel(logging.ERROR) 179 | 180 | h3 = logging.handlers.MemoryHandler(30000, target=h2) 181 | h3.addFilter(logging.Filter('bad')) 182 | 183 | logging.getLogger('').addHandler(h3) 184 | 185 | self.assertEqual(build_description(), '''\ 186 | <--"" 187 | Level WARNING 188 | Handler Memory capacity=30000 189 | Filter name='bad' 190 | Flushes output to: 191 | Handler Memory capacity=30000 192 | Level ERROR 193 | Filter name='worse' 194 | Flushes output to: 195 | Handler Stream %r 196 | ''' % (h1.stream,)) 197 | 198 | def test_formatter_with_no_fmt_attributes(self): 199 | f = logging.Formatter() 200 | del f._fmt 201 | del f.datefmt 202 | h = logging.StreamHandler() 203 | h.setFormatter(f) 204 | logging.getLogger('').addHandler(h) 205 | 206 | self.assertEqual(build_description(), '''\ 207 | <--"" 208 | Level WARNING 209 | Handler Stream %r 210 | Formatter fmt=None datefmt=None 211 | ''' % (h.stream,)) 212 | 213 | def test_formatter_that_is_not_a_Formatter_instance(self): 214 | h = logging.StreamHandler() 215 | h.setFormatter("Ceci n'est pas une formatter") 216 | logging.getLogger('').addHandler(h) 217 | 218 | self.assertEqual(build_description(), '''\ 219 | <--"" 220 | Level WARNING 221 | Handler Stream %r 222 | Formatter "Ceci n'est pas une formatter" 223 | ''' % (h.stream,)) 224 | 225 | def test_handler_with_wrong_parent_attribute(self): 226 | logging.getLogger('celery') 227 | logging.getLogger('app.model') 228 | logging.getLogger('app.task').parent = logging.getLogger('celery.task') 229 | logging.getLogger('app.view') 230 | 231 | self.assertEqual(build_description(), '''\ 232 | <--"" 233 | Level WARNING 234 | | 235 | o<--[app] 236 | | | 237 | | o<--"app.model" 238 | | | Level NOTSET so inherits level WARNING 239 | | | 240 | | o !-"app.task" 241 | | | Broken .parent redirects messages to 'celery.task' instead 242 | | | Level NOTSET so inherits level WARNING 243 | | | 244 | | o<--"app.view" 245 | | Level NOTSET so inherits level WARNING 246 | | 247 | o<--"celery" 248 | Level NOTSET so inherits level WARNING 249 | | 250 | o<--"celery.task" 251 | Level NOTSET so inherits level WARNING 252 | ''') 253 | 254 | def test_handler_with_parent_attribute_that_is_none(self): 255 | logging.getLogger('app').parent = None 256 | 257 | self.assertEqual(build_description(), '''\ 258 | <--"" 259 | Level WARNING 260 | | 261 | o !-"app" 262 | Broken .parent is None, so messages stop here 263 | Level NOTSET so inherits level NOTSET 264 | ''') 265 | 266 | 267 | class MyFilter(object): 268 | def __repr__(self): 269 | return '' 270 | 271 | 272 | class MyHandler(object): 273 | def __repr__(self): 274 | return '' 275 | 276 | 277 | def _spoofout_repr(self): 278 | """Improve how doctest's fake stdout looks in our package's doctest.""" 279 | return '' 280 | 281 | doctest._SpoofOut.__repr__ = _spoofout_repr 282 | 283 | def load_tests(loader, suite, ignore): 284 | suite.addTests(doctest.DocTestSuite(logging_tree)) 285 | return suite 286 | 287 | if __name__ == '__main__': # for Python <= 2.4 288 | unittest.main() 289 | --------------------------------------------------------------------------------