debugbundle-python 2.1.0__tar.gz → 2.1.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.
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/PKG-INFO +7 -6
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/README.md +6 -5
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/pyproject.toml +1 -1
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/capture_hooks.py +14 -6
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/exception_capture.py +8 -2
- debugbundle_python-2.1.1/src/debugbundle/exception_identity.py +47 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/logger_integrations.py +16 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/structlog_integration.py +21 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/PKG-INFO +7 -6
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/SOURCES.txt +2 -0
- debugbundle_python-2.1.1/tests/test_logging_exception.py +110 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/LICENSE +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/setup.cfg +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/__init__.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/acknowledgement.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/before_send.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/canonical_event.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/config.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/core.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/delivery_hooks.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/diagnostic_budget.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/event-envelope.schema.json +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/event_support.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/evidence_delivery.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/evidence_http.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/evidence_json.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/evidence_wire.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/exception_evidence.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/exception_snapshot.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/flush_aggregates.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/flush_delivery.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/ingestion_wire.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/__init__.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/common.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/django.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/fastapi.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/flask.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/relay_django.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/relay_fastapi.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/relay_flask.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/operation_context.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/probe_capture.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/process_support.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/py.typed +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/queue_admission.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/recovery_capture.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/redaction.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/relay.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/relay_delivery.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/remote_config_client.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/request_capture_policy.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/scope_ownership.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/suppression.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/transport.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/trigger_token.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/dependency_links.txt +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/requires.txt +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/top_level.txt +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_before_send.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_contracts.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_delivery_hooks.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_evidence_capture.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_evidence_wire.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_exception_evidence.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_exception_snapshot.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_framework_integrations.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_hooks.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_http_integration.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_optional_imports.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_privacy_conformance.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_relay.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_relay_delivery.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_relay_evidence.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_remote_config.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_repository_metadata.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_schema_validation.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_sdk.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_smoke_script.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_structlog_filtering.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_suppression.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_transport_reliability.py +0 -0
- {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_trigger_token.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: debugbundle-python
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: DebugBundle SDK for Python
|
|
5
5
|
Author: DebugBundle
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -41,6 +41,8 @@ Dynamic: license-file
|
|
|
41
41
|
|
|
42
42
|
# debugbundle-python
|
|
43
43
|
|
|
44
|
+
Patch 2.1.1 corrects native logger exception evidence and capture safety while preserving public APIs and event schemas. Upgrade the DebugBundle distribution to adopt the correction.
|
|
45
|
+
|
|
44
46
|
Python SDK for DebugBundle.
|
|
45
47
|
|
|
46
48
|
After `make install-docker`, `make evidence-performance` compares identical
|
|
@@ -59,16 +61,15 @@ Use this package to capture Python backend exceptions, request metadata, structu
|
|
|
59
61
|
|
|
60
62
|
Requires Python 3.10 or newer.
|
|
61
63
|
|
|
62
|
-
## Evidence
|
|
64
|
+
## Evidence in 2.1.0
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
Version 2.1.0 adds `capture_exception_evidence=True` and
|
|
65
67
|
`capture_recovery_observations=False` installation options. Collection requires
|
|
66
68
|
verified delivery/reader support, discovered on existing config/ACK traffic with
|
|
67
69
|
`debugbundle_evidence=1`. No capture call performs discovery or adds polling.
|
|
68
70
|
`evidence_capabilities` is an optional descriptor pin asserting the **entire**
|
|
69
71
|
path, including local readers, relays and wrappers; later discovery can revoke it.
|
|
70
|
-
Without proof, exception events retain their legacy shape. These additions
|
|
71
|
-
not available in the installed 2.0.1 release.
|
|
72
|
+
Without proof, exception events retain their legacy shape. These additions require upgrading from 2.0.1 to 2.1.0.
|
|
72
73
|
|
|
73
74
|
Native cause, implicit context and Python 3.11+ exception-group evidence excludes
|
|
74
75
|
locals, source excerpts, arbitrary renderers and application properties. Python
|
|
@@ -89,7 +90,7 @@ Application results and exceptions pass through the context manager unchanged.
|
|
|
89
90
|
The existing background sender remains best-effort at immediate process exit.
|
|
90
91
|
The uncaught hook collects bounded native evidence without waiting for transport;
|
|
91
92
|
applications needing a delivery attempt use explicit `flush()` in a controlled
|
|
92
|
-
shutdown window. This
|
|
93
|
+
shutdown window. This release does not add default crash-file persistence or
|
|
93
94
|
synchronous capture transport.
|
|
94
95
|
|
|
95
96
|
Pass a fresh UUID v4 `failure_ref` in exception/log/request context. A valid
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# debugbundle-python
|
|
2
2
|
|
|
3
|
+
Patch 2.1.1 corrects native logger exception evidence and capture safety while preserving public APIs and event schemas. Upgrade the DebugBundle distribution to adopt the correction.
|
|
4
|
+
|
|
3
5
|
Python SDK for DebugBundle.
|
|
4
6
|
|
|
5
7
|
After `make install-docker`, `make evidence-performance` compares identical
|
|
@@ -18,16 +20,15 @@ Use this package to capture Python backend exceptions, request metadata, structu
|
|
|
18
20
|
|
|
19
21
|
Requires Python 3.10 or newer.
|
|
20
22
|
|
|
21
|
-
## Evidence
|
|
23
|
+
## Evidence in 2.1.0
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
Version 2.1.0 adds `capture_exception_evidence=True` and
|
|
24
26
|
`capture_recovery_observations=False` installation options. Collection requires
|
|
25
27
|
verified delivery/reader support, discovered on existing config/ACK traffic with
|
|
26
28
|
`debugbundle_evidence=1`. No capture call performs discovery or adds polling.
|
|
27
29
|
`evidence_capabilities` is an optional descriptor pin asserting the **entire**
|
|
28
30
|
path, including local readers, relays and wrappers; later discovery can revoke it.
|
|
29
|
-
Without proof, exception events retain their legacy shape. These additions
|
|
30
|
-
not available in the installed 2.0.1 release.
|
|
31
|
+
Without proof, exception events retain their legacy shape. These additions require upgrading from 2.0.1 to 2.1.0.
|
|
31
32
|
|
|
32
33
|
Native cause, implicit context and Python 3.11+ exception-group evidence excludes
|
|
33
34
|
locals, source excerpts, arbitrary renderers and application properties. Python
|
|
@@ -48,7 +49,7 @@ Application results and exceptions pass through the context manager unchanged.
|
|
|
48
49
|
The existing background sender remains best-effort at immediate process exit.
|
|
49
50
|
The uncaught hook collects bounded native evidence without waiting for transport;
|
|
50
51
|
applications needing a delivery attempt use explicit `flush()` in a controlled
|
|
51
|
-
shutdown window. This
|
|
52
|
+
shutdown window. This release does not add default crash-file persistence or
|
|
52
53
|
synchronous capture transport.
|
|
53
54
|
|
|
54
55
|
Pass a fresh UUID v4 `failure_ref` in exception/log/request context. A valid
|
|
@@ -30,14 +30,19 @@ class DebugBundleLogHandler(logging.Handler):
|
|
|
30
30
|
def emit(self, record: logging.LogRecord) -> None:
|
|
31
31
|
if not self._sdk._log_level_eligible(record.levelname.lower()):
|
|
32
32
|
return
|
|
33
|
+
context = {"logger_name": record.name, "pathname": record.pathname, "lineno": record.lineno}
|
|
34
|
+
if (
|
|
35
|
+
record.levelno >= logging.ERROR
|
|
36
|
+
and type(record.exc_info) is tuple
|
|
37
|
+
and len(record.exc_info) == 3
|
|
38
|
+
and isinstance(record.exc_info[1], BaseException)
|
|
39
|
+
):
|
|
40
|
+
self._sdk.capture_exception(record.exc_info[1], {**context, "log_message": safe_log_message(record)})
|
|
41
|
+
return
|
|
33
42
|
self._sdk.capture_log(
|
|
34
43
|
safe_log_message(record),
|
|
35
44
|
level=record.levelname.lower(),
|
|
36
|
-
context=
|
|
37
|
-
"logger_name": record.name,
|
|
38
|
-
"pathname": record.pathname,
|
|
39
|
-
"lineno": record.lineno,
|
|
40
|
-
},
|
|
45
|
+
context=context,
|
|
41
46
|
)
|
|
42
47
|
|
|
43
48
|
|
|
@@ -55,7 +60,10 @@ def safe_log_message(record: logging.LogRecord) -> str:
|
|
|
55
60
|
if type(args) is not tuple or len(args) > 8:
|
|
56
61
|
return template + " [log arguments withheld]"
|
|
57
62
|
safe_args = [
|
|
58
|
-
value[:1024]
|
|
63
|
+
value[:1024]
|
|
64
|
+
if type(value) is str
|
|
65
|
+
else str(value)
|
|
66
|
+
if type(value) in (int, float, bool)
|
|
59
67
|
else "[unsupported argument]"
|
|
60
68
|
for value in args
|
|
61
69
|
]
|
|
@@ -14,6 +14,7 @@ from .event_support import (
|
|
|
14
14
|
runtime_process_facts,
|
|
15
15
|
)
|
|
16
16
|
from .exception_evidence import snapshot_exception
|
|
17
|
+
from .exception_identity import admit as admit_identity
|
|
17
18
|
from .exception_snapshot import exception_details
|
|
18
19
|
from .process_support import ensure_current_process
|
|
19
20
|
from .queue_admission import may_prepare_event
|
|
@@ -35,6 +36,9 @@ def capture_exception(
|
|
|
35
36
|
if not sdk._enabled or not may_prepare_event(sdk, True, max_pending_events, max_pending_bytes):
|
|
36
37
|
return
|
|
37
38
|
try:
|
|
39
|
+
automatic = fatal or type(context) is dict and "logger_name" in context
|
|
40
|
+
if not admit_identity(sdk, error, automatic):
|
|
41
|
+
return
|
|
38
42
|
name, message, stack = exception_details(error)
|
|
39
43
|
redacted_context = redact_mapping(dict(context or {}), sdk._redact_fields)
|
|
40
44
|
request_payload = backend_exception_request_payload(redacted_context.get("request"))
|
|
@@ -84,8 +88,10 @@ def capture_exception(
|
|
|
84
88
|
return
|
|
85
89
|
try:
|
|
86
90
|
if sdk._enabled and sdk._suppression.should_capture(suppression_key, sdk._time_provider()):
|
|
87
|
-
if sdk._offer_protected_event(event, event_bytes)
|
|
88
|
-
sdk
|
|
91
|
+
if sdk._offer_protected_event(event, event_bytes):
|
|
92
|
+
admit_identity(sdk, error, automatic, accepted=True)
|
|
93
|
+
if fatal:
|
|
94
|
+
sdk._hook_bypass.add(id(event))
|
|
89
95
|
finally:
|
|
90
96
|
sdk._lock.release()
|
|
91
97
|
except BaseException:
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Opaque receipts on native exceptions; no raw exception retention or equality hooks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
_SLOT = "_debugbundle_capture_receipt"
|
|
9
|
+
_DICT = BaseException.__dict__["__dict__"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def admit(sdk: Any, error: BaseException, automatic: bool, *, accepted: bool = False) -> bool:
|
|
13
|
+
try:
|
|
14
|
+
# BaseException is not universally weak-referenceable. Store only an opaque
|
|
15
|
+
# owner, operation ID and time on its native dictionary, bypassing overrides.
|
|
16
|
+
if sdk._before_send is not None:
|
|
17
|
+
return True
|
|
18
|
+
fields = _DICT.__get__(error)
|
|
19
|
+
owner = getattr(sdk, "_exception_identity_owner", None)
|
|
20
|
+
if owner is None:
|
|
21
|
+
owner = sdk._exception_identity_owner = object()
|
|
22
|
+
operation = sdk._operations.current()
|
|
23
|
+
scope = operation.scope["operation_id"] if operation is not None else None
|
|
24
|
+
if scope is None:
|
|
25
|
+
return True
|
|
26
|
+
now = time.monotonic()
|
|
27
|
+
previous = fields.get(_SLOT)
|
|
28
|
+
if type(previous) is tuple and len(previous) == 4:
|
|
29
|
+
token, prior_scope, at, prior_automatic = previous
|
|
30
|
+
if (
|
|
31
|
+
not accepted
|
|
32
|
+
and token is owner
|
|
33
|
+
and type(prior_scope) is str
|
|
34
|
+
and scope == prior_scope
|
|
35
|
+
and type(at) is float
|
|
36
|
+
and type(prior_automatic) is bool
|
|
37
|
+
and now - at < 30
|
|
38
|
+
and automatic != prior_automatic
|
|
39
|
+
):
|
|
40
|
+
fields.pop(_SLOT, None)
|
|
41
|
+
return False
|
|
42
|
+
if accepted:
|
|
43
|
+
fields[_SLOT] = (owner, scope, now, automatic)
|
|
44
|
+
except BaseException:
|
|
45
|
+
# Unavailable identity must preserve the failure, never infer sameness.
|
|
46
|
+
pass
|
|
47
|
+
return True
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/logger_integrations.py
RENAMED
|
@@ -75,11 +75,27 @@ def _attach_loguru(
|
|
|
75
75
|
return None
|
|
76
76
|
|
|
77
77
|
try:
|
|
78
|
+
|
|
78
79
|
def sink(message: Any) -> None:
|
|
79
80
|
record = getattr(message, "record", None)
|
|
80
81
|
if not isinstance(record, dict):
|
|
81
82
|
return
|
|
82
83
|
context = dict(record.get("extra") or {})
|
|
84
|
+
exception = record.get("exception")
|
|
85
|
+
error = exception.value if exception is not None else None
|
|
86
|
+
level = _normalize_level(str(getattr(record.get("level"), "name", "warning")))
|
|
87
|
+
if (
|
|
88
|
+
level in {"error", "critical"}
|
|
89
|
+
and isinstance(error, BaseException)
|
|
90
|
+
and hasattr(sdk, "capture_exception")
|
|
91
|
+
):
|
|
92
|
+
eligible = getattr(sdk, "_log_level_eligible", None)
|
|
93
|
+
if callable(eligible) and not eligible(level):
|
|
94
|
+
return
|
|
95
|
+
sdk.capture_exception(
|
|
96
|
+
error, {**context, "logger_name": "loguru", "log_message": str(record.get("message") or "")}
|
|
97
|
+
)
|
|
98
|
+
return
|
|
83
99
|
sdk.capture_log(
|
|
84
100
|
str(record.get("message") or ""),
|
|
85
101
|
level=_normalize_level(str(getattr(record.get("level"), "name", "warning"))),
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/structlog_integration.py
RENAMED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import copy
|
|
4
4
|
import inspect
|
|
5
5
|
import logging
|
|
6
|
+
import sys
|
|
6
7
|
from contextvars import ContextVar
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
@@ -19,6 +20,17 @@ def _capture(sdk: Any, active: list[bool], name: str, record: dict[str, Any]) ->
|
|
|
19
20
|
context = dict(record)
|
|
20
21
|
message = str(context.pop("event", ""))
|
|
21
22
|
level = {"warn": "warning", "exception": "error", "fatal": "critical"}.get(name, name)
|
|
23
|
+
info = context.get("exc_info")
|
|
24
|
+
if info is True:
|
|
25
|
+
info = sys.exc_info()
|
|
26
|
+
error = info[1] if type(info) is tuple and len(info) == 3 else info
|
|
27
|
+
if level in {"error", "critical"} and isinstance(error, BaseException) and hasattr(sdk, "capture_exception"):
|
|
28
|
+
eligible = getattr(sdk, "_log_level_eligible", None)
|
|
29
|
+
if callable(eligible) and not eligible(level):
|
|
30
|
+
return
|
|
31
|
+
context.pop("exc_info", None)
|
|
32
|
+
sdk.capture_exception(error, {**context, "logger_name": "structlog", "log_message": message})
|
|
33
|
+
return
|
|
22
34
|
sdk.capture_log(message, level=level, context=context or None)
|
|
23
35
|
except Exception:
|
|
24
36
|
# A logger adapter must not throw into the application's logging path.
|
|
@@ -61,8 +73,10 @@ class StructlogLoggerProxy:
|
|
|
61
73
|
if not callable(attribute):
|
|
62
74
|
return attribute
|
|
63
75
|
if name in _BIND_METHODS:
|
|
76
|
+
|
|
64
77
|
def bind(*args: Any, **kwargs: Any) -> StructlogLoggerProxy:
|
|
65
78
|
return StructlogLoggerProxy(attribute(*args, **kwargs), self._sdk, self._active)
|
|
79
|
+
|
|
66
80
|
return bind
|
|
67
81
|
method_name = name[1:] if name.startswith("a") else name
|
|
68
82
|
if method_name not in _LOG_METHODS:
|
|
@@ -84,6 +98,7 @@ class StructlogLoggerProxy:
|
|
|
84
98
|
if args:
|
|
85
99
|
record["event"] = args[1] if method_name == "log" and len(args) > 1 else args[0]
|
|
86
100
|
processors = list(bound._processors)
|
|
101
|
+
native_info = kwargs.get("exc_info", sys.exc_info() if method_name == "exception" else None)
|
|
87
102
|
if processors:
|
|
88
103
|
last = processors[-1]
|
|
89
104
|
|
|
@@ -96,6 +111,8 @@ class StructlogLoggerProxy:
|
|
|
96
111
|
if isinstance(snapshot, dict):
|
|
97
112
|
record.clear()
|
|
98
113
|
record.update(snapshot)
|
|
114
|
+
if native_info is not None:
|
|
115
|
+
record["exc_info"] = native_info
|
|
99
116
|
return result
|
|
100
117
|
|
|
101
118
|
processors[-1] = observe
|
|
@@ -107,12 +124,15 @@ class StructlogLoggerProxy:
|
|
|
107
124
|
return attribute
|
|
108
125
|
|
|
109
126
|
if inspect.iscoroutinefunction(attribute):
|
|
127
|
+
|
|
110
128
|
async def async_log(*args: Any, **kwargs: Any) -> Any:
|
|
111
129
|
return await prepare(args, kwargs)(*args, **kwargs)
|
|
130
|
+
|
|
112
131
|
return async_log
|
|
113
132
|
|
|
114
133
|
def log(*args: Any, **kwargs: Any) -> Any:
|
|
115
134
|
return prepare(args, kwargs)(*args, **kwargs)
|
|
135
|
+
|
|
116
136
|
return log
|
|
117
137
|
|
|
118
138
|
def _ensure_stdlib_handler(self, sink: Any) -> None:
|
|
@@ -136,4 +156,5 @@ class StructlogLoggerProxy:
|
|
|
136
156
|
record = {**kwargs, **{f"arg_{i}": value for i, value in enumerate(args)}, "event": event or ""}
|
|
137
157
|
_capture(self._sdk, self._active, name, record)
|
|
138
158
|
return result
|
|
159
|
+
|
|
139
160
|
return log
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: debugbundle-python
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: DebugBundle SDK for Python
|
|
5
5
|
Author: DebugBundle
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -41,6 +41,8 @@ Dynamic: license-file
|
|
|
41
41
|
|
|
42
42
|
# debugbundle-python
|
|
43
43
|
|
|
44
|
+
Patch 2.1.1 corrects native logger exception evidence and capture safety while preserving public APIs and event schemas. Upgrade the DebugBundle distribution to adopt the correction.
|
|
45
|
+
|
|
44
46
|
Python SDK for DebugBundle.
|
|
45
47
|
|
|
46
48
|
After `make install-docker`, `make evidence-performance` compares identical
|
|
@@ -59,16 +61,15 @@ Use this package to capture Python backend exceptions, request metadata, structu
|
|
|
59
61
|
|
|
60
62
|
Requires Python 3.10 or newer.
|
|
61
63
|
|
|
62
|
-
## Evidence
|
|
64
|
+
## Evidence in 2.1.0
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
Version 2.1.0 adds `capture_exception_evidence=True` and
|
|
65
67
|
`capture_recovery_observations=False` installation options. Collection requires
|
|
66
68
|
verified delivery/reader support, discovered on existing config/ACK traffic with
|
|
67
69
|
`debugbundle_evidence=1`. No capture call performs discovery or adds polling.
|
|
68
70
|
`evidence_capabilities` is an optional descriptor pin asserting the **entire**
|
|
69
71
|
path, including local readers, relays and wrappers; later discovery can revoke it.
|
|
70
|
-
Without proof, exception events retain their legacy shape. These additions
|
|
71
|
-
not available in the installed 2.0.1 release.
|
|
72
|
+
Without proof, exception events retain their legacy shape. These additions require upgrading from 2.0.1 to 2.1.0.
|
|
72
73
|
|
|
73
74
|
Native cause, implicit context and Python 3.11+ exception-group evidence excludes
|
|
74
75
|
locals, source excerpts, arbitrary renderers and application properties. Python
|
|
@@ -89,7 +90,7 @@ Application results and exceptions pass through the context manager unchanged.
|
|
|
89
90
|
The existing background sender remains best-effort at immediate process exit.
|
|
90
91
|
The uncaught hook collects bounded native evidence without waiting for transport;
|
|
91
92
|
applications needing a delivery attempt use explicit `flush()` in a controlled
|
|
92
|
-
shutdown window. This
|
|
93
|
+
shutdown window. This release does not add default crash-file persistence or
|
|
93
94
|
synchronous capture transport.
|
|
94
95
|
|
|
95
96
|
Pass a fresh UUID v4 `failure_ref` in exception/log/request context. A valid
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/SOURCES.txt
RENAMED
|
@@ -18,6 +18,7 @@ src/debugbundle/evidence_json.py
|
|
|
18
18
|
src/debugbundle/evidence_wire.py
|
|
19
19
|
src/debugbundle/exception_capture.py
|
|
20
20
|
src/debugbundle/exception_evidence.py
|
|
21
|
+
src/debugbundle/exception_identity.py
|
|
21
22
|
src/debugbundle/exception_snapshot.py
|
|
22
23
|
src/debugbundle/flush_aggregates.py
|
|
23
24
|
src/debugbundle/flush_delivery.py
|
|
@@ -62,6 +63,7 @@ tests/test_exception_snapshot.py
|
|
|
62
63
|
tests/test_framework_integrations.py
|
|
63
64
|
tests/test_hooks.py
|
|
64
65
|
tests/test_http_integration.py
|
|
66
|
+
tests/test_logging_exception.py
|
|
65
67
|
tests/test_optional_imports.py
|
|
66
68
|
tests/test_privacy_conformance.py
|
|
67
69
|
tests/test_relay.py
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import structlog
|
|
7
|
+
|
|
8
|
+
from debugbundle.capture_hooks import DebugBundleLogHandler
|
|
9
|
+
from debugbundle.core import DebugBundleSdk
|
|
10
|
+
from debugbundle.transport import TransportResponse
|
|
11
|
+
|
|
12
|
+
_ORIGINAL_GET_LOGGER = structlog.get_logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.parametrize("drop_first", [False, True])
|
|
16
|
+
def test_repeated_native_records_and_capture_after_rejected_hook(drop_first):
|
|
17
|
+
events = []
|
|
18
|
+
calls = []
|
|
19
|
+
|
|
20
|
+
def hook(event):
|
|
21
|
+
calls.append(event)
|
|
22
|
+
return None if len(calls) == 1 else event
|
|
23
|
+
|
|
24
|
+
sdk = DebugBundleSdk(transport=lambda request: (events.extend(request["events"]), TransportResponse(202))[1])
|
|
25
|
+
sdk.init("synthetic-token", batch_size=1000, flush_interval=3600, before_send=hook if drop_first else None)
|
|
26
|
+
error = RuntimeError("reused object")
|
|
27
|
+
try:
|
|
28
|
+
with sdk.operation_context({"kind": "request"}):
|
|
29
|
+
sdk.capture_exception(error, context={"logger_name": "synthetic"})
|
|
30
|
+
sdk.flush()
|
|
31
|
+
sdk.capture_exception(error, context={} if drop_first else {"logger_name": "synthetic"})
|
|
32
|
+
sdk.flush()
|
|
33
|
+
assert len(events) == (1 if drop_first else 2)
|
|
34
|
+
finally:
|
|
35
|
+
sdk.dispose()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_stdlib_exception_preserves_traceback_and_cause_without_frame_logs():
|
|
39
|
+
events = []
|
|
40
|
+
|
|
41
|
+
def send(request):
|
|
42
|
+
events.extend(request["events"])
|
|
43
|
+
return TransportResponse(202)
|
|
44
|
+
|
|
45
|
+
sdk = DebugBundleSdk(transport=send)
|
|
46
|
+
sdk.init("synthetic-token", batch_size=1000, flush_interval=3600)
|
|
47
|
+
logger = logging.Logger("synthetic", level=logging.ERROR)
|
|
48
|
+
logger.addHandler(DebugBundleLogHandler(sdk))
|
|
49
|
+
try:
|
|
50
|
+
try:
|
|
51
|
+
try:
|
|
52
|
+
raise ValueError("synthetic cause")
|
|
53
|
+
except ValueError as cause:
|
|
54
|
+
raise RuntimeError("synthetic outer") from cause
|
|
55
|
+
except RuntimeError:
|
|
56
|
+
logger.exception("operation failed")
|
|
57
|
+
logger.error("ordinary error")
|
|
58
|
+
sdk.flush()
|
|
59
|
+
assert [event["event_type"] for event in events] == ["backend_exception", "log_event"]
|
|
60
|
+
assert "synthetic cause" in events[0]["payload"]["stack"]
|
|
61
|
+
assert "test_stdlib_exception" in events[0]["payload"]["stack"]
|
|
62
|
+
finally:
|
|
63
|
+
sdk.dispose()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_logger_and_explicit_capture_share_identity_only_inside_owned_operation():
|
|
67
|
+
events = []
|
|
68
|
+
sdk = DebugBundleSdk(transport=lambda request: (events.extend(request["events"]), TransportResponse(202))[1])
|
|
69
|
+
sdk.init("synthetic-token", batch_size=1000, flush_interval=3600)
|
|
70
|
+
logger = logging.Logger("synthetic", level=logging.ERROR)
|
|
71
|
+
logger.addHandler(DebugBundleLogHandler(sdk))
|
|
72
|
+
error = RuntimeError("same object")
|
|
73
|
+
try:
|
|
74
|
+
for _ in range(2):
|
|
75
|
+
with sdk.operation_context({"kind": "request"}):
|
|
76
|
+
logger.error("failed", exc_info=(RuntimeError, error, None))
|
|
77
|
+
sdk.capture_exception(error)
|
|
78
|
+
sdk.flush()
|
|
79
|
+
assert len(events) == 2
|
|
80
|
+
finally:
|
|
81
|
+
sdk.dispose()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_loguru_and_structlog_preserve_native_cause_after_formatters(monkeypatch):
|
|
85
|
+
import structlog
|
|
86
|
+
from loguru import logger
|
|
87
|
+
|
|
88
|
+
events = []
|
|
89
|
+
sdk = DebugBundleSdk(transport=lambda request: (events.extend(request["events"]), TransportResponse(202))[1])
|
|
90
|
+
sdk.init("synthetic-token", batch_size=1000, flush_interval=3600)
|
|
91
|
+
previous = structlog.get_config()
|
|
92
|
+
monkeypatch.setattr(structlog, "get_logger", _ORIGINAL_GET_LOGGER)
|
|
93
|
+
structlog.configure(
|
|
94
|
+
processors=[structlog.processors.format_exc_info, structlog.processors.JSONRenderer()],
|
|
95
|
+
logger_factory=structlog.ReturnLoggerFactory(),
|
|
96
|
+
)
|
|
97
|
+
sdk.capture_logging(logger=logging.Logger("synthetic-integrations"))
|
|
98
|
+
try:
|
|
99
|
+
try:
|
|
100
|
+
raise ValueError("native cause")
|
|
101
|
+
except ValueError:
|
|
102
|
+
logger.exception("loguru failure")
|
|
103
|
+
structlog.get_logger().exception("structlog failure")
|
|
104
|
+
sdk.flush()
|
|
105
|
+
assert len(events) == 2
|
|
106
|
+
assert all(event["event_type"] == "backend_exception" for event in events)
|
|
107
|
+
assert all("native cause" in event["payload"]["stack"] for event in events)
|
|
108
|
+
finally:
|
|
109
|
+
structlog.configure(**previous)
|
|
110
|
+
sdk.dispose()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/event-envelope.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/__init__.py
RENAMED
|
File without changes
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/common.py
RENAMED
|
File without changes
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/django.py
RENAMED
|
File without changes
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/fastapi.py
RENAMED
|
File without changes
|
|
File without changes
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/relay_django.py
RENAMED
|
File without changes
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/relay_fastapi.py
RENAMED
|
File without changes
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/relay_flask.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/remote_config_client.py
RENAMED
|
File without changes
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/request_capture_policy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|