sentry-structlog 3.0.0__tar.gz → 3.0.1__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.
- {sentry_structlog-3.0.0/sentry_structlog.egg-info → sentry_structlog-3.0.1}/PKG-INFO +16 -7
- {sentry_structlog-3.0.0 → sentry_structlog-3.0.1}/README.md +15 -6
- {sentry_structlog-3.0.0 → sentry_structlog-3.0.1}/pyproject.toml +2 -1
- {sentry_structlog-3.0.0 → sentry_structlog-3.0.1}/sentry_structlog/__init__.py +57 -27
- {sentry_structlog-3.0.0 → sentry_structlog-3.0.1/sentry_structlog.egg-info}/PKG-INFO +16 -7
- {sentry_structlog-3.0.0 → sentry_structlog-3.0.1}/test/test_sentry_processor.py +167 -1
- {sentry_structlog-3.0.0 → sentry_structlog-3.0.1}/LICENSE +0 -0
- {sentry_structlog-3.0.0 → sentry_structlog-3.0.1}/sentry_structlog/py.typed +0 -0
- {sentry_structlog-3.0.0 → sentry_structlog-3.0.1}/sentry_structlog.egg-info/SOURCES.txt +0 -0
- {sentry_structlog-3.0.0 → sentry_structlog-3.0.1}/sentry_structlog.egg-info/dependency_links.txt +0 -0
- {sentry_structlog-3.0.0 → sentry_structlog-3.0.1}/sentry_structlog.egg-info/requires.txt +0 -0
- {sentry_structlog-3.0.0 → sentry_structlog-3.0.1}/sentry_structlog.egg-info/top_level.txt +0 -0
- {sentry_structlog-3.0.0 → sentry_structlog-3.0.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sentry-structlog
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.1
|
|
4
4
|
Summary: Sentry integration for structlog (thread-safe fork of structlog-sentry)
|
|
5
5
|
Author-email: "Kiwi.com platform" <platform@kiwi.com>
|
|
6
6
|
Maintainer: Barsoomx
|
|
@@ -207,10 +207,11 @@ The `SentryProcessor` class takes the following arguments:
|
|
|
207
207
|
Add `structlog.stdlib.add_log_level` (or `add_log_level_number`) before the
|
|
208
208
|
processor. `add_logger_name` is optional and also belongs before it. Names are
|
|
209
209
|
resolved from non-empty strings in `event_dict["logger"]`, `_record.name`, then
|
|
210
|
-
the wrapped logger's `name`.
|
|
211
|
-
|
|
212
|
-
original event data.
|
|
213
|
-
are
|
|
210
|
+
the wrapped logger's `name`. The resolved name supplies the Sentry event logger
|
|
211
|
+
and breadcrumb category even when the event dict's `logger` is `None`, empty,
|
|
212
|
+
or not a string, without changing original event data. Without a resolved name,
|
|
213
|
+
non-string values are omitted from the event logger and breadcrumb category.
|
|
214
|
+
`CapturingLogger` and mock loggers without a string name are supported.
|
|
214
215
|
|
|
215
216
|
### Capture status
|
|
216
217
|
|
|
@@ -283,6 +284,8 @@ stack in `threads.values`, with `crashed=False` and `current=True`, even when
|
|
|
283
284
|
`include_local_variables`, `include_source_context`, and `max_value_length`
|
|
284
285
|
options. With `attach_stacktrace=True`, the SDK supplies the stack instead.
|
|
285
286
|
Plain events without either flag only get a stack when that client option is on.
|
|
287
|
+
Stacks captured by the processor omit `sentry_structlog`, `structlog`, and
|
|
288
|
+
`logging` frames so raw log data in their locals cannot bypass context scrubbing.
|
|
286
289
|
|
|
287
290
|
The processor leaves `exc_info`, `stack_info`, and `stack` unchanged for
|
|
288
291
|
subsequent processors. If `StackInfoRenderer` ran first, the rendered `stack`
|
|
@@ -398,11 +401,17 @@ sentry_sdk.init(
|
|
|
398
401
|
With this scrubber, `value="secret"` and `nested={"value": "s"}` are filtered in
|
|
399
402
|
the context, and `value` is omitted from tags. `scrub=False` disables these
|
|
400
403
|
processor-level protections; it does not disable the tag policy above. If the
|
|
401
|
-
client has no event scrubber, the processor
|
|
402
|
-
tags
|
|
404
|
+
client has no event scrubber, the processor does not scrub context values or
|
|
405
|
+
eligible tags. When configured, the SDK's event scrubber handles breadcrumb
|
|
403
406
|
data while preparing the containing event, even with `scrub=False`; the
|
|
404
407
|
processor does not scrub it a second time.
|
|
405
408
|
|
|
409
|
+
Context and breadcrumb data use separate copies of dictionaries, lists, and
|
|
410
|
+
tuples. Repeated references to these containers become `"<cyclic reference>"`,
|
|
411
|
+
including shared containers without a cycle, so recursive scrubbing and
|
|
412
|
+
serialization can finish. This also applies with `scrub=False` or no client
|
|
413
|
+
scrubber. Original log values and callback hints remain unchanged.
|
|
414
|
+
|
|
406
415
|
### Scopes and thread isolation
|
|
407
416
|
|
|
408
417
|
Configure shared processors as `SentryProcessor()` without `scope=`. In
|
|
@@ -175,10 +175,11 @@ The `SentryProcessor` class takes the following arguments:
|
|
|
175
175
|
Add `structlog.stdlib.add_log_level` (or `add_log_level_number`) before the
|
|
176
176
|
processor. `add_logger_name` is optional and also belongs before it. Names are
|
|
177
177
|
resolved from non-empty strings in `event_dict["logger"]`, `_record.name`, then
|
|
178
|
-
the wrapped logger's `name`.
|
|
179
|
-
|
|
180
|
-
original event data.
|
|
181
|
-
are
|
|
178
|
+
the wrapped logger's `name`. The resolved name supplies the Sentry event logger
|
|
179
|
+
and breadcrumb category even when the event dict's `logger` is `None`, empty,
|
|
180
|
+
or not a string, without changing original event data. Without a resolved name,
|
|
181
|
+
non-string values are omitted from the event logger and breadcrumb category.
|
|
182
|
+
`CapturingLogger` and mock loggers without a string name are supported.
|
|
182
183
|
|
|
183
184
|
### Capture status
|
|
184
185
|
|
|
@@ -251,6 +252,8 @@ stack in `threads.values`, with `crashed=False` and `current=True`, even when
|
|
|
251
252
|
`include_local_variables`, `include_source_context`, and `max_value_length`
|
|
252
253
|
options. With `attach_stacktrace=True`, the SDK supplies the stack instead.
|
|
253
254
|
Plain events without either flag only get a stack when that client option is on.
|
|
255
|
+
Stacks captured by the processor omit `sentry_structlog`, `structlog`, and
|
|
256
|
+
`logging` frames so raw log data in their locals cannot bypass context scrubbing.
|
|
254
257
|
|
|
255
258
|
The processor leaves `exc_info`, `stack_info`, and `stack` unchanged for
|
|
256
259
|
subsequent processors. If `StackInfoRenderer` ran first, the rendered `stack`
|
|
@@ -366,11 +369,17 @@ sentry_sdk.init(
|
|
|
366
369
|
With this scrubber, `value="secret"` and `nested={"value": "s"}` are filtered in
|
|
367
370
|
the context, and `value` is omitted from tags. `scrub=False` disables these
|
|
368
371
|
processor-level protections; it does not disable the tag policy above. If the
|
|
369
|
-
client has no event scrubber, the processor
|
|
370
|
-
tags
|
|
372
|
+
client has no event scrubber, the processor does not scrub context values or
|
|
373
|
+
eligible tags. When configured, the SDK's event scrubber handles breadcrumb
|
|
371
374
|
data while preparing the containing event, even with `scrub=False`; the
|
|
372
375
|
processor does not scrub it a second time.
|
|
373
376
|
|
|
377
|
+
Context and breadcrumb data use separate copies of dictionaries, lists, and
|
|
378
|
+
tuples. Repeated references to these containers become `"<cyclic reference>"`,
|
|
379
|
+
including shared containers without a cycle, so recursive scrubbing and
|
|
380
|
+
serialization can finish. This also applies with `scrub=False` or no client
|
|
381
|
+
scrubber. Original log values and callback hints remain unchanged.
|
|
382
|
+
|
|
374
383
|
### Scopes and thread isolation
|
|
375
384
|
|
|
376
385
|
Configure shared processors as `SentryProcessor()` without `scope=`. In
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sentry-structlog"
|
|
7
|
-
version = "3.0.
|
|
7
|
+
version = "3.0.1"
|
|
8
8
|
description = "Sentry integration for structlog (thread-safe fork of structlog-sentry)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -50,6 +50,7 @@ dev = [
|
|
|
50
50
|
"ruff==0.15.17",
|
|
51
51
|
"mypy==1.20.0",
|
|
52
52
|
"twine>=6",
|
|
53
|
+
"pyright>=1.1.400",
|
|
53
54
|
]
|
|
54
55
|
|
|
55
56
|
[tool.setuptools.packages.find]
|
|
@@ -29,7 +29,7 @@ try:
|
|
|
29
29
|
from structlog.processors import NAME_TO_LEVEL
|
|
30
30
|
except ImportError: # Older structlog versions expose the same mapping privately.
|
|
31
31
|
from structlog.processors import ( # type: ignore[attr-defined,no-redef]
|
|
32
|
-
_NAME_TO_LEVEL as NAME_TO_LEVEL,
|
|
32
|
+
_NAME_TO_LEVEL as NAME_TO_LEVEL, # pyright: ignore[reportAttributeAccessIssue]
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
|
|
@@ -54,6 +54,7 @@ _TAG_KEY_PATTERN = re.compile(r"[a-zA-Z0-9_.:-]{1,32}")
|
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def _to_tag_value(value: object) -> str | None:
|
|
57
|
+
__tracebackhide__ = True
|
|
57
58
|
if not isinstance(value, (str, int, float, bool, UUID, Decimal, Enum)):
|
|
58
59
|
return None
|
|
59
60
|
value = str(value)
|
|
@@ -62,13 +63,22 @@ def _to_tag_value(value: object) -> str | None:
|
|
|
62
63
|
return value
|
|
63
64
|
|
|
64
65
|
|
|
65
|
-
def _copy_scrub_data(value: Any) -> Any:
|
|
66
|
-
"""Copy containers
|
|
66
|
+
def _copy_scrub_data(value: Any, memo: set[int] | None = None) -> Any:
|
|
67
|
+
"""Copy containers, replacing repeated references and preserving opaque values."""
|
|
68
|
+
__tracebackhide__ = True
|
|
69
|
+
if not isinstance(value, (dict, list, tuple)):
|
|
70
|
+
return value
|
|
71
|
+
if memo is None:
|
|
72
|
+
memo = set()
|
|
73
|
+
identity = id(value)
|
|
74
|
+
if identity in memo:
|
|
75
|
+
return "<cyclic reference>"
|
|
76
|
+
memo.add(identity)
|
|
67
77
|
if isinstance(value, dict):
|
|
68
|
-
return {key: _copy_scrub_data(item) for key, item in value.items()}
|
|
78
|
+
return {key: _copy_scrub_data(item, memo) for key, item in value.items()}
|
|
69
79
|
if isinstance(value, list):
|
|
70
|
-
return [_copy_scrub_data(item) for item in value]
|
|
71
|
-
return value
|
|
80
|
+
return [_copy_scrub_data(item, memo) for item in value]
|
|
81
|
+
return tuple(_copy_scrub_data(item, memo) for item in value)
|
|
72
82
|
|
|
73
83
|
|
|
74
84
|
def _figure_out_exc_info(v: Any) -> ExcInfo | None:
|
|
@@ -77,6 +87,7 @@ def _figure_out_exc_info(v: Any) -> ExcInfo | None:
|
|
|
77
87
|
to transform *v* into an ``exc_info`` tuple. Falsy flags become ``None``;
|
|
78
88
|
the empty ``sys.exc_info()`` tuple is preserved to request a stack trace.
|
|
79
89
|
"""
|
|
90
|
+
__tracebackhide__ = True
|
|
80
91
|
if isinstance(v, BaseException):
|
|
81
92
|
return (v.__class__, v, v.__traceback__)
|
|
82
93
|
elif isinstance(v, tuple):
|
|
@@ -172,6 +183,7 @@ class SentryProcessor:
|
|
|
172
183
|
:param logger: logger instance
|
|
173
184
|
:param event_dict: structlog event_dict
|
|
174
185
|
"""
|
|
186
|
+
__tracebackhide__ = True
|
|
175
187
|
l_name = event_dict.get("logger")
|
|
176
188
|
if isinstance(l_name, str) and l_name:
|
|
177
189
|
return l_name
|
|
@@ -187,10 +199,12 @@ class SentryProcessor:
|
|
|
187
199
|
return None
|
|
188
200
|
|
|
189
201
|
def _get_scope(self) -> Scope:
|
|
202
|
+
__tracebackhide__ = True
|
|
190
203
|
return self._scope or get_isolation_scope()
|
|
191
204
|
|
|
192
205
|
@staticmethod
|
|
193
206
|
def _get_hint(event_dict: EventDict) -> Hint:
|
|
207
|
+
__tracebackhide__ = True
|
|
194
208
|
hint: Hint = {"structlog": dict(event_dict)}
|
|
195
209
|
record = event_dict.get("_record")
|
|
196
210
|
if isinstance(record, logging.LogRecord):
|
|
@@ -207,6 +221,7 @@ class SentryProcessor:
|
|
|
207
221
|
`sentry_skip`, before capturing the event; used for tags and context.
|
|
208
222
|
Defaults to `event_dict` itself.
|
|
209
223
|
"""
|
|
224
|
+
__tracebackhide__ = True
|
|
210
225
|
if original_event_dict is None:
|
|
211
226
|
original_event_dict = event_dict
|
|
212
227
|
|
|
@@ -222,24 +237,34 @@ class SentryProcessor:
|
|
|
222
237
|
mechanism={"type": "structlog", "handled": True},
|
|
223
238
|
)
|
|
224
239
|
else:
|
|
225
|
-
event
|
|
240
|
+
event = {}
|
|
241
|
+
hint = {}
|
|
226
242
|
# The SDK client supplies its own stack when attach_stacktrace is set.
|
|
227
243
|
if (exc_info or event_dict.get("stack_info")) and not options.get(
|
|
228
244
|
"attach_stacktrace"
|
|
229
245
|
):
|
|
230
246
|
with capture_internal_exceptions():
|
|
247
|
+
stacktrace = current_stacktrace(
|
|
248
|
+
include_local_variables=options.get(
|
|
249
|
+
"include_local_variables", True
|
|
250
|
+
),
|
|
251
|
+
include_source_context=options.get(
|
|
252
|
+
"include_source_context", True
|
|
253
|
+
),
|
|
254
|
+
max_value_length=options.get("max_value_length"),
|
|
255
|
+
)
|
|
256
|
+
# Nonrecursive SDK scrubbing cannot clean nested event_dict locals.
|
|
257
|
+
stacktrace["frames"] = [
|
|
258
|
+
frame
|
|
259
|
+
for frame in stacktrace["frames"]
|
|
260
|
+
if not (frame.get("module") or "").startswith(
|
|
261
|
+
("sentry_structlog", "structlog", "logging")
|
|
262
|
+
)
|
|
263
|
+
]
|
|
231
264
|
event["threads"] = {
|
|
232
265
|
"values": [
|
|
233
266
|
{
|
|
234
|
-
"stacktrace":
|
|
235
|
-
include_local_variables=options.get(
|
|
236
|
-
"include_local_variables", True
|
|
237
|
-
),
|
|
238
|
-
include_source_context=options.get(
|
|
239
|
-
"include_source_context", True
|
|
240
|
-
),
|
|
241
|
-
max_value_length=options.get("max_value_length"),
|
|
242
|
-
),
|
|
267
|
+
"stacktrace": stacktrace,
|
|
243
268
|
"crashed": False,
|
|
244
269
|
"current": True,
|
|
245
270
|
}
|
|
@@ -249,8 +274,9 @@ class SentryProcessor:
|
|
|
249
274
|
# Structlog permits missing/dynamic values; preserve them for the SDK.
|
|
250
275
|
event["message"] = event_dict.get("event") # type: ignore[typeddict-item]
|
|
251
276
|
event["level"] = event_dict.get("level") # type: ignore[typeddict-item]
|
|
252
|
-
|
|
253
|
-
|
|
277
|
+
logger_name = event_dict.get("logger")
|
|
278
|
+
if isinstance(logger_name, str):
|
|
279
|
+
event["logger"] = logger_name
|
|
254
280
|
|
|
255
281
|
scrubber = (
|
|
256
282
|
self._get_scope().get_client().options.get("event_scrubber")
|
|
@@ -258,11 +284,7 @@ class SentryProcessor:
|
|
|
258
284
|
else None
|
|
259
285
|
)
|
|
260
286
|
if self._as_context:
|
|
261
|
-
context = (
|
|
262
|
-
_copy_scrub_data(original_event_dict)
|
|
263
|
-
if scrubber is not None and scrubber.recursive
|
|
264
|
-
else dict(original_event_dict)
|
|
265
|
-
)
|
|
287
|
+
context = _copy_scrub_data(original_event_dict)
|
|
266
288
|
if isinstance(context.get("_record"), logging.LogRecord):
|
|
267
289
|
context.pop("_record")
|
|
268
290
|
if scrubber is not None:
|
|
@@ -292,24 +314,27 @@ class SentryProcessor:
|
|
|
292
314
|
def _get_breadcrumb_and_hint(
|
|
293
315
|
self, event_dict: EventDict
|
|
294
316
|
) -> tuple[Breadcrumb, Hint]:
|
|
317
|
+
__tracebackhide__ = True
|
|
295
318
|
data = {
|
|
296
319
|
k: v
|
|
297
320
|
for k, v in event_dict.items()
|
|
298
321
|
if k not in self.ignore_breadcrumb_data
|
|
299
322
|
and not (k == "_record" and isinstance(v, logging.LogRecord))
|
|
300
323
|
}
|
|
324
|
+
logger_name = event_dict.get("logger")
|
|
301
325
|
event: Breadcrumb = {
|
|
302
326
|
"type": "log",
|
|
303
327
|
"level": event_dict.get("level"),
|
|
304
|
-
"category":
|
|
328
|
+
"category": logger_name if isinstance(logger_name, str) else None,
|
|
305
329
|
"message": event_dict["event"],
|
|
306
330
|
"timestamp": event_dict.get("timestamp"),
|
|
307
|
-
"data": data,
|
|
331
|
+
"data": _copy_scrub_data(data),
|
|
308
332
|
}
|
|
309
333
|
|
|
310
334
|
return event, self._get_hint(event_dict)
|
|
311
335
|
|
|
312
336
|
def _can_record(self, logger_name: str | None, event_dict: EventDict) -> bool:
|
|
337
|
+
__tracebackhide__ = True
|
|
313
338
|
if logger_name:
|
|
314
339
|
for ignored_logger in _IGNORED_LOGGERS | self._ignored_loggers:
|
|
315
340
|
if fnmatch(logger_name, ignored_logger):
|
|
@@ -325,9 +350,10 @@ class SentryProcessor:
|
|
|
325
350
|
sentry_level: LogLevelStr | None = None,
|
|
326
351
|
logger_name: str | None = None,
|
|
327
352
|
) -> None:
|
|
353
|
+
__tracebackhide__ = True
|
|
328
354
|
with capture_internal_exceptions():
|
|
329
355
|
event, hint = self._get_event_and_hint(event_dict, original_event_dict)
|
|
330
|
-
if
|
|
356
|
+
if logger_name is not None:
|
|
331
357
|
event["logger"] = logger_name
|
|
332
358
|
if sentry_level is not None:
|
|
333
359
|
event["level"] = sentry_level
|
|
@@ -343,9 +369,10 @@ class SentryProcessor:
|
|
|
343
369
|
sentry_level: str | None = None,
|
|
344
370
|
logger_name: str | None = None,
|
|
345
371
|
) -> None:
|
|
372
|
+
__tracebackhide__ = True
|
|
346
373
|
with capture_internal_exceptions():
|
|
347
374
|
event, hint = self._get_breadcrumb_and_hint(event_dict)
|
|
348
|
-
if
|
|
375
|
+
if logger_name is not None:
|
|
349
376
|
event["category"] = logger_name
|
|
350
377
|
if sentry_level is not None:
|
|
351
378
|
event["level"] = sentry_level
|
|
@@ -354,6 +381,7 @@ class SentryProcessor:
|
|
|
354
381
|
@staticmethod
|
|
355
382
|
def _resolve_level(event_dict: EventDict) -> int | None:
|
|
356
383
|
"""Prefer a numeric level, then structlog aliases and registered names."""
|
|
384
|
+
__tracebackhide__ = True
|
|
357
385
|
level = event_dict.get("level_number")
|
|
358
386
|
if isinstance(level, int):
|
|
359
387
|
return level
|
|
@@ -375,6 +403,7 @@ class SentryProcessor:
|
|
|
375
403
|
@staticmethod
|
|
376
404
|
def _get_sentry_level(level: int) -> LogLevelStr:
|
|
377
405
|
"""Map numeric ranges to Sentry severities, including custom levels."""
|
|
406
|
+
__tracebackhide__ = True
|
|
378
407
|
if level >= logging.CRITICAL:
|
|
379
408
|
return "fatal"
|
|
380
409
|
if level >= logging.ERROR:
|
|
@@ -389,6 +418,7 @@ class SentryProcessor:
|
|
|
389
418
|
self, logger: WrappedLogger, name: str, event_dict: EventDict
|
|
390
419
|
) -> EventDict:
|
|
391
420
|
"""A middleware to process structlog `event_dict` and send it to Sentry."""
|
|
421
|
+
__tracebackhide__ = True
|
|
392
422
|
try:
|
|
393
423
|
with capture_internal_exceptions():
|
|
394
424
|
sentry_skip = event_dict.pop("sentry_skip", False)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sentry-structlog
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.1
|
|
4
4
|
Summary: Sentry integration for structlog (thread-safe fork of structlog-sentry)
|
|
5
5
|
Author-email: "Kiwi.com platform" <platform@kiwi.com>
|
|
6
6
|
Maintainer: Barsoomx
|
|
@@ -207,10 +207,11 @@ The `SentryProcessor` class takes the following arguments:
|
|
|
207
207
|
Add `structlog.stdlib.add_log_level` (or `add_log_level_number`) before the
|
|
208
208
|
processor. `add_logger_name` is optional and also belongs before it. Names are
|
|
209
209
|
resolved from non-empty strings in `event_dict["logger"]`, `_record.name`, then
|
|
210
|
-
the wrapped logger's `name`.
|
|
211
|
-
|
|
212
|
-
original event data.
|
|
213
|
-
are
|
|
210
|
+
the wrapped logger's `name`. The resolved name supplies the Sentry event logger
|
|
211
|
+
and breadcrumb category even when the event dict's `logger` is `None`, empty,
|
|
212
|
+
or not a string, without changing original event data. Without a resolved name,
|
|
213
|
+
non-string values are omitted from the event logger and breadcrumb category.
|
|
214
|
+
`CapturingLogger` and mock loggers without a string name are supported.
|
|
214
215
|
|
|
215
216
|
### Capture status
|
|
216
217
|
|
|
@@ -283,6 +284,8 @@ stack in `threads.values`, with `crashed=False` and `current=True`, even when
|
|
|
283
284
|
`include_local_variables`, `include_source_context`, and `max_value_length`
|
|
284
285
|
options. With `attach_stacktrace=True`, the SDK supplies the stack instead.
|
|
285
286
|
Plain events without either flag only get a stack when that client option is on.
|
|
287
|
+
Stacks captured by the processor omit `sentry_structlog`, `structlog`, and
|
|
288
|
+
`logging` frames so raw log data in their locals cannot bypass context scrubbing.
|
|
286
289
|
|
|
287
290
|
The processor leaves `exc_info`, `stack_info`, and `stack` unchanged for
|
|
288
291
|
subsequent processors. If `StackInfoRenderer` ran first, the rendered `stack`
|
|
@@ -398,11 +401,17 @@ sentry_sdk.init(
|
|
|
398
401
|
With this scrubber, `value="secret"` and `nested={"value": "s"}` are filtered in
|
|
399
402
|
the context, and `value` is omitted from tags. `scrub=False` disables these
|
|
400
403
|
processor-level protections; it does not disable the tag policy above. If the
|
|
401
|
-
client has no event scrubber, the processor
|
|
402
|
-
tags
|
|
404
|
+
client has no event scrubber, the processor does not scrub context values or
|
|
405
|
+
eligible tags. When configured, the SDK's event scrubber handles breadcrumb
|
|
403
406
|
data while preparing the containing event, even with `scrub=False`; the
|
|
404
407
|
processor does not scrub it a second time.
|
|
405
408
|
|
|
409
|
+
Context and breadcrumb data use separate copies of dictionaries, lists, and
|
|
410
|
+
tuples. Repeated references to these containers become `"<cyclic reference>"`,
|
|
411
|
+
including shared containers without a cycle, so recursive scrubbing and
|
|
412
|
+
serialization can finish. This also applies with `scrub=False` or no client
|
|
413
|
+
scrubber. Original log values and callback hints remain unchanged.
|
|
414
|
+
|
|
406
415
|
### Scopes and thread isolation
|
|
407
416
|
|
|
408
417
|
Configure shared processors as `SentryProcessor()` without `scope=`. In
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import logging
|
|
2
3
|
import sys
|
|
3
4
|
import threading
|
|
@@ -5,8 +6,9 @@ from concurrent.futures import ThreadPoolExecutor
|
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
from decimal import Decimal
|
|
7
8
|
from enum import Enum
|
|
9
|
+
from io import StringIO
|
|
8
10
|
from unittest.mock import Mock
|
|
9
|
-
from uuid import UUID
|
|
11
|
+
from uuid import UUID, uuid4
|
|
10
12
|
|
|
11
13
|
import pytest
|
|
12
14
|
import sentry_sdk
|
|
@@ -487,6 +489,60 @@ def test_per_call_stack_without_exception(sentry_events, flags):
|
|
|
487
489
|
assert {key: downstream[key] for key in flags} == flags
|
|
488
490
|
|
|
489
491
|
|
|
492
|
+
@pytest.mark.parametrize(
|
|
493
|
+
"sentry_events", [{"include_local_variables": True}], indirect=True
|
|
494
|
+
)
|
|
495
|
+
@pytest.mark.parametrize("flag", ["stack_info", "exc_info"])
|
|
496
|
+
@pytest.mark.parametrize("use_formatter", [False, True])
|
|
497
|
+
def test_per_call_stack_does_not_leak_log_password(sentry_events, flag, use_formatter):
|
|
498
|
+
processor = SentryProcessor(tag_keys="__all__")
|
|
499
|
+
if use_formatter:
|
|
500
|
+
logger = logging.Logger("stack.logger")
|
|
501
|
+
handler = logging.StreamHandler(StringIO())
|
|
502
|
+
handler.setFormatter(
|
|
503
|
+
structlog.stdlib.ProcessorFormatter(
|
|
504
|
+
processors=[
|
|
505
|
+
processor,
|
|
506
|
+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
507
|
+
structlog.processors.JSONRenderer(),
|
|
508
|
+
]
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
logger.addHandler(handler)
|
|
512
|
+
processors = [structlog.stdlib.ProcessorFormatter.wrap_for_formatter]
|
|
513
|
+
else:
|
|
514
|
+
logger = structlog.ReturnLogger()
|
|
515
|
+
processors = [processor]
|
|
516
|
+
log = structlog.wrap_logger(
|
|
517
|
+
logger,
|
|
518
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
519
|
+
processors=[structlog.stdlib.add_log_level, *processors],
|
|
520
|
+
)
|
|
521
|
+
# A generated value avoids embedding the password in captured source context.
|
|
522
|
+
password = str(uuid4())
|
|
523
|
+
assert sys.exc_info() == (None, None, None)
|
|
524
|
+
|
|
525
|
+
log.error("stack requested", password=password, **{flag: True})
|
|
526
|
+
|
|
527
|
+
[event] = sentry_events
|
|
528
|
+
assert password not in json.dumps(event, default=str)
|
|
529
|
+
assert event["contexts"]["structlog"]["password"] == "[Filtered]"
|
|
530
|
+
assert event["tags"] == {}
|
|
531
|
+
frames = event["threads"]["values"][0]["stacktrace"]["frames"]
|
|
532
|
+
assert all(
|
|
533
|
+
not (frame.get("module") or "").startswith(
|
|
534
|
+
("sentry_structlog", "structlog", "logging")
|
|
535
|
+
)
|
|
536
|
+
for frame in frames
|
|
537
|
+
)
|
|
538
|
+
[test_frame] = [
|
|
539
|
+
frame
|
|
540
|
+
for frame in frames
|
|
541
|
+
if frame["function"] == "test_per_call_stack_does_not_leak_log_password"
|
|
542
|
+
]
|
|
543
|
+
assert test_frame["vars"]["password"] == "[Filtered]"
|
|
544
|
+
|
|
545
|
+
|
|
490
546
|
@pytest.mark.parametrize("value", [None, False, 0, "", []])
|
|
491
547
|
def test_falsy_exc_info_is_normalized_to_none(value):
|
|
492
548
|
assert _figure_out_exc_info(value) is None
|
|
@@ -745,6 +801,13 @@ def test_logger_name_is_resolved_once_for_filter_and_payloads(sentry_events):
|
|
|
745
801
|
"logger, record_name, event_name, expected_name",
|
|
746
802
|
[
|
|
747
803
|
(logging.getLogger("wrapped.logger"), absent, absent, "wrapped.logger"),
|
|
804
|
+
(MockLogger("wrapped.logger"), absent, None, "wrapped.logger"),
|
|
805
|
+
(MockLogger("wrapped.logger"), absent, 42, "wrapped.logger"),
|
|
806
|
+
(MockLogger("wrapped.logger"), absent, "", "wrapped.logger"),
|
|
807
|
+
(None, "record.logger", absent, "record.logger"),
|
|
808
|
+
(MockLogger("wrapped.logger"), "record.logger", None, "record.logger"),
|
|
809
|
+
(MockLogger("wrapped.logger"), "record.logger", 42, "record.logger"),
|
|
810
|
+
(MockLogger("wrapped.logger"), "record.logger", "", "record.logger"),
|
|
748
811
|
(Mock(), "record.logger", absent, "record.logger"),
|
|
749
812
|
(structlog.testing.CapturingLogger(), "record.logger", absent, "record.logger"),
|
|
750
813
|
(MockLogger(None), "record.logger", absent, "record.logger"),
|
|
@@ -782,6 +845,23 @@ def test_resolved_logger_name_is_used_in_payloads(
|
|
|
782
845
|
assert ("logger" in first["contexts"]["structlog"]) == (event_name is not absent)
|
|
783
846
|
|
|
784
847
|
|
|
848
|
+
@pytest.mark.parametrize("event_name", [None, 42, Mock(), lambda: "name"])
|
|
849
|
+
def test_nonstring_logger_without_fallback_is_omitted(sentry_events, event_name):
|
|
850
|
+
processor = SentryProcessor()
|
|
851
|
+
event_data = {"level": "error", "event": "first", "logger": event_name}
|
|
852
|
+
|
|
853
|
+
event, _ = processor._get_event_and_hint(event_data)
|
|
854
|
+
assert "logger" not in event
|
|
855
|
+
processor(None, "error", event_data)
|
|
856
|
+
sentry_sdk.capture_message("flush breadcrumb")
|
|
857
|
+
|
|
858
|
+
first, second = sentry_events
|
|
859
|
+
assert "logger" not in first
|
|
860
|
+
[breadcrumb] = second["breadcrumbs"]["values"]
|
|
861
|
+
assert breadcrumb.get("category") is None
|
|
862
|
+
assert event_data["logger"] is event_name
|
|
863
|
+
|
|
864
|
+
|
|
785
865
|
@pytest.mark.parametrize("event_name", [None, "", 42, Mock(), lambda: "name"])
|
|
786
866
|
def test_invalid_event_logger_name_falls_back_for_ignore_filter(
|
|
787
867
|
sentry_events, event_name
|
|
@@ -1446,6 +1526,92 @@ def test_scrubbing_respects_nonrecursive_client_setting(sentry_events):
|
|
|
1446
1526
|
assert event["contexts"]["structlog"]["nested"] == {"value": "s"}
|
|
1447
1527
|
|
|
1448
1528
|
|
|
1529
|
+
@pytest.mark.parametrize(
|
|
1530
|
+
"sentry_events",
|
|
1531
|
+
[
|
|
1532
|
+
{"event_scrubber": EventScrubber(recursive=True)},
|
|
1533
|
+
{"event_scrubber": EventScrubber()},
|
|
1534
|
+
],
|
|
1535
|
+
indirect=True,
|
|
1536
|
+
ids=["recursive", "nonrecursive"],
|
|
1537
|
+
)
|
|
1538
|
+
@pytest.mark.parametrize("scrub", [True, False])
|
|
1539
|
+
@pytest.mark.parametrize("container", ["dict", "list", "tuple"])
|
|
1540
|
+
def test_cyclic_kwargs_are_delivered_without_mutation(
|
|
1541
|
+
sentry_events, sentry_hints, scrub, container
|
|
1542
|
+
):
|
|
1543
|
+
if container == "dict":
|
|
1544
|
+
value = {}
|
|
1545
|
+
value["self"] = value
|
|
1546
|
+
expected = {"self": "<cyclic reference>"}
|
|
1547
|
+
elif container == "list":
|
|
1548
|
+
value = []
|
|
1549
|
+
value.append({"self": value})
|
|
1550
|
+
expected = [{"self": "<cyclic reference>"}]
|
|
1551
|
+
else:
|
|
1552
|
+
items = []
|
|
1553
|
+
value = (items,)
|
|
1554
|
+
items.append(value)
|
|
1555
|
+
expected = [["<cyclic reference>"]]
|
|
1556
|
+
nested = {"child": {"cycle": value, "password": "nested password"}}
|
|
1557
|
+
log = structlog.wrap_logger(
|
|
1558
|
+
structlog.ReturnLogger(),
|
|
1559
|
+
processors=[
|
|
1560
|
+
structlog.stdlib.add_log_level,
|
|
1561
|
+
SentryProcessor(scrub=scrub, tag_keys="__all__"),
|
|
1562
|
+
],
|
|
1563
|
+
)
|
|
1564
|
+
|
|
1565
|
+
_, downstream = log.error("cyclic kwargs", nested=nested, password="top password")
|
|
1566
|
+
sentry_sdk.capture_message("flush breadcrumb")
|
|
1567
|
+
|
|
1568
|
+
first, second = sentry_events
|
|
1569
|
+
context = first["contexts"]["structlog"]
|
|
1570
|
+
assert context["nested"]["child"]["cycle"] == expected
|
|
1571
|
+
assert context["password"] == ("[Filtered]" if scrub else "top password")
|
|
1572
|
+
recursive = sentry_sdk.get_client().options["event_scrubber"].recursive
|
|
1573
|
+
assert context["nested"]["child"]["password"] == (
|
|
1574
|
+
"[Filtered]" if scrub and recursive else "nested password"
|
|
1575
|
+
)
|
|
1576
|
+
assert first["tags"] == ({} if scrub else {"password": "top password"})
|
|
1577
|
+
[breadcrumb] = second["breadcrumbs"]["values"]
|
|
1578
|
+
# The SDK may stringify deeply nested breadcrumb values at its depth limit.
|
|
1579
|
+
assert "<cyclic reference>" in json.dumps(
|
|
1580
|
+
breadcrumb["data"]["nested"]["child"]["cycle"]
|
|
1581
|
+
)
|
|
1582
|
+
assert breadcrumb["data"]["password"] == "[Filtered]"
|
|
1583
|
+
assert breadcrumb["data"]["nested"]["child"]["password"] == (
|
|
1584
|
+
"[Filtered]" if recursive else "nested password"
|
|
1585
|
+
)
|
|
1586
|
+
assert downstream["nested"] is nested
|
|
1587
|
+
assert nested["child"]["password"] == "nested password"
|
|
1588
|
+
assert downstream["password"] == "top password"
|
|
1589
|
+
if container == "dict":
|
|
1590
|
+
assert value["self"] is value
|
|
1591
|
+
elif container == "list":
|
|
1592
|
+
assert value[0]["self"] is value
|
|
1593
|
+
else:
|
|
1594
|
+
assert value[0][0] is value
|
|
1595
|
+
assert sentry_hints["events"][0]["structlog"]["nested"] is nested
|
|
1596
|
+
assert sentry_hints["breadcrumbs"][0]["structlog"]["nested"] is nested
|
|
1597
|
+
|
|
1598
|
+
|
|
1599
|
+
@pytest.mark.parametrize("container", [dict, list, tuple])
|
|
1600
|
+
def test_repeated_containers_use_cycle_marker(sentry_events, container):
|
|
1601
|
+
value = container()
|
|
1602
|
+
SentryProcessor(tag_keys="__all__")(
|
|
1603
|
+
None, "error", {"level": "error", "event": "aliases", "values": [value, value]}
|
|
1604
|
+
)
|
|
1605
|
+
sentry_sdk.capture_message("flush breadcrumb")
|
|
1606
|
+
|
|
1607
|
+
first, second = sentry_events
|
|
1608
|
+
expected = [{} if container is dict else [], "<cyclic reference>"]
|
|
1609
|
+
assert first["contexts"]["structlog"]["values"] == expected
|
|
1610
|
+
assert first["tags"] == {}
|
|
1611
|
+
[breadcrumb] = second["breadcrumbs"]["values"]
|
|
1612
|
+
assert breadcrumb["data"]["values"] == expected
|
|
1613
|
+
|
|
1614
|
+
|
|
1449
1615
|
def test_capture_without_client_scrubber(sentry_events, monkeypatch):
|
|
1450
1616
|
client = sentry_sdk.get_client()
|
|
1451
1617
|
monkeypatch.setitem(client.options, "event_scrubber", None)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sentry_structlog-3.0.0 → sentry_structlog-3.0.1}/sentry_structlog.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|