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"""
7Primitives to deal with a concurrency supporting context, as introduced in
8Python 3.7 as :mod:`contextvars`.
9
10.. versionadded:: 20.1.0
11.. versionchanged:: 21.1.0
12 Reimplemented without using a single dict as context carrier for improved
13 isolation. Every key-value pair is a separate `contextvars.ContextVar` now.
14.. versionchanged:: 23.3.0
15 Callsite parameters are now also collected under asyncio.
16
17See :doc:`contextvars`.
18"""
19
20from __future__ import annotations
21
22import contextlib
23import contextvars
24
25from collections.abc import Generator, Mapping
26from types import FrameType
27from typing import Any
28
29import structlog
30
31from .typing import BindableLogger, EventDict, WrappedLogger
32
33
34STRUCTLOG_KEY_PREFIX = "structlog_"
35STRUCTLOG_KEY_PREFIX_LEN = len(STRUCTLOG_KEY_PREFIX)
36
37_ASYNC_CALLING_STACK: contextvars.ContextVar[FrameType] = (
38 contextvars.ContextVar("_ASYNC_CALLING_STACK")
39)
40
41# Stores thread info captured at async call time.
42# Value is a tuple of (thread_id: int, thread_name: str)
43_ASYNC_CALLING_THREAD: contextvars.ContextVar[tuple[int, str]] = (
44 contextvars.ContextVar("_ASYNC_CALLING_THREAD")
45)
46
47# For proper isolation, we have to use a dict of ContextVars instead of a
48# single ContextVar with a dict.
49# See https://github.com/hynek/structlog/pull/302 for details.
50_CONTEXT_VARS: dict[str, contextvars.ContextVar[Any]] = {}
51
52
53def get_contextvars() -> dict[str, Any]:
54 """
55 Return a copy of the *structlog*-specific context-local context.
56
57 .. versionadded:: 21.2.0
58 """
59 rv = {}
60 ctx = contextvars.copy_context()
61
62 for k in ctx:
63 if k.name.startswith(STRUCTLOG_KEY_PREFIX) and ctx[k] is not Ellipsis:
64 rv[k.name[STRUCTLOG_KEY_PREFIX_LEN:]] = ctx[k]
65
66 return rv
67
68
69def get_merged_contextvars(bound_logger: BindableLogger) -> dict[str, Any]:
70 """
71 Return a copy of the current context-local context merged with the context
72 from *bound_logger*.
73
74 .. versionadded:: 21.2.0
75 """
76 ctx = get_contextvars()
77 ctx.update(structlog.get_context(bound_logger))
78
79 return ctx
80
81
82def merge_contextvars(
83 logger: WrappedLogger, method_name: str, event_dict: EventDict
84) -> EventDict:
85 """
86 A processor that merges in a global (context-local) context.
87
88 Use this as your first processor in :func:`structlog.configure` to ensure
89 context-local context is included in all log calls.
90
91 .. versionadded:: 20.1.0
92 .. versionchanged:: 21.1.0 See toplevel note.
93 """
94 ctx = contextvars.copy_context()
95
96 for k in ctx:
97 if k.name.startswith(STRUCTLOG_KEY_PREFIX) and ctx[k] is not Ellipsis:
98 event_dict.setdefault(k.name[STRUCTLOG_KEY_PREFIX_LEN:], ctx[k])
99
100 return event_dict
101
102
103def clear_contextvars() -> None:
104 """
105 Clear the context-local context.
106
107 The typical use-case for this function is to invoke it early in request-
108 handling code.
109
110 .. versionadded:: 20.1.0
111 .. versionchanged:: 21.1.0 See toplevel note.
112 """
113 ctx = contextvars.copy_context()
114 for k in ctx:
115 if k.name.startswith(STRUCTLOG_KEY_PREFIX):
116 k.set(Ellipsis)
117
118
119def bind_contextvars(**kw: Any) -> Mapping[str, contextvars.Token[Any]]:
120 r"""
121 Put keys and values into the context-local context.
122
123 Use this instead of :func:`~structlog.BoundLogger.bind` when you want some
124 context to be global (context-local).
125
126 Return the mapping of `contextvars.Token`\s resulting
127 from setting the backing :class:`~contextvars.ContextVar`\s.
128 Suitable for passing to :func:`reset_contextvars`.
129
130 .. versionadded:: 20.1.0
131 .. versionchanged:: 21.1.0 Return the `contextvars.Token` mapping
132 rather than None. See also the toplevel note.
133 """
134 rv = {}
135 for k, v in kw.items():
136 structlog_k = f"{STRUCTLOG_KEY_PREFIX}{k}"
137 try:
138 var = _CONTEXT_VARS[structlog_k]
139 except KeyError:
140 var = contextvars.ContextVar(structlog_k, default=Ellipsis)
141 _CONTEXT_VARS[structlog_k] = var
142
143 rv[k] = var.set(v)
144
145 return rv
146
147
148def reset_contextvars(**kw: contextvars.Token[Any]) -> None:
149 r"""
150 Reset contextvars corresponding to the given Tokens.
151
152 .. versionadded:: 21.1.0
153 """
154 for k, v in kw.items():
155 structlog_k = f"{STRUCTLOG_KEY_PREFIX}{k}"
156 var = _CONTEXT_VARS[structlog_k]
157 var.reset(v)
158
159
160def unbind_contextvars(*keys: str) -> None:
161 """
162 Remove *keys* from the context-local context if they are present.
163
164 Use this instead of :func:`~structlog.BoundLogger.unbind` when you want to
165 remove keys from a global (context-local) context.
166
167 .. versionadded:: 20.1.0
168 .. versionchanged:: 21.1.0 See toplevel note.
169 """
170 for k in keys:
171 structlog_k = f"{STRUCTLOG_KEY_PREFIX}{k}"
172 if structlog_k in _CONTEXT_VARS:
173 _CONTEXT_VARS[structlog_k].set(Ellipsis)
174
175
176@contextlib.contextmanager
177def bound_contextvars(**kw: Any) -> Generator[None, None, None]:
178 """
179 Bind *kw* to the current context-local context. Unbind or restore *kw*
180 afterwards. Do **not** affect other keys.
181
182 Can be used as a context manager or decorator.
183
184 .. versionadded:: 21.4.0
185 """
186 context = get_contextvars()
187 saved = {k: context[k] for k in context.keys() & kw.keys()}
188
189 bind_contextvars(**kw)
190 try:
191 yield
192 finally:
193 unbind_contextvars(*kw.keys())
194 bind_contextvars(**saved)