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