diff --git a/.travis.yml b/.travis.yml index aac4ef5..5f95cf2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,8 @@ python: - pypy - 3.4 before_install: - - pip install codecov + - pip install nose coverage codecov install: - - pip install -r test-requirements.txt - pip install -e . script: nosetests after_success: diff --git a/MANIFEST.in b/MANIFEST.in index 068dca0..2c38b05 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,6 @@ include LICENSE include README.rst include HISTORY.rst -include *requirements.txt include tests.py graft docs global-exclude __pycache__ diff --git a/README.rst b/README.rst index 074b12b..589d5be 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ sprockets.logging ================= Making logs nicer since 2015! -|Version| |Downloads| |Status| |Coverage| |License| +|Version| |Downloads| |Travis| |CodeCov| |ReadTheDocs| Installation ------------ @@ -24,7 +24,7 @@ Requirements Example ------- -This examples demonstrates how to use ``sprockets.logging`` by ... +This examples demonstrates the most basic usage of ``sprockets.logging`` .. code-block:: python @@ -33,13 +33,15 @@ This examples demonstrates how to use ``sprockets.logging`` by ... import sprockets.logging + formatter = logging.Formatter('%(levelname)s %(message)s {%(context)s}') handler = logging.StreamHandler(sys.stdout) handler.setFormatter(formatter) handler.addFilter(sprockets.logging.ContextFilter(properties=['context'])) logging.Logger.root.addHandler(handler) + logging.Logger.root.setLevel(logging.DEBUG) - # Outputs: INFO Hi there {} + # Outputs: INFO Hi there {None} logging.info('Hi there') # Outputs: INFO No KeyError {bah} @@ -47,7 +49,7 @@ This examples demonstrates how to use ``sprockets.logging`` by ... # Outputs: INFO Now with context! {foo} adapted = logging.LoggerAdapter(logging.Logger.root, extra={'context': 'foo'}) - adapter.info('Now with context!') + adapted.info('Now with context!') Source ------ @@ -61,14 +63,14 @@ License .. |Version| image:: https://badge.fury.io/py/sprockets.logging.svg? :target: http://badge.fury.io/py/sprockets.logging -.. |Status| image:: https://travis-ci.org/sprockets/sprockets.logging.svg?branch=master +.. |Travis| image:: https://travis-ci.org/sprockets/sprockets.logging.svg?branch=master :target: https://travis-ci.org/sprockets/sprockets.logging -.. |Coverage| image:: http://codecov.io/github/sprockets/sprockets.logging/coverage.svg?branch=master +.. |CodeCov| image:: http://codecov.io/github/sprockets/sprockets.logging/coverage.svg?branch=master :target: https://codecov.io/github/sprockets/sprockets.logging?branch=master .. |Downloads| image:: https://pypip.in/d/sprockets.logging/badge.svg? :target: https://pypi.python.org/pypi/sprockets.logging -.. |License| image:: https://pypip.in/license/sprockets.logging/badge.svg? +.. |ReadTheDocs| image:: https://readthedocs.org/projects/sprocketslogging/badge/ :target: https://sprocketslogging.readthedocs.org diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index 6a259fc..0000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ --r requirements.txt --r test-requirements.txt -flake8>=2.1,<3 -sphinx>=1.2,<2 -sphinx-rtd-theme>=0.1,<1.0 -sphinxcontrib-httpdomain>=1.2,<2 diff --git a/docs/examples.rst b/docs/examples.rst index 45f5d6c..db435cb 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -3,29 +3,17 @@ Examples Simple Usage ------------ -The following snippet uses :class:`sprockets.logging.filters.ContextFilter` +The following snippet uses :class:`sprockets.logging.ContextFilter` to insert context information into a message using a :class:`logging.LoggerAdapter` instance. -.. code-block:: python +.. literalinclude:: ../examples/simple.py - import logging - import sys +Dictionary-based Configuration +------------------------------ +This package begins to shine if you use the dictionary-based logging +configuration offered by :func:`logging.config.dictConfig`. You can insert +the custom filter and format string into the logging infrastructure and +insert context easily with :class:`logging.LoggerAdapter`. - import sprockets.logging - - formatter = logging.Formatter('%(levelname)s %(message)s {%(context)s}') - handler = logging.StreamHandler(sys.stdout) - handler.setFormatter(formatter) - handler.addFilter(sprockets.logging.ContextFilter(properties=['context'])) - logging.Logger.root.addHandler(handler) - - # Outputs: INFO Hi there {} - logging.info('Hi there') - - # Outputs: INFO No KeyError {bah} - logging.info('No KeyError', extra={'context': 'bah'}) - - # Outputs: INFO Now with context! {foo} - adapted = logging.LoggerAdapter(logging.Logger.root, extra={'context': 'foo'}) - adapter.info('Now with context!') +.. literalinclude:: ../examples/tornado-app.py diff --git a/docs/history.rst b/docs/history.rst index b91c00b..b0808d0 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -3,4 +3,4 @@ Version History Next Release ------------ - - implement greatness + - Added :class:`sprockets.logging.ContextFilter` diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..1a37ebe --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx-rtd-theme +sphinxcontrib-httpdomain diff --git a/examples/simple.py b/examples/simple.py new file mode 100644 index 0000000..a1ac45f --- /dev/null +++ b/examples/simple.py @@ -0,0 +1,22 @@ +import logging +import sys + +import sprockets.logging + + +formatter = logging.Formatter('%(levelname)s %(message)s {%(context)s}') +handler = logging.StreamHandler(sys.stdout) +handler.setFormatter(formatter) +handler.addFilter(sprockets.logging.ContextFilter(properties=['context'])) +logging.Logger.root.addHandler(handler) +logging.Logger.root.setLevel(logging.DEBUG) + +# Outputs: INFO Hi there {None} +logging.info('Hi there') + +# Outputs: INFO No KeyError {bah} +logging.info('No KeyError', extra={'context': 'bah'}) + +# Outputs: INFO Now with context! {foo} +adapted = logging.LoggerAdapter(logging.Logger.root, extra={'context': 'foo'}) +adapted.info('Now with context!') diff --git a/examples/tornado-app.py b/examples/tornado-app.py new file mode 100644 index 0000000..d88a852 --- /dev/null +++ b/examples/tornado-app.py @@ -0,0 +1,77 @@ +import logging.config +import signal +import uuid + +from tornado import ioloop, web +import sprockets.logging + + +LOG_CONFIG = { + 'version': 1, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'stream': 'ext://sys.stdout', + 'formatter': 'simple', + 'filters': ['context'], + }, + }, + 'formatters': { + 'simple': { + 'class': 'logging.Formatter', + 'format': '%(levelname)s %(name)s: %(message)s [%(context)s]', + }, + }, + 'filters': { + 'context': { + '()': 'sprockets.logging.ContextFilter', + 'properties': ['context'], + }, + }, + 'loggers': { + 'tornado': { + 'level': 'DEBUG', + }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'DEBUG', + }, + 'incremental': False, +} + + +class RequestHandler(web.RequestHandler): + + def __init__(self, *args, **kwargs): + self.parent_log = kwargs.pop('parent_log') + super(RequestHandler, self).__init__(*args, **kwargs) + + def prepare(self): + uniq_id = self.request.headers.get('X-UniqID', uuid.uuid4().hex) + self.logger = logging.LoggerAdapter( + self.parent_log.getChild('RequestHandler'), + extra={'context': uniq_id}) + + def get(self, object_id): + self.logger.debug('fetchin %s', object_id) + self.set_status(200) + return self.finish() + +def sig_handler(signo, frame): + logging.info('caught signal %d, stopping IO loop', signo) + iol = ioloop.IOLoop.instance() + iol.add_callback_from_signal(iol.stop) + +if __name__ == '__main__': + logging.config.dictConfig(LOG_CONFIG) + logger = logging.getLogger('app') + app = web.Application([ + web.url('/(?P\w+)', RequestHandler, + kwargs={'parent_log': logger}), + ]) + app.listen(8000) + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + ioloop.IOLoop.instance().start() + logger.info('IO loop stopped, exiting') diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e69de29..0000000 diff --git a/setup.py b/setup.py index 6f8ace2..93fe0ca 100755 --- a/setup.py +++ b/setup.py @@ -7,23 +7,9 @@ import setuptools import sprockets.logging -def read_requirements_file(req_name): - requirements = [] - try: - with codecs.open(req_name, encoding='utf-8') as req_file: - for req_line in req_file: - if '#' in req_line: - req_line = req_line[0:req_line.find('#')].strip() - if req_line: - requirements.append(req_line.strip()) - except IOError: - pass - return requirements - - -install_requires = read_requirements_file('requirements.txt') -setup_requires = read_requirements_file('setup-requirements.txt') -tests_require = read_requirements_file('test-requirements.txt') +install_requires = [] +setup_requires = [] +tests_require = ['nose>=1.3,<2'] if sys.version_info < (3, 0): tests_require.append('mock') diff --git a/sprockets/logging.py b/sprockets/logging.py index e9a44e8..0387bf1 100644 --- a/sprockets/logging.py +++ b/sprockets/logging.py @@ -1,5 +1,39 @@ """ Make good log output easier. + +- :class:`ContextFilter` adds fixed properties to a log record + """ +from __future__ import absolute_import + +import logging + + version_info = (0, 0, 0) __version__ = '.'.join(str(v) for v in version_info) + + +class ContextFilter(logging.Filter): + """ + Ensures that properties exist on a LogRecord. + + :param list|None properties: optional list of properties that + will be added to LogRecord instances if they are missing + + This filter implementation will ensure that a set of properties + exists on every log record which means that you can always refer + to custom properties in a format string. Without this, referring + to a property that is not explicitly passed in will result in an + ugly ``KeyError`` exception. + + """ + + def __init__(self, name='', properties=None): + logging.Filter.__init__(self, name) + self.properties = list(properties) if properties else [] + + def filter(self, record): + for property_name in self.properties: + if not hasattr(record, property_name): + setattr(record, property_name, None) + return True diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 378a123..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -coverage>=3.7,<4 -nose>=1.3,<2 diff --git a/tests.py b/tests.py index e69de29..f3f36d3 100644 --- a/tests.py +++ b/tests.py @@ -0,0 +1,44 @@ +import logging +import uuid +import unittest + +import sprockets.logging + + +class Prototype(object): + pass + + +class RecordingHandler(logging.FileHandler): + def __init__(self): + logging.FileHandler.__init__(self, filename='/dev/null') + self.log_lines = [] + + def format(self, record): + log_line = logging.FileHandler.format(self, record) + self.log_lines.append(log_line) + return log_line + + +class ContextFilterTests(unittest.TestCase): + + def setUp(self): + super(ContextFilterTests, self).setUp() + self.logger = logging.getLogger(uuid.uuid4().hex) + self.handler = RecordingHandler() + self.logger.addHandler(self.handler) + + def test_that_filter_blocks_key_errors(self): + formatter = logging.Formatter('%(message)s [%(context)s]') + self.handler.setFormatter(formatter) + self.handler.addFilter(sprockets.logging.ContextFilter( + properties=['context'])) + self.logger.info('hi there') + + def test_that_filter_does_not_overwrite_extras(self): + formatter = logging.Formatter('%(message)s [%(context)s]') + self.handler.setFormatter(formatter) + self.handler.addFilter(sprockets.logging.ContextFilter( + properties=['context'])) + self.logger.info('hi there', extra={'context': 'foo'}) + self.assertEqual(self.handler.log_lines[-1], 'hi there [foo]') diff --git a/tox.ini b/tox.ini index 3f3471d..5b9e34b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,10 @@ [tox] envlist = py27,py34 indexserver = - default = https://pypi.python.org/simple + default = https://pypi.python.org/simple toxworkdir = build/tox +skip_missing_interpreters = true [testenv] commands = nosetests [] -deps = -rtest-requirements.txt +deps = nose