sentry-structlog 3.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Kiwi.com
4
+ Copyright (c) 2026 Barsoomx
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,611 @@
1
+ Metadata-Version: 2.4
2
+ Name: sentry-structlog
3
+ Version: 3.0.0
4
+ Summary: Sentry integration for structlog (thread-safe fork of structlog-sentry)
5
+ Author-email: "Kiwi.com platform" <platform@kiwi.com>
6
+ Maintainer: Barsoomx
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/Barsoomx/sentry-structlog
9
+ Project-URL: Repository, https://github.com/Barsoomx/sentry-structlog
10
+ Project-URL: Issues, https://github.com/Barsoomx/sentry-structlog/issues
11
+ Project-URL: Changelog, https://github.com/Barsoomx/sentry-structlog/blob/master/CHANGELOG.md
12
+ Project-URL: Upstream, https://github.com/kiwicom/structlog-sentry
13
+ Keywords: sentry,structlog,logging,observability
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Environment :: Web Environment
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: sentry-sdk<3,>=2.15
30
+ Requires-Dist: structlog>=23.1.0
31
+ Dynamic: license-file
32
+
33
+ # sentry-structlog
34
+
35
+ [![Tests](https://github.com/Barsoomx/sentry-structlog/actions/workflows/tests.yml/badge.svg)](https://github.com/Barsoomx/sentry-structlog/actions/workflows/tests.yml)
36
+ [![PyPI version](https://img.shields.io/pypi/v/sentry-structlog)](https://pypi.org/project/sentry-structlog/)
37
+
38
+ Send structlog messages to Sentry as Error events and breadcrumbs, with structured
39
+ context and optional tags.
40
+
41
+ Fork of [kiwicom/structlog-sentry](https://github.com/kiwicom/structlog-sentry),
42
+ originally authored by Kiwi.com platform and maintained here by
43
+ [@Barsoomx](https://github.com/Barsoomx). Distributed under the [MIT license](LICENSE).
44
+ Based on [Hynek Schlawack's processor](https://gist.github.com/hynek/a1f3f92d57071ebc5b91).
45
+ See the [changelog](CHANGELOG.md) for release history.
46
+
47
+ This fork builds tags and `contexts.structlog` from a snapshot of each call,
48
+ fixing cross-thread substitution of another log line's data on a shared processor.
49
+ An explicitly shared Sentry `Scope` is still mutable shared context; see
50
+ [Scopes and thread isolation](#scopes-and-thread-isolation).
51
+
52
+ ## Installation and compatibility
53
+
54
+ ```sh
55
+ pip install sentry-structlog
56
+ ```
57
+
58
+ | Component | Supported versions / dependency constraint |
59
+ | ---------- | ------------------------------------------- |
60
+ | Python | 3.10–3.14 tested; package requires `>=3.10` |
61
+ | sentry-sdk | `>=2.15,<3` |
62
+ | structlog | `>=23.1.0` |
63
+
64
+ These dependency ranges are broader than the individual versions exercised in CI.
65
+ The [CI matrix](#ci) covers the lockfile, minimum and latest stable dependencies;
66
+ the SDK 3 prerelease check is advisory and does not expand the supported range.
67
+
68
+ ## Quick start
69
+
70
+ Replace the example DSN with your project's DSN, then run this complete script:
71
+
72
+ ```python
73
+ import logging
74
+
75
+ import sentry_sdk
76
+ import structlog
77
+ from sentry_sdk.integrations.logging import LoggingIntegration
78
+
79
+ from sentry_structlog import SentryProcessor
80
+
81
+ sentry_sdk.init(
82
+ dsn="https://public@example.com/1",
83
+ disabled_integrations=[LoggingIntegration()],
84
+ )
85
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
86
+ structlog.configure(
87
+ processors=[
88
+ structlog.stdlib.add_logger_name,
89
+ structlog.stdlib.add_log_level,
90
+ SentryProcessor(event_level=logging.ERROR),
91
+ structlog.processors.StackInfoRenderer(),
92
+ structlog.processors.format_exc_info,
93
+ structlog.processors.JSONRenderer(),
94
+ ],
95
+ logger_factory=structlog.stdlib.LoggerFactory(),
96
+ wrapper_class=structlog.stdlib.BoundLogger,
97
+ )
98
+
99
+ log = structlog.get_logger("example")
100
+ log.info("job started", job="demo")
101
+ try:
102
+ 1 / 0
103
+ except ZeroDivisionError:
104
+ log.error("job failed", job="demo", exc_info=True)
105
+ sentry_sdk.flush()
106
+ ```
107
+
108
+ `add_log_level` must precede `SentryProcessor`. Keep the processor before
109
+ `format_exc_info` and `StackInfoRenderer`: those processors consume the raw
110
+ exception and stack flags. Finish with a renderer for local output.
111
+
112
+ With an active SDK and no filtering, these two calls capture **one Error event**
113
+ and record **two breadcrumbs**, with **no Sentry Logs**. The error event contains
114
+ the earlier info breadcrumb; its own breadcrumb is added after capture for later
115
+ events. The example overrides the processor's default event threshold of
116
+ `WARNING` with `ERROR`.
117
+
118
+ ## Error events, breadcrumbs, and Sentry Logs
119
+
120
+ `SentryProcessor` captures events at `event_level` and above and records
121
+ breadcrumbs at `level` and above. These thresholds are independent. Breadcrumbs
122
+ are stored on the scope for subsequent events, not sent as separate Error events.
123
+ The processor does not emit native Sentry Logs.
124
+
125
+ When structlog uses standard logging, the SDK's default `LoggingIntegration` can
126
+ capture the same record independently, causing duplicate events or breadcrumbs.
127
+ The quick start disables that integration entirely, which works throughout the
128
+ supported SDK range and also disables its Sentry Logs handler.
129
+
130
+ If you keep the integration, `LoggingIntegration(level=None, event_level=None)`
131
+ disables only its breadcrumbs and Error events. On SDK versions with Sentry Logs,
132
+ `enable_logs=True` (including the older experimental option) or an explicit
133
+ `capture_sentry_logs=True` on `LoggingIntegration` can still enable Log items.
134
+ A Log item is distinct from an Error event. On versions that accept them,
135
+ `capture_sentry_logs=False` or `sentry_logs_level=None` disables that integration's
136
+ Logs capture. These keywords, and the top-level `enable_logs` option, are not all
137
+ available in SDK 2.15; do not pass them unconditionally when supporting the full
138
+ range. See the [SDK logging integration](https://getsentry.github.io/sentry-python/_modules/sentry_sdk/integrations/logging.html).
139
+
140
+ `sentry_skip=True`, `active=False`, and `ignore_loggers` affect this processor's
141
+ events and breadcrumbs only. They do not suppress another integration, an
142
+ explicit `sentry_sdk.capture_exception()`, or a later unhandled exception.
143
+ Configure those capture paths separately if enabled.
144
+
145
+ ## Migrating from structlog-sentry
146
+
147
+ ```sh
148
+ pip uninstall structlog-sentry
149
+ pip install sentry-structlog
150
+ ```
151
+
152
+ Replace `from structlog_sentry import SentryProcessor` with
153
+ `from sentry_structlog import SentryProcessor`. Existing constructor arguments
154
+ keep their positions. Review these 3.0.0 behavior changes:
155
+
156
+ - Python 3.7–3.9 are no longer supported; check the dependency ranges above.
157
+ - `scrub=True` is now the default for context and tag protection.
158
+ - `tag_keys="__all__"` excludes `RESERVED_TAG_KEYS`; explicit selections can
159
+ include them. Supported scalar tag values become strings; invalid keys,
160
+ oversized or newline-containing values, and unsupported types are dropped.
161
+ - `tag_keys` accepts any iterable of keys, but strings other than `"__all__"`
162
+ raise `ValueError`.
163
+ - Events and breadcrumbs use Sentry severities, including `fatal` for critical
164
+ levels. Unknown levels are skipped safely.
165
+ - `hint["log_record"]` is present only for an actual `logging.LogRecord`;
166
+ structured callback data is in `hint["structlog"]`.
167
+ - `scope=` is deprecated and will be removed in 4.0.
168
+
169
+ See [Tags policy](#tags-policy), [Log levels](#log-levels), and the
170
+ [changelog](CHANGELOG.md) for the full contract.
171
+
172
+ ## Processor API
173
+
174
+ The `SentryProcessor` class takes the following arguments:
175
+
176
+ - `level` Events of this or higher levels will be reported as Sentry
177
+ breadcrumbs. Default is `logging.INFO`.
178
+ - `event_level` Events of this or higher levels will be reported to Sentry
179
+ as events. Default is `logging.WARNING`.
180
+ - `active` Enable or disable this processor. Default is `True`.
181
+ - `as_context` Send `event_dict` as `contexts.structlog` in Sentry events.
182
+ Default is `True`.
183
+ - `ignore_breadcrumb_data` Any iterable of data keys that will be excluded from
184
+ [breadcrumb data](https://docs.sentry.io/platforms/python/enriching-events/breadcrumbs/#manual-breadcrumbs).
185
+ Defaults to keys which are already sent separately, i.e. `level`, `logger`,
186
+ `event` and `timestamp`. All other data in `event_dict` will be sent as
187
+ breadcrumb data, except for the callback-only `_record` object. Copied into a
188
+ `frozenset` at construction, so iterators can be used safely across calls and
189
+ later changes to the source collection have no effect. This option does not
190
+ exclude keys from contexts or tags.
191
+ - `tag_keys` Any iterable of keys to send as tags (including lists, tuples, sets,
192
+ and generators), or `"__all__"` for all eligible keys. Defaults to `None` (no
193
+ structlog tags). Any other string raises `ValueError` during construction.
194
+ - `exclude_tag_keys` Additional keys to exclude from tags in either mode.
195
+ Defaults to `()`.
196
+ - `scrub` Apply the client's event scrubber to `contexts.structlog` and remove
197
+ denylisted tags. Defaults to `True`.
198
+ - `ignore_loggers` Any iterable of logger names or wildcard patterns to ignore
199
+ events and breadcrumbs from. Default is `None`. Copied into a `frozenset` at
200
+ construction.
201
+ - `verbose` Report the action taken by the logger in the `event_dict`.
202
+ Default is `False`.
203
+ - `scope` Deprecated optional `sentry_sdk.Scope`. Passing a scope emits
204
+ `DeprecationWarning`; this parameter will be removed in **4.0**. See
205
+ [Scopes and thread isolation](#scopes-and-thread-isolation).
206
+
207
+ Add `structlog.stdlib.add_log_level` (or `add_log_level_number`) before the
208
+ processor. `add_logger_name` is optional and also belongs before it. Names are
209
+ resolved from non-empty strings in `event_dict["logger"]`, `_record.name`, then
210
+ the wrapped logger's `name`. If the event dict has no `logger` key, the resolved
211
+ name supplies the Sentry event logger and breadcrumb category without changing
212
+ original event data. `CapturingLogger` and mock loggers without a string name
213
+ are supported.
214
+
215
+ ### Capture status
216
+
217
+ With `verbose=True`, `sentry="sent"` means the SDK returned an event ID, which is
218
+ also added as `sentry_id`. This indicates SDK acceptance, not guaranteed network
219
+ delivery. `sentry="dropped"` means the SDK returned no event ID; no `sentry_id` is
220
+ added, and the SDK's reason is not inferred. `sentry="ignored"` marks an ignored
221
+ logger, while `sentry="skipped"` covers disabled processing, `sentry_skip`, and
222
+ level filtering. Breadcrumbs are recorded independently according to `level`,
223
+ even when the SDK drops an event. With `verbose=False`, user-supplied `sentry`
224
+ metadata is left unchanged.
225
+
226
+ ### Log levels
227
+
228
+ Levels are resolved in this order:
229
+
230
+ 1. An integer `level_number`, as supplied by `structlog.stdlib.add_log_level_number`.
231
+ This takes precedence over `level` and works without a level name.
232
+ 2. A case-insensitive name from `structlog.processors.NAME_TO_LEVEL`
233
+ (`_NAME_TO_LEVEL` on older versions), including `exception` as `error` and
234
+ `warn` as `warning`.
235
+ 3. A name registered with Python logging: first the original spelling, then its
236
+ uppercase spelling. Only integer lookup results are accepted, so mixed-case
237
+ custom names such as `logging.addLevelName(25, "LeVeL")` work.
238
+
239
+ A non-integer `level_number` falls back to the name. Missing or unrecognized
240
+ levels (including `basic_format` and `nonsense`) produce neither an event nor a
241
+ breadcrumb. The original event data is retained, with `sentry="skipped"` added in
242
+ verbose mode. As with other calls, `sentry_skip` is consumed by the processor.
243
+ Errors during processing are contained so application logging can continue.
244
+
245
+ Both thresholds use the resolved number. Events and breadcrumbs use the same
246
+ Sentry severity mapping, including for custom levels:
247
+
248
+ | Numeric level | Sentry severity |
249
+ | ------------- | --------------- |
250
+ | Below 20 | `debug` |
251
+ | 20–29 | `info` |
252
+ | 30–39 | `warning` |
253
+ | 40–49 | `error` |
254
+ | 50 and above | `fatal` |
255
+
256
+ The original `level` and `level_number` remain unchanged for downstream
257
+ processors and in `contexts.structlog` (subject to the configured scrubber).
258
+
259
+ ### Exceptions and per-call stacks
260
+
261
+ The following calls use `log` from the quick start:
262
+
263
+ ```python
264
+ try:
265
+ 1 / 0
266
+ except ZeroDivisionError:
267
+ log.error("division failed", exc_info=True)
268
+
269
+ log.error("current call site", stack_info=True)
270
+ log.error("current call site", exc_info=True) # no active exception here
271
+ log.error("local output only", sentry_skip=True)
272
+ ```
273
+
274
+ A message inside an exception handler does not automatically capture the
275
+ exception: pass `exc_info=True`, an exception instance, or an exception tuple.
276
+ Exception events use `mechanism={"type": "structlog", "handled": True}`.
277
+ When both `exc_info` and `stack_info` are requested and an exception is available,
278
+ its traceback takes priority; no additional thread stack is attached.
279
+
280
+ Without an active exception, `exc_info=True` or `stack_info=True` attaches one
281
+ stack in `threads.values`, with `crashed=False` and `current=True`, even when
282
+ `attach_stacktrace=False`. Per-call capture respects the client's
283
+ `include_local_variables`, `include_source_context`, and `max_value_length`
284
+ options. With `attach_stacktrace=True`, the SDK supplies the stack instead.
285
+ Plain events without either flag only get a stack when that client option is on.
286
+
287
+ The processor leaves `exc_info`, `stack_info`, and `stack` unchanged for
288
+ subsequent processors. If `StackInfoRenderer` ran first, the rendered `stack`
289
+ string remains context, subject to scrubbing and serialization, but cannot be
290
+ converted back into a structured traceback. Global `attach_stacktrace` still
291
+ works. Both stack keys are reserved for tag selection.
292
+
293
+ Captured events use one snapshot for tags, `contexts.structlog`, and breadcrumb
294
+ data, taken after consuming `sentry_skip` and before adding `sentry_id` or verbose
295
+ status. Calls below `event_level` skip the event snapshot; recorded breadcrumbs
296
+ still get a separate callback snapshot.
297
+
298
+ ### Callback hints
299
+
300
+ Both `before_send(event, hint)` and `before_breadcrumb(breadcrumb, hint)` receive
301
+ `hint["structlog"]`: a shallow copy of the current call's event dict, taken before
302
+ adding `sentry_id` or verbose `sentry` status and after consuming `sentry_skip`.
303
+ Changing its top-level keys does not change data passed to downstream processors
304
+ or the other callback. Nested values are shared; the hint is not scrubbed.
305
+
306
+ When the event dict contains a real `logging.LogRecord` under `_record`, such as
307
+ with `structlog.stdlib.ProcessorFormatter`, `hint["log_record"]` contains that
308
+ record. Otherwise `log_record` is absent; pure structlog logs no longer put a
309
+ dict under this key. A callback shared with the SDK's `LoggingIntegration` can
310
+ use `hint.get("log_record")` for logger/level filtering and
311
+ `hint.get("structlog")` for structured metadata. Place `SentryProcessor` before
312
+ `ProcessorFormatter.remove_processors_meta` to retain access to `_record`.
313
+
314
+ Exception events also retain the SDK's `hint["exc_info"]`. Hints are callback
315
+ metadata, not additional event payload fields; a `LogRecord` stored in `_record`
316
+ is excluded from `contexts.structlog` and breadcrumb data.
317
+
318
+ ### Selecting tags and context
319
+
320
+ Replace the processor in the quick start to select tags explicitly:
321
+
322
+ ```python
323
+ SentryProcessor(event_level=logging.ERROR, tag_keys=("city", "timezone"))
324
+ ```
325
+
326
+ Then `log.error("job failed", city="Prague", timezone="Europe/Prague")` adds those
327
+ two tags. Use `tag_keys="__all__"` for all eligible keys, or `as_context=False` to
328
+ omit `contexts.structlog`.
329
+
330
+ | Option | Affects | Does not remove fields from |
331
+ | ------------------------------ | ------------------------------------- | --------------------------- |
332
+ | `tag_keys`, `exclude_tag_keys` | Event tags | Context and breadcrumb data |
333
+ | `as_context=False` | `contexts.structlog` | Tags and breadcrumb data |
334
+ | `ignore_breadcrumb_data` | Breadcrumb data | Tags and context |
335
+ | `scrub=True` | Processor context and denylisted tags | Unscrubbed callback hints |
336
+
337
+ `ignore_breadcrumb_data` defaults to `level`, `logger`, `event`, and `timestamp`,
338
+ which are already separate breadcrumb fields. A real `_record` is also excluded
339
+ from context and breadcrumb data. The SDK's event scrubber, when configured,
340
+ handles breadcrumb data while preparing the containing event.
341
+ Avoid placing secrets in messages or other fields merely because tags exclude
342
+ them; this policy is not a general-purpose sanitizer for all channels.
343
+
344
+ To ignore logger names or wildcard patterns in this processor, replace it with:
345
+
346
+ ```python
347
+ SentryProcessor(ignore_loggers=("noisy.logger", "noisy.worker.*"))
348
+ ```
349
+
350
+ ### Tags policy
351
+
352
+ `RESERVED_TAG_KEYS` is exported from `sentry_structlog`. In version 3.0.0,
353
+ `tag_keys="__all__"` always excludes these reserved keys:
354
+ `event`, `level`, `logger`, `timestamp`, `exc_info`, `exception`, `stack`,
355
+ `stack_info`, `sentry_skip`, `sentry`, `sentry_id`, `_record`, and `_from_structlog`.
356
+ An explicit iterable may select reserved keys. `sentry_skip` is consumed before
357
+ the snapshot and is never included in tags or `contexts.structlog`.
358
+
359
+ `exclude_tag_keys` adds consumer exclusions in both selection modes. For example,
360
+ exclude high-cardinality or personal fields:
361
+
362
+ ```python
363
+ SentryProcessor(
364
+ tag_keys="__all__",
365
+ exclude_tag_keys=("date", "username", "phone", "user_agent", "ip"),
366
+ )
367
+ ```
368
+
369
+ Tag values of type `str`, `int`, `float`, `bool`, `UUID`, `Decimal`, or `Enum`
370
+ are converted with `str()`. `None` and other types (including dictionaries and
371
+ lists) are dropped from tags. Values longer than **200 characters** or containing
372
+ `\n` are also dropped, rather than truncated. Tag keys must match
373
+ `^[a-zA-Z0-9_.:-]{1,32}$`: **1–32 characters**, using only ASCII letters, digits,
374
+ underscores, periods, colons, and hyphens.
375
+
376
+ Excluded keys and rejected values remain in `contexts.structlog` when
377
+ `as_context=True`, subject to scrubbing; tag conversion does not stringify the
378
+ context values.
379
+
380
+ The SDK's `EventScrubber.scrub_event()` does not scrub arbitrary `tags` or
381
+ `contexts`; this processor adds that protection for its own payload.
382
+ `scrub=True` is the default in 3.0.0. If the Sentry client has an `event_scrubber`,
383
+ its `scrub_dict()` cleans a separate copy of `contexts.structlog`, respecting the
384
+ scrubber's `recursive` setting. Tag keys matching its denylist (case-insensitively)
385
+ are removed entirely, rather than assigned `[Filtered]`. For example:
386
+
387
+ ```python
388
+ from sentry_sdk.scrubber import EventScrubber
389
+
390
+ # Replace the sentry_sdk.init() call in the quick start with this one.
391
+ sentry_sdk.init(
392
+ dsn="https://public@example.com/1",
393
+ disabled_integrations=[LoggingIntegration()],
394
+ event_scrubber=EventScrubber(denylist=["value"], recursive=True),
395
+ )
396
+ ```
397
+
398
+ With this scrubber, `value="secret"` and `nested={"value": "s"}` are filtered in
399
+ the context, and `value` is omitted from tags. `scrub=False` disables these
400
+ processor-level protections; it does not disable the tag policy above. If the
401
+ client has no event scrubber, the processor leaves context values and eligible
402
+ tags unchanged. When configured, the SDK's event scrubber handles breadcrumb
403
+ data while preparing the containing event, even with `scrub=False`; the
404
+ processor does not scrub it a second time.
405
+
406
+ ### Scopes and thread isolation
407
+
408
+ Configure shared processors as `SentryProcessor()` without `scope=`. In
409
+ sentry-sdk 2.x, the client attached to a pinned scope is ignored: the SDK selects
410
+ the client from its current, isolation, or global scope. The pinned scope still
411
+ merges its breadcrumbs, tags, and user into every event captured through it,
412
+ including events from other threads.
413
+
414
+ For a temporary client override, use
415
+ `with sentry_sdk.new_scope() as scope:` followed by `scope.set_client(client)`.
416
+ For separate threads or requests, enter an isolation scope in each worker and
417
+ bind its client there:
418
+
419
+ ```python
420
+ # log uses a shared SentryProcessor() configured without scope=.
421
+ def handle_request(client):
422
+ with sentry_sdk.isolation_scope() as scope:
423
+ scope.set_client(client)
424
+ log.error("request failed")
425
+ ```
426
+
427
+ Record request-specific breadcrumbs, tags, and user inside that isolation scope.
428
+ The per-call event snapshot does not transfer request context to another thread:
429
+ pass the needed context explicitly and bind it inside the worker's isolation scope.
430
+ The processor resolves the active scope on each call. `scope=` remains supported
431
+ with a caller-located deprecation warning until its removal in 4.0.
432
+
433
+ ## Output recipes
434
+
435
+ ### JSON and ConsoleRenderer
436
+
437
+ The quick start emits JSON. For a development console, replace its processor list
438
+ with the following, keeping the same SDK and standard logging setup:
439
+
440
+ ```python
441
+ structlog.configure(
442
+ processors=[
443
+ structlog.stdlib.add_logger_name,
444
+ structlog.stdlib.add_log_level,
445
+ SentryProcessor(event_level=logging.ERROR),
446
+ structlog.processors.StackInfoRenderer(),
447
+ structlog.dev.ConsoleRenderer(),
448
+ ],
449
+ logger_factory=structlog.stdlib.LoggerFactory(),
450
+ wrapper_class=structlog.stdlib.BoundLogger,
451
+ )
452
+ ```
453
+
454
+ `ConsoleRenderer` handles exception formatting itself; omit `format_exc_info`
455
+ when using it. Renderer choice does not change the Sentry capture paths.
456
+
457
+ ### Standard logging with ProcessorFormatter
458
+
459
+ After the imports and SDK initialization from the quick start, use this logging
460
+ configuration instead of its `basicConfig` and `structlog.configure` calls. It
461
+ processes both structlog and standard logging records through one handler:
462
+
463
+ ```python
464
+ shared_processors = [
465
+ structlog.stdlib.add_logger_name,
466
+ structlog.stdlib.add_log_level,
467
+ ]
468
+ formatter = structlog.stdlib.ProcessorFormatter(
469
+ foreign_pre_chain=shared_processors,
470
+ processors=[
471
+ SentryProcessor(event_level=logging.ERROR),
472
+ structlog.stdlib.ProcessorFormatter.remove_processors_meta,
473
+ structlog.processors.StackInfoRenderer(),
474
+ structlog.processors.format_exc_info,
475
+ structlog.processors.JSONRenderer(),
476
+ ],
477
+ )
478
+ handler = logging.StreamHandler()
479
+ handler.setFormatter(formatter)
480
+ root = logging.getLogger()
481
+ root.handlers.clear()
482
+ root.addHandler(handler)
483
+ root.setLevel(logging.INFO)
484
+ structlog.configure(
485
+ processors=[
486
+ *shared_processors,
487
+ structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
488
+ ],
489
+ logger_factory=structlog.stdlib.LoggerFactory(),
490
+ wrapper_class=structlog.stdlib.BoundLogger,
491
+ )
492
+
493
+ structlog.get_logger("example").error("structured error", job="demo")
494
+ logging.getLogger("example").error("standard logging error")
495
+ sentry_sdk.flush()
496
+ ```
497
+
498
+ With `LoggingIntegration` disabled as above and no SDK filtering, these calls
499
+ capture two Error events and record two breadcrumbs, with no Sentry Logs. Keep
500
+ `SentryProcessor` only in the formatter for this recipe; putting another instance
501
+ in `shared_processors`, or applying this formatter on multiple handlers, captures
502
+ the same record more than once. Placing it before `remove_processors_meta`
503
+ preserves `_record` for callback hints.
504
+
505
+ ## Typing
506
+
507
+ The package ships inline annotations and a PEP 561 `py.typed` marker in both the
508
+ wheel and source distribution. CI installs the wheel into a separate environment,
509
+ runs [the consumer](scripts/wheel_consumer.py) outside the checkout with Python's
510
+ isolated mode, and checks it with strict Mypy targeting Python 3.10. This verifies
511
+ that installed consumers can discover and use the shipped types; it does not
512
+ promise full strictness for every API or mypyc compatibility.
513
+
514
+ ## Development
515
+
516
+ Use Python 3.10 or newer and [uv](https://docs.astral.sh/uv/). The artifact checker
517
+ uses `tomllib`, so run the full development checklist with Python 3.11 or newer
518
+ (CI's quality and packaging jobs use Python 3.12):
519
+
520
+ ```sh
521
+ uv sync --locked --all-groups
522
+ uv run --no-sync pytest --cov --cov-report=term-missing --cov-report=xml --junitxml=junit.xml
523
+ uv run --no-sync ruff check .
524
+ uv run --no-sync ruff format --check .
525
+ uv run --no-sync mypy sentry_structlog
526
+ uv build
527
+ uv run --no-sync twine check --strict dist/*
528
+ uv run --no-sync python scripts/check_artifacts.py
529
+ ```
530
+
531
+ `uv sync` installs the default `dev` group. For a quick test run use
532
+ `uv run --no-sync pytest -q`; to format locally use `uv run --no-sync ruff format .`.
533
+ When intentionally updating dependencies, run `uv lock` and commit `uv.lock`.
534
+ Mypy is the configured type checker; there is no Pyright CI job or project config.
535
+
536
+ Optional [pre-commit hooks](.pre-commit-config.yaml) run Ruff, Mypy, basic file
537
+ checks, and Prettier for Markdown. Pre-commit is not in the dev dependency group:
538
+
539
+ ```sh
540
+ uv tool run pre-commit install
541
+ uv tool run pre-commit run --all-files
542
+ ```
543
+
544
+ ### CI
545
+
546
+ The [Tests workflow](.github/workflows/tests.yml) runs on pushes to `master`, pull
547
+ requests, a weekly schedule, and calls from the publish workflow. Its blocking
548
+ checks are:
549
+
550
+ | Job | Coverage |
551
+ | ---------------------------------- | ----------------------------------------------------------------------------------------------- |
552
+ | `Code Quality (Ruff, Mypy)` | Lint, formatting, package typing on Python 3.12 |
553
+ | `Python <version> (<os>)` | Locked dependencies on Ubuntu with Python 3.10–3.14 and Windows 2022 with Python 3.12 |
554
+ | `Dependencies (min)` | Python 3.10, `sentry-sdk==2.15.0`, `structlog==23.1.0` |
555
+ | `Dependencies (latest)` | Python 3.10, latest stable versions within the declared ranges |
556
+ | `Package and typed wheel consumer` | Build, strict Twine check, artifact contents, isolated installed-wheel consumer and strict Mypy |
557
+
558
+ `Sentry SDK prerelease` additionally tries SDK 3 prereleases on Python 3.12 with
559
+ `continue-on-error`; it is advisory. Dependency lanes use `uv run --no-sync`
560
+ after installing their selected versions so the lock cannot replace them.
561
+
562
+ Coverage includes branches and has a 90% gate. `coverage.xml` and `junit.xml` are
563
+ uploaded even on failures as `reports-<os>-py<version>` and
564
+ `reports-dependencies-<endpoint>` workflow artifacts. There is no external
565
+ coverage badge service configured.
566
+
567
+ ### Docker
568
+
569
+ Build the development image and run the full test suite:
570
+
571
+ ```sh
572
+ docker compose build
573
+ docker compose run --rm app
574
+ docker compose run --rm app uv run --no-sync ruff check .
575
+ ```
576
+
577
+ The [Docker setup](docker-compose.yml) uses Python 3.12 and bind-mounts the checkout
578
+ at `/app`, so source edits are available immediately. The image keeps its locked
579
+ Python environment at `/opt/venv`, separate from any host `.venv`. Rebuild after
580
+ changing `pyproject.toml` or `uv.lock`.
581
+
582
+ ## Contributing
583
+
584
+ Open an [issue](https://github.com/Barsoomx/sentry-structlog/issues) or
585
+ [pull request](https://github.com/Barsoomx/sentry-structlog/pulls) in this fork.
586
+ The maintainer is [@Barsoomx](https://github.com/Barsoomx). Include a reproduction
587
+ for bugs and run the relevant development checks before submitting changes.
588
+
589
+ ## Releasing
590
+
591
+ Set `project.version` in `pyproject.toml`, update [CHANGELOG.md](CHANGELOG.md),
592
+ refresh `uv.lock` as needed, commit, and push to `master`. Wait for the checks on
593
+ that commit, then push a matching version tag, for example:
594
+
595
+ ```sh
596
+ git tag v3.0.0
597
+ git push origin v3.0.0
598
+ ```
599
+
600
+ The [Publish to PyPI workflow](.github/workflows/publish.yml) requires the tag to
601
+ match `v<project.version>` exactly. `Build and check distributions` validates the
602
+ wheel and source distribution; the reusable Tests workflow runs on the same
603
+ commit. `Publish with PyPI Trusted Publishing` depends on both build and tests,
604
+ including the required jobs above, and publishes the already checked artifacts.
605
+ Only `v*` tag pushes in `Barsoomx/sentry-structlog` publish. Branch pushes, pull
606
+ requests, and manual runs check the build without publishing.
607
+
608
+ Configure PyPI Trusted Publishing for owner `Barsoomx`, repository
609
+ `sentry-structlog`, workflow filename `publish.yml`, and GitHub environment `pypi`.
610
+ Configure that environment to allow deployment only from `v*` tags. The workflow
611
+ requests `id-token: write` for OIDC; no PyPI API token is required.