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