debugbundle-python 1.4.0__py3-none-any.whl → 1.4.1__py3-none-any.whl

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.
@@ -264,7 +264,7 @@ def sdk_version() -> str:
264
264
  try:
265
265
  return metadata.version("debugbundle-python")
266
266
  except metadata.PackageNotFoundError:
267
- return "1.4.0"
267
+ return "1.4.1"
268
268
 
269
269
 
270
270
  def sdk_config_endpoint(events_endpoint: str) -> str:
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  from collections.abc import Callable
4
4
  from typing import Any, Protocol
5
5
 
6
+ from .structlog_integration import StructlogLoggerProxy as _StructlogLoggerProxy
7
+
6
8
 
7
9
  class LogCaptureApi(Protocol):
8
10
  def capture_log(
@@ -44,14 +46,18 @@ def _attach_structlog(
44
46
  if getattr(original_get_logger, "__debugbundle_structlog_wrapper__", False):
45
47
  return None
46
48
 
49
+ active = [True]
50
+
47
51
  def get_logger(*args: Any, **kwargs: Any) -> Any:
48
- return _StructlogLoggerProxy(original_get_logger(*args, **kwargs), sdk)
52
+ return _StructlogLoggerProxy(original_get_logger(*args, **kwargs), sdk, active)
49
53
 
50
54
  setattr(get_logger, "__debugbundle_structlog_wrapper__", True)
51
55
  structlog.get_logger = get_logger
52
56
 
53
57
  def restore() -> None:
54
- structlog.get_logger = original_get_logger
58
+ active[0] = False
59
+ if structlog.get_logger is get_logger:
60
+ structlog.get_logger = original_get_logger
55
61
 
56
62
  return restore
57
63
  except Exception as error:
@@ -100,40 +106,6 @@ def _normalize_level(level: str) -> str:
100
106
  return normalized
101
107
 
102
108
 
103
- class _StructlogLoggerProxy:
104
- def __init__(self, logger: Any, sdk: LogCaptureApi) -> None:
105
- self._logger = logger
106
- self._sdk = sdk
107
-
108
- def bind(self, *args: Any, **kwargs: Any) -> _StructlogLoggerProxy:
109
- return _StructlogLoggerProxy(self._logger.bind(*args, **kwargs), self._sdk)
110
-
111
- def new(self, *args: Any, **kwargs: Any) -> _StructlogLoggerProxy:
112
- return _StructlogLoggerProxy(self._logger.new(*args, **kwargs), self._sdk)
113
-
114
- def __getattr__(self, name: str) -> Any:
115
- attribute = getattr(self._logger, name)
116
- if name not in {
117
- "debug",
118
- "info",
119
- "warning",
120
- "warn",
121
- "error",
122
- "critical",
123
- "exception",
124
- } or not callable(attribute):
125
- return attribute
126
-
127
- def wrapped(event: Any = None, *args: Any, **kwargs: Any) -> Any:
128
- context = dict(kwargs)
129
- for index, value in enumerate(args):
130
- context[f"arg_{index}"] = value
131
- self._sdk.capture_log(str(event or ""), level=_normalize_level(name), context=context or None)
132
- return attribute(event, *args, **kwargs)
133
-
134
- return wrapped
135
-
136
-
137
109
  def _emit_diagnostic(
138
110
  on_diagnostic: Callable[[dict[str, object]], None] | None,
139
111
  error: Exception,
@@ -151,4 +123,4 @@ def _emit_diagnostic(
151
123
  }
152
124
  },
153
125
  }
154
- )
126
+ )
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import inspect
5
+ import logging
6
+ from contextvars import ContextVar
7
+ from typing import Any
8
+
9
+ _CAPTURING: ContextVar[bool] = ContextVar("debugbundle_structlog_capture", default=False)
10
+ _LOG_METHODS = {"debug", "info", "warning", "warn", "error", "critical", "fatal", "exception", "log"}
11
+ _BIND_METHODS = {"bind", "new", "unbind", "try_unbind"}
12
+
13
+
14
+ def _capture(sdk: Any, active: list[bool], name: str, record: dict[str, Any]) -> None:
15
+ if not active[0] or _CAPTURING.get():
16
+ return
17
+ token = _CAPTURING.set(True)
18
+ try:
19
+ context = dict(record)
20
+ message = str(context.pop("event", ""))
21
+ level = {"warn": "warning", "exception": "error", "fatal": "critical"}.get(name, name)
22
+ sdk.capture_log(message, level=level, context=context or None)
23
+ except Exception:
24
+ # A logger adapter must not throw into the application's logging path.
25
+ pass
26
+ finally:
27
+ _CAPTURING.reset(token)
28
+
29
+
30
+ class _SinkProxy:
31
+ def __init__(self, sink: Any, sdk: Any, active: list[bool], record: dict[str, Any]) -> None:
32
+ self._sink = sink
33
+ self._sdk = sdk
34
+ self._active = active
35
+ self._record = record
36
+
37
+ def __getattr__(self, name: str) -> Any:
38
+ method = getattr(self._sink, name)
39
+ if name not in _LOG_METHODS or not callable(method):
40
+ return method
41
+
42
+ def emit(*args: Any, **kwargs: Any) -> Any:
43
+ result = method(*args, **kwargs)
44
+ # stdlib records go through the existing SDK logging.Handler, after
45
+ # Logger.disabled, logging.disable(), levels and logger filters.
46
+ if not isinstance(self._sink, (logging.Logger, logging.LoggerAdapter)):
47
+ _capture(self._sdk, self._active, name, self._record)
48
+ return result
49
+
50
+ return emit
51
+
52
+
53
+ class StructlogLoggerProxy:
54
+ def __init__(self, logger: Any, sdk: Any, active: list[bool] | None = None) -> None:
55
+ self._logger = logger
56
+ self._sdk = sdk
57
+ self._active = active if active is not None else [True]
58
+
59
+ def __getattr__(self, name: str) -> Any:
60
+ attribute = getattr(self._logger, name)
61
+ if not callable(attribute):
62
+ return attribute
63
+ if name in _BIND_METHODS:
64
+ def bind(*args: Any, **kwargs: Any) -> StructlogLoggerProxy:
65
+ return StructlogLoggerProxy(attribute(*args, **kwargs), self._sdk, self._active)
66
+ return bind
67
+ method_name = name[1:] if name.startswith("a") else name
68
+ if method_name not in _LOG_METHODS:
69
+ return attribute
70
+
71
+ def prepare(args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
72
+ if not self._active[0] or _CAPTURING.get():
73
+ return attribute
74
+ try:
75
+ bound = self._logger.bind() if hasattr(self._logger, "bind") else self._logger
76
+ if not hasattr(bound, "_processors") or not hasattr(bound, "_logger"):
77
+ return self._legacy_method(attribute, method_name)
78
+ # Keep the application's bound logger and processor list intact.
79
+ # The per-call clone also isolates concurrent/async log records.
80
+ clone = copy.copy(bound)
81
+ self._ensure_stdlib_handler(bound._logger)
82
+ record = dict(bound._context)
83
+ record.update(kwargs)
84
+ if args:
85
+ record["event"] = args[1] if method_name == "log" and len(args) > 1 else args[0]
86
+ processors = list(bound._processors)
87
+ if processors:
88
+ last = processors[-1]
89
+
90
+ def observe(logger: Any, level: str, event: Any) -> Any:
91
+ result = last(logger, level, event)
92
+ snapshot = result if isinstance(result, dict) else event
93
+ if isinstance(snapshot, dict):
94
+ record.clear()
95
+ record.update(snapshot)
96
+ return result
97
+
98
+ processors[-1] = observe
99
+ clone._processors = processors
100
+ clone._logger = _SinkProxy(bound._logger, self._sdk, self._active, record)
101
+ return getattr(clone, name)
102
+ except Exception:
103
+ # Preparation can fail for a custom logger; call it exactly once.
104
+ return attribute
105
+
106
+ if inspect.iscoroutinefunction(attribute):
107
+ async def async_log(*args: Any, **kwargs: Any) -> Any:
108
+ return await prepare(args, kwargs)(*args, **kwargs)
109
+ return async_log
110
+
111
+ def log(*args: Any, **kwargs: Any) -> Any:
112
+ return prepare(args, kwargs)(*args, **kwargs)
113
+ return log
114
+
115
+ def _ensure_stdlib_handler(self, sink: Any) -> None:
116
+ logger = sink.logger if isinstance(sink, logging.LoggerAdapter) else sink
117
+ if not isinstance(logger, logging.Logger):
118
+ return
119
+ current: logging.Logger | None = logger
120
+ while current is not None:
121
+ if any(getattr(handler, "_sdk", None) is self._sdk for handler in current.handlers):
122
+ return
123
+ if not current.propagate:
124
+ break
125
+ current = current.parent
126
+ attach = getattr(self._sdk, "capture_logging", None)
127
+ if callable(attach):
128
+ attach(logger=logger)
129
+
130
+ def _legacy_method(self, attribute: Any, name: str) -> Any:
131
+ def log(event: Any = None, *args: Any, **kwargs: Any) -> Any:
132
+ result = attribute(event, *args, **kwargs)
133
+ record = {**kwargs, **{f"arg_{i}": value for i, value in enumerate(args)}, "event": event or ""}
134
+ _capture(self._sdk, self._active, name, record)
135
+ return result
136
+ return log
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: debugbundle-python
3
- Version: 1.4.0
3
+ Version: 1.4.1
4
4
  Summary: DebugBundle SDK for Python
5
5
  Author: DebugBundle
6
6
  License-Expression: Apache-2.0
@@ -49,6 +49,10 @@ Use this package to capture Python backend exceptions, request metadata, structu
49
49
 
50
50
  Requires Python 3.10 or newer.
51
51
 
52
+ ## Automatic capture and application filtering
53
+
54
+ `capture_logging()` attaches the stdlib logging handler and detects installed structlog/loguru integrations. Obtain structlog loggers after attachment. Structlog level filters and `DropEvent` processors run before capture; stdlib-backed structlog loggers also honor logger filters, disabled state and `logging.disable()`. Bound context and async logging are supported. SDK disposal stops capture through cached proxies. DebugBundle has its own minimum capture level; destination-specific filters on other handlers or Loguru sinks do not configure the DebugBundle handler.
55
+
52
56
  ## Installation
53
57
 
54
58
  ```bash
@@ -3,12 +3,13 @@ debugbundle/acknowledgement.py,sha256=tyI8XLRG9N3-Weo6uiGrAlf_O4tjptVe1x1cG7RjrM
3
3
  debugbundle/before_send.py,sha256=_tmV6ovHUBIxfQPMQ8a0C6r91IAnd4DVs8UcjsKhqX0,7792
4
4
  debugbundle/config.py,sha256=ENRSR5jUI7xYdBqUakWws3GIjBk00GWFmwXVE_iQp2Y,9296
5
5
  debugbundle/core.py,sha256=ox4fNADe2kRic9aUePB_RJmT81vCQ9SFW6PaCSL-YL0,32888
6
- debugbundle/event_support.py,sha256=hg_GFyj_9nesQ5oOzvW-TmHI6R2E-6WGHsBp-MhYBOQ,9046
7
- debugbundle/logger_integrations.py,sha256=RuTNaD9RRVmiE-BBkksAXWVEGaMzLrWavVpQdgGZBpE,4564
6
+ debugbundle/event_support.py,sha256=vSiD8HoMPSKnXIlc-S5ZtnGllH9al79nLc04RX-eywY,9046
7
+ debugbundle/logger_integrations.py,sha256=T6LR10_DgDS8C-CUPxtC1kZz3qfX5j0B2YUV7LxDovk,3572
8
8
  debugbundle/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  debugbundle/redaction.py,sha256=QTaPkSsYv54yR1mz8SB6Q4vWjvp7Ddcui4WXaH2RVz8,736
10
10
  debugbundle/relay.py,sha256=p5-tFXsgH8TZfB7Va3C2XFJsi8mYOMnGjr01W_kApME,14366
11
11
  debugbundle/relay_delivery.py,sha256=VL-nIgJR6mYXqKy-EbA0USWeY_iS_uAr1KBzrnwBnnk,4676
12
+ debugbundle/structlog_integration.py,sha256=fIrv5U2TrbYURsEt_Bne-JuW3xLdgpNyM3RyBNp7RyU,5794
12
13
  debugbundle/suppression.py,sha256=XMn0GfF_-WZk2wHWk3KfbO5_u5QhEunues5h3jOInLs,4098
13
14
  debugbundle/transport.py,sha256=V289FwFWL6KV5gyzOsesluEMbpRobm_FHu3x8BpOYKU,2014
14
15
  debugbundle/trigger_token.py,sha256=YUwIWnnxu1klAVaB8Ltgk_PwWW_PPjq9rzmEbWXeFeM,4455
@@ -20,8 +21,8 @@ debugbundle/integrations/flask.py,sha256=cgyHbsAH96gpNPf_5IGJYzp2va28JdDgXROBsF-
20
21
  debugbundle/integrations/relay_django.py,sha256=Wj8BE9D5otU2KmtjgqdKIJ-BvXVcagCKgpLYvnPZ6WE,2281
21
22
  debugbundle/integrations/relay_fastapi.py,sha256=-Zl6bvhYMLii__mP6-UvSdwxS5VKsb9OZvk-yy-3hEw,2227
22
23
  debugbundle/integrations/relay_flask.py,sha256=VFHkDTJy4LkZzL_Ry_Ba-e_gW6UTG4PBwnLcvB_oXuQ,2011
23
- debugbundle_python-1.4.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
24
- debugbundle_python-1.4.0.dist-info/METADATA,sha256=aFboU5dUpifzAC0Hl3boTfOanvIenWq6IBN9n0Zuavw,13831
25
- debugbundle_python-1.4.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
26
- debugbundle_python-1.4.0.dist-info/top_level.txt,sha256=RCB9STTFnl1OKdojxz-xhaks2zkRFs1meZXsKnm18LM,12
27
- debugbundle_python-1.4.0.dist-info/RECORD,,
24
+ debugbundle_python-1.4.1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
25
+ debugbundle_python-1.4.1.dist-info/METADATA,sha256=565SQt1hHG6Unoj5Cj7Pa60AsMgs097_XnQSg7VnxuI,14452
26
+ debugbundle_python-1.4.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
27
+ debugbundle_python-1.4.1.dist-info/top_level.txt,sha256=RCB9STTFnl1OKdojxz-xhaks2zkRFs1meZXsKnm18LM,12
28
+ debugbundle_python-1.4.1.dist-info/RECORD,,