Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/structlog/twisted.py: 8%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# SPDX-License-Identifier: MIT OR Apache-2.0
2# This file is dual licensed under the terms of the Apache License, Version
3# 2.0, and the MIT License. See the LICENSE file in the root of this
4# repository for complete details.
6"""
7Processors and tools specific to the `Twisted <https://twisted.org/>`_
8networking engine.
10See also :doc:`structlog's Twisted support <twisted>`.
11"""
13from __future__ import annotations
15import json
16import sys
18from collections.abc import Callable, Sequence
19from typing import Any, TextIO
21from twisted.python import log
22from twisted.python.failure import Failure
23from twisted.python.log import ILogObserver, textFromEventDict
24from zope.interface import implementer
26from ._base import BoundLoggerBase
27from ._config import _BUILTIN_DEFAULT_PROCESSORS
28from .processors import JSONRenderer as GenericJSONRenderer
29from .typing import EventDict, WrappedLogger
32class BoundLogger(BoundLoggerBase):
33 """
34 Twisted-specific version of `structlog.BoundLogger`.
36 Works exactly like the generic one except that it takes advantage of
37 knowing the logging methods in advance.
39 Use it like::
41 configure(
42 wrapper_class=structlog.twisted.BoundLogger,
43 )
45 """
47 def msg(self, event: str | None = None, **kw: Any) -> Any:
48 """
49 Process event and call ``log.msg()`` with the result.
50 """
51 return self._proxy_to_logger("msg", event, **kw)
53 def err(self, event: str | None = None, **kw: Any) -> Any:
54 """
55 Process event and call ``log.err()`` with the result.
56 """
57 return self._proxy_to_logger("err", event, **kw)
60class LoggerFactory:
61 """
62 Build a Twisted logger when an *instance* is called.
64 >>> from structlog import configure
65 >>> from structlog.twisted import LoggerFactory
66 >>> configure(logger_factory=LoggerFactory())
67 """
69 def __call__(self, *args: Any) -> WrappedLogger:
70 """
71 Positional arguments are silently ignored.
73 :rvalue: A new Twisted logger.
75 .. versionchanged:: 0.4.0
76 Added support for optional positional arguments.
77 """
78 return log
81_FAIL_TYPES = (BaseException, Failure)
84def _extractStuffAndWhy(eventDict: EventDict) -> tuple[Any, Any, EventDict]:
85 """
86 Removes all possible *_why*s and *_stuff*s, analyzes exc_info and returns
87 a tuple of ``(_stuff, _why, eventDict)``.
89 **Modifies** *eventDict*!
90 """
91 _stuff = eventDict.pop("_stuff", None)
92 _why = eventDict.pop("_why", None)
93 event = eventDict.pop("event", None)
95 if isinstance(_stuff, _FAIL_TYPES) and isinstance(event, _FAIL_TYPES):
96 raise ValueError("Both _stuff and event contain an Exception/Failure.")
98 # `log.err('event', _why='alsoEvent')` is ambiguous.
99 if _why and isinstance(event, str):
100 raise ValueError("Both `_why` and `event` supplied.")
102 # Two failures are ambiguous too.
103 if not isinstance(_stuff, _FAIL_TYPES) and isinstance(event, _FAIL_TYPES):
104 _why = _why or "error"
105 _stuff = event
107 if isinstance(event, str):
108 _why = event
110 if not _stuff and sys.exc_info() != (None, None, None):
111 _stuff = Failure() # type: ignore[no-untyped-call]
113 # Either we used the error ourselves or the user supplied one for
114 # formatting. Avoid log.err() to dump another traceback into the log.
115 if isinstance(_stuff, BaseException) and not isinstance(_stuff, Failure):
116 _stuff = Failure(_stuff) # type: ignore[no-untyped-call]
118 return _stuff, _why, eventDict
121class ReprWrapper:
122 """
123 Wrap a string and return it as the ``__repr__``.
125 This is needed for ``twisted.python.log.err`` that calls `repr` on
126 ``_stuff``:
128 >>> repr("foo")
129 "'foo'"
130 >>> repr(ReprWrapper("foo"))
131 'foo'
133 Note the extra quotes in the unwrapped example.
134 """
136 def __init__(self, string: str) -> None:
137 self.string = string
139 def __eq__(self, other: object) -> bool:
140 """
141 Check for equality, just for tests.
142 """
143 return (
144 isinstance(other, self.__class__) and self.string == other.string
145 )
147 def __repr__(self) -> str:
148 return self.string
151class JSONRenderer(GenericJSONRenderer):
152 """
153 Behaves like `structlog.processors.JSONRenderer` except that it formats
154 tracebacks and failures itself if called with ``err()``.
156 .. note::
158 This ultimately means that the messages get logged out using ``msg()``,
159 and *not* ``err()`` which renders failures in separate lines.
161 Therefore it will break your tests that contain assertions using
162 `flushLoggedErrors
163 <https://docs.twisted.org/en/stable/api/
164 twisted.trial.unittest.SynchronousTestCase.html#flushLoggedErrors>`_.
166 *Not* an adapter like `EventAdapter` but a real formatter. Also does *not*
167 require to be adapted using it.
169 Use together with a `JSONLogObserverWrapper`-wrapped Twisted logger like
170 `plainJSONStdOutLogger` for pure-JSON logs.
171 """
173 def __call__( # type: ignore[override]
174 self,
175 logger: WrappedLogger,
176 name: str,
177 eventDict: EventDict,
178 ) -> tuple[Sequence[Any], dict[str, Any]]:
179 _stuff, _why, eventDict = _extractStuffAndWhy(eventDict)
180 if name == "err":
181 eventDict["event"] = _why
182 if isinstance(_stuff, Failure):
183 eventDict["exception"] = _stuff.getTraceback(detail="verbose")
184 _stuff.cleanFailure() # type: ignore[no-untyped-call]
185 else:
186 eventDict["event"] = _why
187 return (
188 (
189 ReprWrapper(
190 GenericJSONRenderer.__call__( # type: ignore[arg-type]
191 self, logger, name, eventDict
192 )
193 ),
194 ),
195 {"_structlog": True},
196 )
199@implementer(ILogObserver)
200class PlainFileLogObserver:
201 """
202 Write only the plain message without timestamps or anything else.
204 Great to just print JSON to stdout where you catch it with something like
205 runit.
207 Args:
208 file: File to print to.
210 .. versionadded:: 0.2.0
211 """
213 def __init__(self, file: TextIO) -> None:
214 self._write = file.write
215 self._flush = file.flush
217 def __call__(self, eventDict: EventDict) -> None:
218 self._write(
219 textFromEventDict(eventDict) # type: ignore[arg-type, operator]
220 + "\n",
221 )
222 self._flush()
225@implementer(ILogObserver)
226class JSONLogObserverWrapper:
227 """
228 Wrap a log *observer* and render non-`JSONRenderer` entries to JSON.
230 Args:
231 observer (ILogObserver):
232 Twisted log observer to wrap. For example
233 :class:`PlainFileObserver` or Twisted's stock `FileLogObserver
234 <https://docs.twisted.org/en/stable/api/
235 twisted.python.log.FileLogObserver.html>`_
237 .. versionadded:: 0.2.0
238 """
240 def __init__(self, observer: Any) -> None:
241 self._observer = observer
243 def __call__(self, eventDict: EventDict) -> str:
244 if "_structlog" not in eventDict:
245 eventDict["message"] = (
246 json.dumps(
247 {
248 "event": textFromEventDict(
249 eventDict # type: ignore[arg-type]
250 ),
251 "system": eventDict.get("system"),
252 }
253 ),
254 )
255 eventDict["_structlog"] = True
257 return self._observer(eventDict)
260def plainJSONStdOutLogger() -> JSONLogObserverWrapper:
261 """
262 Return a logger that writes only the message to stdout.
264 Transforms non-`JSONRenderer` messages to JSON.
266 Ideal for JSONifying log entries from Twisted plugins and libraries that
267 are outside of your control::
269 $ twistd -n --logger structlog.twisted.plainJSONStdOutLogger web
270 {"event": "Log opened.", "system": "-"}
271 {"event": "twistd 13.1.0 (python 2.7.3) starting up.", "system": "-"}
272 {"event": "reactor class: twisted...EPollReactor.", "system": "-"}
273 {"event": "Site starting on 8080", "system": "-"}
274 {"event": "Starting factory <twisted.web.server.Site ...>", ...}
275 ...
277 Composes `PlainFileLogObserver` and `JSONLogObserverWrapper` to a usable
278 logger.
280 .. versionadded:: 0.2.0
281 """
282 return JSONLogObserverWrapper(PlainFileLogObserver(sys.stdout))
285class EventAdapter:
286 """
287 Adapt an ``event_dict`` to Twisted logging system.
289 Particularly, make a wrapped `twisted.python.log.err
290 <https://docs.twisted.org/en/stable/api/twisted.python.log.html#err>`_
291 behave as expected.
293 Args:
294 dictRenderer:
295 Renderer that is used for the actual log message. Please note that
296 structlog comes with a dedicated `JSONRenderer`.
298 **Must** be the last processor in the chain and requires a *dictRenderer*
299 for the actual formatting as an constructor argument in order to be able to
300 fully support the original behaviors of ``log.msg()`` and ``log.err()``.
301 """
303 def __init__(
304 self,
305 dictRenderer: (
306 Callable[[WrappedLogger, str, EventDict], str] | None
307 ) = None,
308 ) -> None:
309 self._dictRenderer = dictRenderer or _BUILTIN_DEFAULT_PROCESSORS[-1]
311 def __call__(
312 self, logger: WrappedLogger, name: str, eventDict: EventDict
313 ) -> Any:
314 if name == "err":
315 # This aspires to handle the following cases correctly:
316 # 1. log.err(failure, _why='event', **kw)
317 # 2. log.err('event', **kw)
318 # 3. log.err(_stuff=failure, _why='event', **kw)
319 _stuff, _why, eventDict = _extractStuffAndWhy(eventDict)
320 eventDict["event"] = _why
322 return (
323 (),
324 {
325 "_stuff": _stuff,
326 "_why": self._dictRenderer(logger, name, eventDict),
327 },
328 )
330 return self._dictRenderer(logger, name, eventDict)