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"""
7**Deprecated** primitives to keep context global but thread (and greenlet)
8local.
9
10See `thread-local`, but please use :doc:`contextvars` instead.
11
12.. deprecated:: 22.1.0
13"""
14
15from __future__ import annotations
16
17import contextlib
18import sys
19import threading
20import uuid
21import warnings
22
23from typing import Any, Generator, Iterator, TypeVar
24
25import structlog
26
27from ._config import BoundLoggerLazyProxy
28from .typing import BindableLogger, Context, EventDict, WrappedLogger
29
30
31def _determine_threadlocal() -> type[Any]:
32 """
33 Return a dict-like threadlocal storage depending on whether we run with
34 greenlets or not.
35 """
36 try:
37 from ._greenlets import GreenThreadLocal
38 except ImportError:
39 from threading import local
40
41 return local
42
43 return GreenThreadLocal # pragma: no cover
44
45
46ThreadLocal = _determine_threadlocal()
47
48
49def _deprecated() -> None:
50 """
51 Raise a warning with best-effort stacklevel adjustment.
52 """
53 callsite = ""
54
55 with contextlib.suppress(Exception):
56 f = sys._getframe()
57 callsite = f.f_back.f_back.f_globals[ # type: ignore[union-attr]
58 "__name__"
59 ]
60
61 # Avoid double warnings if TL functions call themselves.
62 if callsite == "structlog.threadlocal":
63 return
64
65 stacklevel = 3
66 # If a function is used as a decorator, we need to add two stack levels.
67 # This logic will probably break eventually, but it's not worth any more
68 # complexity.
69 if callsite == "contextlib":
70 stacklevel += 2
71
72 warnings.warn(
73 "`structlog.threadlocal` is deprecated, please use "
74 "`structlog.contextvars` instead.",
75 DeprecationWarning,
76 stacklevel=stacklevel,
77 )
78
79
80def wrap_dict(dict_class: type[Context]) -> type[Context]:
81 """
82 Wrap a dict-like class and return the resulting class.
83
84 The wrapped class and used to keep global in the current thread.
85
86 Args:
87 dict_class: Class used for keeping context.
88
89 .. deprecated:: 22.1.0
90 """
91 _deprecated()
92 Wrapped = type(
93 "WrappedDict-" + str(uuid.uuid4()), (_ThreadLocalDictWrapper,), {}
94 )
95 Wrapped._tl = ThreadLocal() # type: ignore[attr-defined]
96 Wrapped._dict_class = dict_class # type: ignore[attr-defined]
97
98 return Wrapped
99
100
101TLLogger = TypeVar("TLLogger", bound=BindableLogger)
102
103
104def as_immutable(logger: TLLogger) -> TLLogger:
105 """
106 Extract the context from a thread local logger into an immutable logger.
107
108 Args:
109 logger (structlog.typing.BindableLogger):
110 A logger with *possibly* thread local state.
111
112 Returns:
113 :class:`~structlog.BoundLogger` with an immutable context.
114
115 .. deprecated:: 22.1.0
116 """
117 _deprecated()
118 if isinstance(logger, BoundLoggerLazyProxy):
119 logger = logger.bind()
120
121 try:
122 ctx = logger._context._tl.dict_.__class__( # type: ignore[union-attr]
123 logger._context._dict # type: ignore[union-attr]
124 )
125 bl = logger.__class__(
126 logger._logger, # type: ignore[attr-defined, call-arg]
127 processors=logger._processors, # type: ignore[attr-defined]
128 context={},
129 )
130 bl._context = ctx
131
132 return bl
133 except AttributeError:
134 return logger
135
136
137@contextlib.contextmanager
138def tmp_bind(
139 logger: TLLogger, **tmp_values: Any
140) -> Generator[TLLogger, None, None]:
141 """
142 Bind *tmp_values* to *logger* & memorize current state. Rewind afterwards.
143
144 Only works with `structlog.threadlocal.wrap_dict`-based contexts.
145 Use :func:`~structlog.threadlocal.bound_threadlocal` for new code.
146
147 .. deprecated:: 22.1.0
148 """
149 _deprecated()
150 if isinstance(logger, BoundLoggerLazyProxy):
151 logger = logger.bind()
152
153 saved = as_immutable(logger)._context
154 try:
155 yield logger.bind(**tmp_values)
156 finally:
157 logger._context.clear()
158 logger._context.update(saved)
159
160
161class _ThreadLocalDictWrapper:
162 """
163 Wrap a dict-like class and keep the state *global* but *thread-local*.
164
165 Attempts to re-initialize only updates the wrapped dictionary.
166
167 Useful for short-lived threaded applications like requests in web app.
168
169 Use :func:`wrap` to instantiate and use
170 :func:`structlog.BoundLogger.new` to clear the context.
171 """
172
173 _tl: Any
174 _dict_class: type[dict[str, Any]]
175
176 def __init__(self, *args: Any, **kw: Any) -> None:
177 """
178 We cheat. A context dict gets never recreated.
179 """
180 if args and isinstance(args[0], self.__class__):
181 # our state is global, no need to look at args[0] if it's of our
182 # class
183 self._dict.update(**kw)
184 else:
185 self._dict.update(*args, **kw)
186
187 @property
188 def _dict(self) -> Context:
189 """
190 Return or create and return the current context.
191 """
192 try:
193 return self.__class__._tl.dict_
194 except AttributeError:
195 self.__class__._tl.dict_ = self.__class__._dict_class()
196
197 return self.__class__._tl.dict_
198
199 def __repr__(self) -> str:
200 return f"<{self.__class__.__name__}({self._dict!r})>"
201
202 def __eq__(self, other: object) -> bool:
203 # Same class == same dictionary
204 return self.__class__ == other.__class__
205
206 def __ne__(self, other: object) -> bool:
207 return not self.__eq__(other)
208
209 # Proxy methods necessary for structlog.
210 # Dunder methods don't trigger __getattr__ so we need to proxy by hand.
211 def __iter__(self) -> Iterator[str]:
212 return self._dict.__iter__()
213
214 def __setitem__(self, key: str, value: Any) -> None:
215 self._dict[key] = value
216
217 def __delitem__(self, key: str) -> None:
218 self._dict.__delitem__(key)
219
220 def __len__(self) -> int:
221 return self._dict.__len__()
222
223 def __getattr__(self, name: str) -> Any:
224 return getattr(self._dict, name)
225
226
227_CONTEXT = threading.local()
228
229
230def get_threadlocal() -> Context:
231 """
232 Return a copy of the current thread-local context.
233
234 .. versionadded:: 21.2.0
235 .. deprecated:: 22.1.0
236 """
237 _deprecated()
238 return _get_context().copy()
239
240
241def get_merged_threadlocal(bound_logger: BindableLogger) -> Context:
242 """
243 Return a copy of the current thread-local context merged with the context
244 from *bound_logger*.
245
246 .. versionadded:: 21.2.0
247 .. deprecated:: 22.1.0
248 """
249 _deprecated()
250 ctx = _get_context().copy()
251 ctx.update(structlog.get_context(bound_logger))
252
253 return ctx
254
255
256def merge_threadlocal(
257 logger: WrappedLogger, method_name: str, event_dict: EventDict
258) -> EventDict:
259 """
260 A processor that merges in a global (thread-local) context.
261
262 Use this as your first processor in :func:`structlog.configure` to ensure
263 thread-local context is included in all log calls.
264
265 .. versionadded:: 19.2.0
266
267 .. versionchanged:: 20.1.0
268 This function used to be called ``merge_threadlocal_context`` and that
269 name is still kept around for backward compatibility.
270
271 .. deprecated:: 22.1.0
272 """
273 _deprecated()
274 context = _get_context().copy()
275 context.update(event_dict)
276
277 return context
278
279
280# Alias that shouldn't be used anymore.
281merge_threadlocal_context = merge_threadlocal
282
283
284def clear_threadlocal() -> None:
285 """
286 Clear the thread-local context.
287
288 The typical use-case for this function is to invoke it early in
289 request-handling code.
290
291 .. versionadded:: 19.2.0
292 .. deprecated:: 22.1.0
293 """
294 _deprecated()
295 _CONTEXT.context = {}
296
297
298def bind_threadlocal(**kw: Any) -> None:
299 """
300 Put keys and values into the thread-local context.
301
302 Use this instead of :func:`~structlog.BoundLogger.bind` when you want some
303 context to be global (thread-local).
304
305 .. versionadded:: 19.2.0
306 .. deprecated:: 22.1.0
307 """
308 _deprecated()
309 _get_context().update(kw)
310
311
312def unbind_threadlocal(*keys: str) -> None:
313 """
314 Tries to remove bound *keys* from threadlocal logging context if present.
315
316 .. versionadded:: 20.1.0
317 .. deprecated:: 22.1.0
318 """
319 _deprecated()
320 context = _get_context()
321 for key in keys:
322 context.pop(key, None)
323
324
325@contextlib.contextmanager
326def bound_threadlocal(**kw: Any) -> Generator[None, None, None]:
327 """
328 Bind *kw* to the current thread-local context. Unbind or restore *kw*
329 afterwards. Do **not** affect other keys.
330
331 Can be used as a context manager or decorator.
332
333 .. versionadded:: 21.4.0
334 .. deprecated:: 22.1.0
335 """
336 _deprecated()
337 context = get_threadlocal()
338 saved = {k: context[k] for k in context.keys() & kw.keys()}
339
340 bind_threadlocal(**kw)
341 try:
342 yield
343 finally:
344 unbind_threadlocal(*kw.keys())
345 bind_threadlocal(**saved)
346
347
348def _get_context() -> Context:
349 try:
350 return _CONTEXT.context
351 except AttributeError:
352 _CONTEXT.context = {}
353
354 return _CONTEXT.context