Commit bb785fc1 authored by Jason Madden's avatar Jason Madden

LoggingLogAdapter proxies to the underlying logger. Fixes #663.

parent 0e0099a2
...@@ -15,6 +15,10 @@ ...@@ -15,6 +15,10 @@
leak (`details`_). Thanks to Jay Oster. leak (`details`_). Thanks to Jay Oster.
- Allow subclasses of ``WSGIHandler`` to handle invalid HTTP client - Allow subclasses of ``WSGIHandler`` to handle invalid HTTP client
requests. Reported by not-bob. requests. Reported by not-bob.
- ``WSGIServer`` more robustly supports ``Logger``-like parameters for
``log`` and ``error_log`` (as introduced in 1.1b1, this could cause
integration issues with gunicorn). Reported in :issue:`663` by Jay
Oster.
.. _details: https://mail.python.org/pipermail/cython-devel/2015-October/004571.html .. _details: https://mail.python.org/pipermail/cython-devel/2015-October/004571.html
......
...@@ -984,20 +984,27 @@ class LoggingLogAdapter(object): ...@@ -984,20 +984,27 @@ class LoggingLogAdapter(object):
a ``LoopExit``. a ``LoopExit``.
.. versionadded:: 1.1a3 .. versionadded:: 1.1a3
.. versionchanged:: 1.1b6
Attributes not present on this object are proxied to the underlying
logger instance. This permits using custom :class:`~logging.Logger`
subclasses (or indeed, even duck-typed objects).
""" """
# gevent avoids importing and using logging because importing it and # gevent avoids importing and using logging because importing it and
# creating loggers creates native locks unless monkey-patched. # creating loggers creates native locks unless monkey-patched.
__slots__ = ('_logger', '_level')
def __init__(self, logger, level=20): def __init__(self, logger, level=20):
""" """
Write information to the *logger* at the given *level* (default to INFO). Write information to the *logger* at the given *level* (default to INFO).
""" """
self.logger = logger self._logger = logger
self.level = level self._level = level
def write(self, msg): def write(self, msg):
self.logger.log(self.level, msg) self._logger.log(self._level, msg)
def flush(self): def flush(self):
"No-op; required to be a file-like object" "No-op; required to be a file-like object"
...@@ -1007,6 +1014,18 @@ class LoggingLogAdapter(object): ...@@ -1007,6 +1014,18 @@ class LoggingLogAdapter(object):
for line in lines: for line in lines:
self.write(line) self.write(line)
def __getattr__(self, name):
return getattr(self._logger, name)
def __setattr__(self, name, value):
if name not in LoggingLogAdapter.__slots__:
setattr(self._logger, name, value)
else:
object.__setattr__(self, name, value)
def __delattr__(self, name):
delattr(self._logger, name)
class WSGIServer(StreamServer): class WSGIServer(StreamServer):
""" """
...@@ -1014,24 +1033,26 @@ class WSGIServer(StreamServer): ...@@ -1014,24 +1033,26 @@ class WSGIServer(StreamServer):
:keyword log: If given, an object with a ``write`` method to which :keyword log: If given, an object with a ``write`` method to which
request (access) logs will be written. If not given, defaults to request (access) logs will be written. If not given, defaults
:obj:`sys.stderr`. You may pass ``None`` to disable request to :obj:`sys.stderr`. You may pass ``None`` to disable request
logging. You may use a wrapper, around e.g., :mod:`logging`, logging. You may use a wrapper, around e.g., :mod:`logging`,
to support objects that don't implement a ``write`` method. to support objects that don't implement a ``write`` method.
(If you pass a :class:`logging.Logger` instance, such a (If you pass a :class:`~logging.Logger` instance, or in
wrapper will automatically be created and it will be logged to general something that provides a ``log`` method but not a
at the :data:`logging.INFO` level.) ``write`` method, such a wrapper will automatically be created
and it will be logged to at the :data:`~logging.INFO` level.)
:keyword error_log: If given, a file-like object with ``write``, :keyword error_log: If given, a file-like object with ``write``,
``writelines`` and ``flush`` methods to which error logs will ``writelines`` and ``flush`` methods to which error logs will
be written. If not given, defaults to :obj:`sys.stderr`. You be written. If not given, defaults to :obj:`sys.stderr`. You
may pass ``None`` to disable error logging (not recommended). may pass ``None`` to disable error logging (not recommended).
You may use a wrapper, around e.g., :mod:`logging`, to support You may use a wrapper, around e.g., :mod:`logging`, to support
objects that don't implement the proper methods. (If you pass objects that don't implement the proper methods. This
a :class:`logging.Logger` instance, such a wrapper will parameter will become the value for ``wsgi.errors`` in the
automatically be created, and it will be logged to at the WSGI environment (if not already set). (As with *log*,
:data:`logging.ERROR` level.) This parameter will become the wrappers for :class:`~logging.Logger` instances and the like
value for ``wsgi.errors`` in the WSGI environment (if not already set). will be created automatically and logged to at the :data:`~logging.ERROR`
level.)
.. seealso:: .. seealso::
......
...@@ -1444,6 +1444,43 @@ class Test414(TestCase): ...@@ -1444,6 +1444,43 @@ class Test414(TestCase):
read_http(fd, code=414) read_http(fd, code=414)
class Test663(TestCase):
def init_logger(self):
# Something that gets wrapped in a LoggingLogAdapter
class Logger(object):
accessed = None
logged = None
thing = None
def log(self, level, msg):
self.logged = (level, msg)
def access(self, msg):
self.accessed = msg
def get_thing(self):
return self.thing
return Logger()
def test_proxy_methods_on_log(self):
# An object that looks like a logger gets wrapped
# with a proxy that
self.assertTrue(isinstance(self.server.log, pywsgi.LoggingLogAdapter))
self.server.log.access("access")
self.server.log.write("write")
self.assertEqual(self.server.log.accessed, "access")
self.assertEqual(self.server.log.logged, (20, "write"))
def test_set_attributes(self):
# Not defined by the wrapper, it goes to the logger
self.server.log.thing = 42
self.assertEqual(self.server.log.get_thing(), 42)
del self.server.log.thing
self.assertEqual(self.server.log.get_thing(), None)
del CommonTests del CommonTests
if __name__ == '__main__': if __name__ == '__main__':
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment