File: thread-local.md

package info (click to toggle)
python-structlog 25.3.0-2
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 2,460 kB
  • sloc: python: 8,011; makefile: 138
file content (183 lines) | stat: -rw-r--r-- 6,781 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# Legacy Thread-local Context

:::{attention}
The `structlog.threadlocal` module is deprecated as of *structlog* 22.1.0 in favor of {doc}`contextvars`.

The standard library {mod}`contextvars` module provides a more feature-rich superset of the thread-local APIs and works with thread-local data, async code, and greenlets.

Therefore, as of 22.1.0, the `structlog.threadlocal` module is frozen and will be removed after May 2023.
:::

```{testsetup} *
import structlog
structlog.configure(
      processors=[structlog.processors.KeyValueRenderer()],
)
```

```{testcleanup} *
import structlog
structlog.reset_defaults()
```


## The `merge_threadlocal` processor

*structlog* provides a simple set of functions that allow explicitly binding certain fields to a global (thread-local) context and merge them later using a processor into the event dict.

The general flow of using these functions is:

- Use {func}`structlog.configure` with {func}`structlog.threadlocal.merge_threadlocal` as your first processor.
- Call {func}`structlog.threadlocal.clear_threadlocal` at the beginning of your request handler (or whenever you want to reset the thread-local context).
- Call {func}`structlog.threadlocal.bind_threadlocal` as an alternative to your bound logger's `bind()` when you want to bind a particular variable to the thread-local context.
- Use *structlog* as normal.
  Loggers act as they always do, but the {func}`structlog.threadlocal.merge_threadlocal` processor ensures that any thread-local binds get included in all of your log messages.
- If you want to access the thread-local storage, you use {func}`structlog.threadlocal.get_threadlocal` and {func}`structlog.threadlocal.get_merged_threadlocal`.

These functions map 1:1 to the {doc}`contextvars` APIs, so please use those instead:

- {func}`structlog.contextvars.merge_contextvars`
- {func}`structlog.contextvars.clear_contextvars`
- {func}`structlog.contextvars.bind_contextvars`
- {func}`structlog.contextvars.get_contextvars`
- {func}`structlog.contextvars.get_merged_contextvars`


## Thread-local contexts

*structlog* also provides thread-local context storage in a form that you may already know from [*Flask*](https://flask.palletsprojects.com/en/latest/design/#thread-locals) and that makes the *entire context* global to your thread or greenlet.

This makes its behavior more difficult to reason about which is why we generally recommend to use the {func}`~structlog.contextvars.merge_contextvars` route.
Therefore, there are currently no plans to re-implement this behavior on top of context variables.


### Wrapped dicts

In order to make your context thread-local, *structlog* ships with a function that can wrap any dict-like class to make it usable for thread-local storage: {func}`structlog.threadlocal.wrap_dict`.

Within one thread, every instance of the returned class will have a *common* instance of the wrapped dict-like class:

```{doctest}
>>> from structlog.threadlocal import wrap_dict
>>> WrappedDictClass = wrap_dict(dict)
>>> d1 = WrappedDictClass({"a": 1})
>>> d2 = WrappedDictClass({"b": 2})
>>> d3 = WrappedDictClass()
>>> d3["c"] = 3
>>> d1 is d3
False
>>> d1 == d2 == d3 == WrappedDictClass()
True
>>> d3  # doctest: +ELLIPSIS
<WrappedDict-...({'a': 1, 'b': 2, 'c': 3})>
```

To enable thread-local context use the generated class as the context class:

```python
configure(context_class=WrappedDictClass)
```

:::{note}
Creation of a new `BoundLogger` initializes the logger's context as `context_class(initial_values)`, and then adds any values passed via `.bind()`.
As all instances of a wrapped dict-like class share the same data, in the case above, the new logger's context will contain all previously bound values in addition to the new ones.
:::

`structlog.threadlocal.wrap_dict` returns always a completely *new* wrapped class:

```{doctest}
>>> from structlog.threadlocal import wrap_dict
>>> WrappedDictClass = wrap_dict(dict)
>>> AnotherWrappedDictClass = wrap_dict(dict)
>>> WrappedDictClass() != AnotherWrappedDictClass()
True
>>> WrappedDictClass.__name__  # doctest: +SKIP
WrappedDict-41e8382d-bee5-430e-ad7d-133c844695cc
>>> AnotherWrappedDictClass.__name__   # doctest: +SKIP
WrappedDict-e0fc330e-e5eb-42ee-bcec-ffd7bd09ad09
```

In order to be able to bind values temporarily to a logger, `structlog.threadlocal` comes with a [context manager](https://docs.python.org/2/library/stdtypes.html#context-manager-types): {func}`structlog.threadlocal.tmp_bind`:

```{testsetup} ctx
from structlog import PrintLogger, wrap_logger
from structlog.threadlocal import tmp_bind, wrap_dict
WrappedDictClass = wrap_dict(dict)
log = wrap_logger(PrintLogger(), context_class=WrappedDictClass)
```

```{doctest} ctx
>>> log.bind(x=42)  # doctest: +ELLIPSIS
<BoundLoggerFilteringAtNotset(context=<WrappedDict-...({'x': 42})>, ...)>
>>> log.msg("event!")
x=42 event='event!'
>>> with tmp_bind(log, x=23, y="foo") as tmp_log:
...     tmp_log.msg("another event!")
x=23 y='foo' event='another event!'
>>> log.msg("one last event!")
x=42 event='one last event!'
```

The state before the `with` statement is saved and restored once it's left.

If you want to detach a logger from thread-local data, there's {func}`structlog.threadlocal.as_immutable`.


#### Downsides & caveats

The convenience of having a thread-local context comes at a price though:

:::{warning}
- If you can't rule out that your application reuses threads, you *must* remember to **initialize your thread-local context** at the start of each request using {func}`~structlog.BoundLogger.new` (instead of {func}`~structlog.BoundLogger.bind`).
  Otherwise you may start a new request with the context still filled with data from the request before.

- **Don't** stop assigning the results of your `bind()`s and `new()`s!

  **Do**:

  ```
  log = log.new(y=23)
  log = log.bind(x=42)
  ```

  **Don't**:

  ```
  log.new(y=23)
  log.bind(x=42)
  ```

  Although the state is saved in a global data structure, you still need the global wrapped logger produce a real bound logger.
  Otherwise each log call will result in an instantiation of a temporary BoundLogger.

  See `configuration` for more details.

- It [doesn't play well](https://github.com/hynek/structlog/issues/296) with `os.fork` and thus `multiprocessing` (unless configured to use the `spawn` start method).
:::


## API

```{eval-rst}
.. module:: structlog.threadlocal

.. autofunction:: bind_threadlocal

.. autofunction:: unbind_threadlocal

.. autofunction:: bound_threadlocal

.. autofunction:: get_threadlocal

.. autofunction:: get_merged_threadlocal

.. autofunction:: merge_threadlocal

.. autofunction:: clear_threadlocal

.. autofunction:: wrap_dict

.. autofunction:: tmp_bind(logger, **tmp_values)

.. autofunction:: as_immutable
```