Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/structlog/twisted.py: 7%

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

86 statements  

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)