twisted.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. # This file is dual licensed under the terms of the Apache License, Version
  2. # 2.0, and the MIT License. See the LICENSE file in the root of this
  3. # repository for complete details.
  4. """
  5. Processors and tools specific to the `Twisted <https://twistedmatrix.com/>`_
  6. networking engine.
  7. See also :doc:`structlog's Twisted support <twisted>`.
  8. """
  9. from __future__ import absolute_import, division, print_function
  10. import json
  11. import sys
  12. from six import PY2, string_types
  13. from twisted.python import log
  14. from twisted.python.failure import Failure
  15. from twisted.python.log import ILogObserver, textFromEventDict
  16. from zope.interface import implementer
  17. from ._base import BoundLoggerBase
  18. from ._config import _BUILTIN_DEFAULT_PROCESSORS
  19. from ._utils import until_not_interrupted
  20. from .processors import (
  21. # can't import processors module without risking circular imports
  22. JSONRenderer as GenericJSONRenderer,
  23. )
  24. class BoundLogger(BoundLoggerBase):
  25. """
  26. Twisted-specific version of :class:`structlog.BoundLogger`.
  27. Works exactly like the generic one except that it takes advantage of
  28. knowing the logging methods in advance.
  29. Use it like::
  30. configure(
  31. wrapper_class=structlog.twisted.BoundLogger,
  32. )
  33. """
  34. def msg(self, event=None, **kw):
  35. """
  36. Process event and call ``log.msg()`` with the result.
  37. """
  38. return self._proxy_to_logger("msg", event, **kw)
  39. def err(self, event=None, **kw):
  40. """
  41. Process event and call ``log.err()`` with the result.
  42. """
  43. return self._proxy_to_logger("err", event, **kw)
  44. class LoggerFactory(object):
  45. """
  46. Build a Twisted logger when an *instance* is called.
  47. >>> from structlog import configure
  48. >>> from structlog.twisted import LoggerFactory
  49. >>> configure(logger_factory=LoggerFactory())
  50. """
  51. def __call__(self, *args):
  52. """
  53. Positional arguments are silently ignored.
  54. :rvalue: A new Twisted logger.
  55. .. versionchanged:: 0.4.0
  56. Added support for optional positional arguments.
  57. """
  58. return log
  59. _FAIL_TYPES = (BaseException, Failure)
  60. def _extractStuffAndWhy(eventDict):
  61. """
  62. Removes all possible *_why*s and *_stuff*s, analyzes exc_info and returns
  63. a tuple of `(_stuff, _why, eventDict)`.
  64. **Modifies** *eventDict*!
  65. """
  66. _stuff = eventDict.pop('_stuff', None)
  67. _why = eventDict.pop('_why', None)
  68. event = eventDict.pop('event', None)
  69. if (
  70. isinstance(_stuff, _FAIL_TYPES) and
  71. isinstance(event, _FAIL_TYPES)
  72. ):
  73. raise ValueError('Both _stuff and event contain an Exception/Failure.')
  74. # `log.err('event', _why='alsoEvent')` is ambiguous.
  75. if _why and isinstance(event, string_types):
  76. raise ValueError('Both `_why` and `event` supplied.')
  77. # Two failures are ambiguous too.
  78. if not isinstance(_stuff, _FAIL_TYPES) and isinstance(event, _FAIL_TYPES):
  79. _why = _why or 'error'
  80. _stuff = event
  81. if isinstance(event, string_types):
  82. _why = event
  83. if not _stuff and sys.exc_info() != (None, None, None):
  84. _stuff = Failure()
  85. # Either we used the error ourselves or the user supplied one for
  86. # formatting. Avoid log.err() to dump another traceback into the log.
  87. if isinstance(_stuff, BaseException):
  88. _stuff = Failure(_stuff)
  89. if PY2:
  90. sys.exc_clear()
  91. return _stuff, _why, eventDict
  92. class ReprWrapper(object):
  93. """
  94. Wrap a string and return it as the __repr__.
  95. This is needed for log.err() that calls repr() on _stuff:
  96. >>> repr("foo")
  97. "'foo'"
  98. >>> repr(ReprWrapper("foo"))
  99. 'foo'
  100. Note the extra quotes in the unwrapped example.
  101. """
  102. def __init__(self, string):
  103. self.string = string
  104. def __eq__(self, other):
  105. """
  106. Check for equality, actually just for tests.
  107. """
  108. return isinstance(other, self.__class__) \
  109. and self.string == other.string
  110. def __repr__(self):
  111. return self.string
  112. class JSONRenderer(GenericJSONRenderer):
  113. """
  114. Behaves like :class:`structlog.processors.JSONRenderer` except that it
  115. formats tracebacks and failures itself if called with `err()`.
  116. .. note::
  117. This ultimately means that the messages get logged out using `msg()`,
  118. and *not* `err()` which renders failures in separate lines.
  119. Therefore it will break your tests that contain assertions using
  120. `flushLoggedErrors <https://twistedmatrix.com/documents/
  121. current/api/twisted.trial.unittest.SynchronousTestCase.html
  122. #flushLoggedErrors>`_.
  123. *Not* an adapter like :class:`EventAdapter` but a real formatter. Nor does
  124. it require to be adapted using it.
  125. Use together with a :class:`JSONLogObserverWrapper`-wrapped Twisted logger
  126. like :func:`plainJSONStdOutLogger` for pure-JSON logs.
  127. """
  128. def __call__(self, logger, name, eventDict):
  129. _stuff, _why, eventDict = _extractStuffAndWhy(eventDict)
  130. if name == 'err':
  131. eventDict['event'] = _why
  132. if isinstance(_stuff, Failure):
  133. eventDict['exception'] = _stuff.getTraceback(detail='verbose')
  134. _stuff.cleanFailure()
  135. else:
  136. eventDict['event'] = _why
  137. return ((ReprWrapper(
  138. GenericJSONRenderer.__call__(self, logger, name, eventDict)
  139. ),), {'_structlog': True})
  140. @implementer(ILogObserver)
  141. class PlainFileLogObserver(object):
  142. """
  143. Write only the the plain message without timestamps or anything else.
  144. Great to just print JSON to stdout where you catch it with something like
  145. runit.
  146. :param file file: File to print to.
  147. .. versionadded:: 0.2.0
  148. """
  149. def __init__(self, file):
  150. self._write = file.write
  151. self._flush = file.flush
  152. def __call__(self, eventDict):
  153. until_not_interrupted(self._write, textFromEventDict(eventDict) + '\n')
  154. until_not_interrupted(self._flush)
  155. @implementer(ILogObserver)
  156. class JSONLogObserverWrapper(object):
  157. """
  158. Wrap a log *observer* and render non-:class:`JSONRenderer` entries to JSON.
  159. :param ILogObserver observer: Twisted log observer to wrap. For example
  160. :class:`PlainFileObserver` or Twisted's stock `FileLogObserver
  161. <https://twistedmatrix.com/documents/current/api/twisted.python.log.
  162. FileLogObserver.html>`_
  163. .. versionadded:: 0.2.0
  164. """
  165. def __init__(self, observer):
  166. self._observer = observer
  167. def __call__(self, eventDict):
  168. if '_structlog' not in eventDict:
  169. eventDict['message'] = (json.dumps({
  170. 'event': textFromEventDict(eventDict),
  171. 'system': eventDict.get('system'),
  172. }),)
  173. eventDict['_structlog'] = True
  174. return self._observer(eventDict)
  175. def plainJSONStdOutLogger():
  176. """
  177. Return a logger that writes only the message to stdout.
  178. Transforms non-:class:`~structlog.twisted.JSONRenderer` messages to JSON.
  179. Ideal for JSONifying log entries from Twisted plugins and libraries that
  180. are outside of your control::
  181. $ twistd -n --logger structlog.twisted.plainJSONStdOutLogger web
  182. {"event": "Log opened.", "system": "-"}
  183. {"event": "twistd 13.1.0 (python 2.7.3) starting up.", "system": "-"}
  184. {"event": "reactor class: twisted...EPollReactor.", "system": "-"}
  185. {"event": "Site starting on 8080", "system": "-"}
  186. {"event": "Starting factory <twisted.web.server.Site ...>", ...}
  187. ...
  188. Composes :class:`PlainFileLogObserver` and :class:`JSONLogObserverWrapper`
  189. to a usable logger.
  190. .. versionadded:: 0.2.0
  191. """
  192. return JSONLogObserverWrapper(PlainFileLogObserver(sys.stdout))
  193. class EventAdapter(object):
  194. """
  195. Adapt an ``event_dict`` to Twisted logging system.
  196. Particularly, make a wrapped `twisted.python.log.err
  197. <https://twistedmatrix.com/documents/current/
  198. api/twisted.python.log.html#err>`_ behave as expected.
  199. :param callable dictRenderer: Renderer that is used for the actual
  200. log message. Please note that structlog comes with a dedicated
  201. :class:`JSONRenderer`.
  202. **Must** be the last processor in the chain and requires a `dictRenderer`
  203. for the actual formatting as an constructor argument in order to be able to
  204. fully support the original behaviors of ``log.msg()`` and ``log.err()``.
  205. """
  206. def __init__(self, dictRenderer=None):
  207. """
  208. :param dictRenderer: A processor used to format the log message.
  209. """
  210. self._dictRenderer = dictRenderer or _BUILTIN_DEFAULT_PROCESSORS[-1]
  211. def __call__(self, logger, name, eventDict):
  212. if name == 'err':
  213. # This aspires to handle the following cases correctly:
  214. # - log.err(failure, _why='event', **kw)
  215. # - log.err('event', **kw)
  216. # - log.err(_stuff=failure, _why='event', **kw)
  217. _stuff, _why, eventDict = _extractStuffAndWhy(eventDict)
  218. eventDict['event'] = _why
  219. return ((), {
  220. '_stuff': _stuff,
  221. '_why': self._dictRenderer(logger, name, eventDict),
  222. })
  223. else:
  224. return self._dictRenderer(logger, name, eventDict)