# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. """ Processors and tools specific to the `Twisted `_ networking engine. See also :doc:`structlog's Twisted support `. """ from __future__ import absolute_import, division, print_function import json import sys from six import PY2, string_types from twisted.python import log from twisted.python.failure import Failure from twisted.python.log import ILogObserver, textFromEventDict from zope.interface import implementer from ._base import BoundLoggerBase from ._config import _BUILTIN_DEFAULT_PROCESSORS from ._utils import until_not_interrupted from .processors import ( # can't import processors module without risking circular imports JSONRenderer as GenericJSONRenderer, ) class BoundLogger(BoundLoggerBase): """ Twisted-specific version of :class:`structlog.BoundLogger`. Works exactly like the generic one except that it takes advantage of knowing the logging methods in advance. Use it like:: configure( wrapper_class=structlog.twisted.BoundLogger, ) """ def msg(self, event=None, **kw): """ Process event and call ``log.msg()`` with the result. """ return self._proxy_to_logger("msg", event, **kw) def err(self, event=None, **kw): """ Process event and call ``log.err()`` with the result. """ return self._proxy_to_logger("err", event, **kw) class LoggerFactory(object): """ Build a Twisted logger when an *instance* is called. >>> from structlog import configure >>> from structlog.twisted import LoggerFactory >>> configure(logger_factory=LoggerFactory()) """ def __call__(self, *args): """ Positional arguments are silently ignored. :rvalue: A new Twisted logger. .. versionchanged:: 0.4.0 Added support for optional positional arguments. """ return log _FAIL_TYPES = (BaseException, Failure) def _extractStuffAndWhy(eventDict): """ Removes all possible *_why*s and *_stuff*s, analyzes exc_info and returns a tuple of `(_stuff, _why, eventDict)`. **Modifies** *eventDict*! """ _stuff = eventDict.pop('_stuff', None) _why = eventDict.pop('_why', None) event = eventDict.pop('event', None) if ( isinstance(_stuff, _FAIL_TYPES) and isinstance(event, _FAIL_TYPES) ): raise ValueError('Both _stuff and event contain an Exception/Failure.') # `log.err('event', _why='alsoEvent')` is ambiguous. if _why and isinstance(event, string_types): raise ValueError('Both `_why` and `event` supplied.') # Two failures are ambiguous too. if not isinstance(_stuff, _FAIL_TYPES) and isinstance(event, _FAIL_TYPES): _why = _why or 'error' _stuff = event if isinstance(event, string_types): _why = event if not _stuff and sys.exc_info() != (None, None, None): _stuff = Failure() # Either we used the error ourselves or the user supplied one for # formatting. Avoid log.err() to dump another traceback into the log. if isinstance(_stuff, BaseException): _stuff = Failure(_stuff) if PY2: sys.exc_clear() return _stuff, _why, eventDict class ReprWrapper(object): """ Wrap a string and return it as the __repr__. This is needed for log.err() that calls repr() on _stuff: >>> repr("foo") "'foo'" >>> repr(ReprWrapper("foo")) 'foo' Note the extra quotes in the unwrapped example. """ def __init__(self, string): self.string = string def __eq__(self, other): """ Check for equality, actually just for tests. """ return isinstance(other, self.__class__) \ and self.string == other.string def __repr__(self): return self.string class JSONRenderer(GenericJSONRenderer): """ Behaves like :class:`structlog.processors.JSONRenderer` except that it formats tracebacks and failures itself if called with `err()`. .. note:: This ultimately means that the messages get logged out using `msg()`, and *not* `err()` which renders failures in separate lines. Therefore it will break your tests that contain assertions using `flushLoggedErrors `_. *Not* an adapter like :class:`EventAdapter` but a real formatter. Nor does it require to be adapted using it. Use together with a :class:`JSONLogObserverWrapper`-wrapped Twisted logger like :func:`plainJSONStdOutLogger` for pure-JSON logs. """ def __call__(self, logger, name, eventDict): _stuff, _why, eventDict = _extractStuffAndWhy(eventDict) if name == 'err': eventDict['event'] = _why if isinstance(_stuff, Failure): eventDict['exception'] = _stuff.getTraceback(detail='verbose') _stuff.cleanFailure() else: eventDict['event'] = _why return ((ReprWrapper( GenericJSONRenderer.__call__(self, logger, name, eventDict) ),), {'_structlog': True}) @implementer(ILogObserver) class PlainFileLogObserver(object): """ Write only the the plain message without timestamps or anything else. Great to just print JSON to stdout where you catch it with something like runit. :param file file: File to print to. .. versionadded:: 0.2.0 """ def __init__(self, file): self._write = file.write self._flush = file.flush def __call__(self, eventDict): until_not_interrupted(self._write, textFromEventDict(eventDict) + '\n') until_not_interrupted(self._flush) @implementer(ILogObserver) class JSONLogObserverWrapper(object): """ Wrap a log *observer* and render non-:class:`JSONRenderer` entries to JSON. :param ILogObserver observer: Twisted log observer to wrap. For example :class:`PlainFileObserver` or Twisted's stock `FileLogObserver `_ .. versionadded:: 0.2.0 """ def __init__(self, observer): self._observer = observer def __call__(self, eventDict): if '_structlog' not in eventDict: eventDict['message'] = (json.dumps({ 'event': textFromEventDict(eventDict), 'system': eventDict.get('system'), }),) eventDict['_structlog'] = True return self._observer(eventDict) def plainJSONStdOutLogger(): """ Return a logger that writes only the message to stdout. Transforms non-:class:`~structlog.twisted.JSONRenderer` messages to JSON. Ideal for JSONifying log entries from Twisted plugins and libraries that are outside of your control:: $ twistd -n --logger structlog.twisted.plainJSONStdOutLogger web {"event": "Log opened.", "system": "-"} {"event": "twistd 13.1.0 (python 2.7.3) starting up.", "system": "-"} {"event": "reactor class: twisted...EPollReactor.", "system": "-"} {"event": "Site starting on 8080", "system": "-"} {"event": "Starting factory ", ...} ... Composes :class:`PlainFileLogObserver` and :class:`JSONLogObserverWrapper` to a usable logger. .. versionadded:: 0.2.0 """ return JSONLogObserverWrapper(PlainFileLogObserver(sys.stdout)) class EventAdapter(object): """ Adapt an ``event_dict`` to Twisted logging system. Particularly, make a wrapped `twisted.python.log.err `_ behave as expected. :param callable dictRenderer: Renderer that is used for the actual log message. Please note that structlog comes with a dedicated :class:`JSONRenderer`. **Must** be the last processor in the chain and requires a `dictRenderer` for the actual formatting as an constructor argument in order to be able to fully support the original behaviors of ``log.msg()`` and ``log.err()``. """ def __init__(self, dictRenderer=None): """ :param dictRenderer: A processor used to format the log message. """ self._dictRenderer = dictRenderer or _BUILTIN_DEFAULT_PROCESSORS[-1] def __call__(self, logger, name, eventDict): if name == 'err': # This aspires to handle the following cases correctly: # - log.err(failure, _why='event', **kw) # - log.err('event', **kw) # - log.err(_stuff=failure, _why='event', **kw) _stuff, _why, eventDict = _extractStuffAndWhy(eventDict) eventDict['event'] = _why return ((), { '_stuff': _stuff, '_why': self._dictRenderer(logger, name, eventDict), }) else: return self._dictRenderer(logger, name, eventDict)