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