debugbundle-python 1.4.2__py3-none-any.whl → 2.0.0__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.
- debugbundle/capture_hooks.py +65 -0
- debugbundle/core.py +379 -251
- debugbundle/delivery_hooks.py +62 -0
- debugbundle/event_support.py +1 -1
- debugbundle/exception_snapshot.py +74 -0
- debugbundle/flush_aggregates.py +37 -0
- debugbundle/probe_capture.py +101 -0
- debugbundle/process_support.py +59 -0
- debugbundle/queue_admission.py +39 -0
- debugbundle/redaction.py +197 -0
- debugbundle/relay.py +8 -0
- debugbundle/request_capture_policy.py +45 -0
- debugbundle/suppression.py +13 -6
- {debugbundle_python-1.4.2.dist-info → debugbundle_python-2.0.0.dist-info}/METADATA +12 -5
- {debugbundle_python-1.4.2.dist-info → debugbundle_python-2.0.0.dist-info}/RECORD +18 -10
- {debugbundle_python-1.4.2.dist-info → debugbundle_python-2.0.0.dist-info}/WHEEL +0 -0
- {debugbundle_python-1.4.2.dist-info → debugbundle_python-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {debugbundle_python-1.4.2.dist-info → debugbundle_python-2.0.0.dist-info}/top_level.txt +0 -0
debugbundle/core.py
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import json
|
|
4
5
|
import logging
|
|
6
|
+
import os
|
|
5
7
|
import sys
|
|
6
8
|
import threading
|
|
7
|
-
import
|
|
9
|
+
import time
|
|
8
10
|
import uuid
|
|
9
11
|
from collections import deque
|
|
10
12
|
from collections.abc import Callable, Mapping
|
|
11
13
|
from contextvars import ContextVar, Token
|
|
12
|
-
from dataclasses import dataclass
|
|
13
14
|
from random import random
|
|
14
15
|
from typing import Any, Protocol, cast
|
|
15
16
|
|
|
16
17
|
from .acknowledgement import decide_acknowledgement
|
|
17
18
|
from .before_send import BeforeSendHook, apply_before_send
|
|
19
|
+
from .capture_hooks import DebugBundleLogHandler, try_capture_lock
|
|
18
20
|
from .config import (
|
|
19
21
|
BALANCED_CAPTURE_POLICY,
|
|
20
22
|
DEFAULT_PROBES_POLL_INTERVAL_MS,
|
|
@@ -25,6 +27,7 @@ from .config import (
|
|
|
25
27
|
find_matching_remote_probe_directives,
|
|
26
28
|
parse_remote_config,
|
|
27
29
|
)
|
|
30
|
+
from .delivery_hooks import finalize_batch
|
|
28
31
|
from .event_support import (
|
|
29
32
|
DEFAULT_LOG_LEVEL,
|
|
30
33
|
LEVEL_RANKS,
|
|
@@ -32,7 +35,6 @@ from .event_support import (
|
|
|
32
35
|
backend_exception_response_payload,
|
|
33
36
|
correlation_payload,
|
|
34
37
|
event_context,
|
|
35
|
-
is_immediate_request_incident_status,
|
|
36
38
|
iso_now,
|
|
37
39
|
level_enabled,
|
|
38
40
|
normalize_level,
|
|
@@ -44,8 +46,20 @@ from .event_support import (
|
|
|
44
46
|
serialize_error,
|
|
45
47
|
time_now,
|
|
46
48
|
)
|
|
49
|
+
from .exception_snapshot import exception_details
|
|
50
|
+
from .flush_aggregates import prepare_flush_aggregates
|
|
47
51
|
from .logger_integrations import attach_optional_integrations
|
|
48
|
-
from .
|
|
52
|
+
from .probe_capture import ProbeEntry, capture_probe
|
|
53
|
+
from .process_support import ensure_current_process
|
|
54
|
+
from .queue_admission import evict_low_priority, high_priority_event, may_prepare_event
|
|
55
|
+
from .redaction import (
|
|
56
|
+
DEFAULT_REDACT_FIELDS,
|
|
57
|
+
UnsafeTelemetry,
|
|
58
|
+
has_safe_event_identity,
|
|
59
|
+
redact_value,
|
|
60
|
+
sanitize_telemetry,
|
|
61
|
+
)
|
|
62
|
+
from .request_capture_policy import should_capture_request_event
|
|
49
63
|
from .suppression import EventSuppressionTracker
|
|
50
64
|
from .transport import HttpTransport, Transport, coerce_transport_response
|
|
51
65
|
from .trigger_token import resolve_request_trigger_directives
|
|
@@ -54,13 +68,8 @@ DEFAULT_BATCH_SIZE = 25
|
|
|
54
68
|
DEFAULT_FLUSH_INTERVAL = 5.0
|
|
55
69
|
DEFAULT_ENDPOINT = "https://api.debugbundle.com/v1/events"
|
|
56
70
|
SCHEMA_VERSION = "2026-03-01"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@dataclass
|
|
60
|
-
class ProbeEntry:
|
|
61
|
-
label: str
|
|
62
|
-
data: dict[str, object]
|
|
63
|
-
timestamp: str
|
|
71
|
+
MAX_PENDING_EVENTS = 1_000
|
|
72
|
+
MAX_PENDING_BYTES = 8 * 1024 * 1024
|
|
64
73
|
|
|
65
74
|
|
|
66
75
|
class ConfigFetchResponse(Protocol):
|
|
@@ -70,23 +79,6 @@ class ConfigFetchResponse(Protocol):
|
|
|
70
79
|
def json(self) -> object: ...
|
|
71
80
|
|
|
72
81
|
|
|
73
|
-
class DebugBundleLogHandler(logging.Handler):
|
|
74
|
-
def __init__(self, sdk: DebugBundleSdk) -> None:
|
|
75
|
-
super().__init__()
|
|
76
|
-
self._sdk = sdk
|
|
77
|
-
|
|
78
|
-
def emit(self, record: logging.LogRecord) -> None:
|
|
79
|
-
self._sdk.capture_log(
|
|
80
|
-
record.getMessage(),
|
|
81
|
-
level=record.levelname.lower(),
|
|
82
|
-
context={
|
|
83
|
-
"logger_name": record.name,
|
|
84
|
-
"pathname": record.pathname,
|
|
85
|
-
"lineno": record.lineno,
|
|
86
|
-
},
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
|
|
90
82
|
class DebugBundleSdk:
|
|
91
83
|
def __init__(
|
|
92
84
|
self,
|
|
@@ -94,10 +86,12 @@ class DebugBundleSdk:
|
|
|
94
86
|
time_provider: Callable[[], float] | None = None,
|
|
95
87
|
) -> None:
|
|
96
88
|
self._transport_override = transport
|
|
89
|
+
self._pid = os.getpid()
|
|
97
90
|
self._time_provider = time_provider or time_now
|
|
98
91
|
self._lock = threading.RLock()
|
|
99
92
|
self._timer: threading.Timer | None = None
|
|
100
93
|
self._remote_config_timer: threading.Timer | None = None
|
|
94
|
+
self._initial_config_ready = threading.Event()
|
|
101
95
|
self._transport: Transport | None = None
|
|
102
96
|
self._http_transport: HttpTransport | None = None
|
|
103
97
|
self._enabled = False
|
|
@@ -111,6 +105,15 @@ class DebugBundleSdk:
|
|
|
111
105
|
self._sample_rate = 1.0
|
|
112
106
|
self._redact_fields = set(DEFAULT_REDACT_FIELDS)
|
|
113
107
|
self._buffer: list[dict[str, object]] = []
|
|
108
|
+
self._buffer_sizes: dict[int, int] = {}
|
|
109
|
+
self._buffer_bytes = 0
|
|
110
|
+
self._pending_low_priority = 0
|
|
111
|
+
self._sending = False
|
|
112
|
+
self._inflight_count = 0
|
|
113
|
+
self._timer_due_at = 0.0
|
|
114
|
+
self._generation = 0
|
|
115
|
+
self._pressure_drops = 0
|
|
116
|
+
self._last_pressure_report_at = 0.0
|
|
114
117
|
self._context: dict[str, object] = {}
|
|
115
118
|
self._scoped_context: ContextVar[dict[str, object] | None] = ContextVar(
|
|
116
119
|
"debugbundle_scoped_context",
|
|
@@ -141,6 +144,7 @@ class DebugBundleSdk:
|
|
|
141
144
|
|
|
142
145
|
@property
|
|
143
146
|
def status(self) -> str:
|
|
147
|
+
ensure_current_process(self)
|
|
144
148
|
with self._lock:
|
|
145
149
|
if not self._enabled:
|
|
146
150
|
return "disconnected"
|
|
@@ -152,6 +156,7 @@ class DebugBundleSdk:
|
|
|
152
156
|
|
|
153
157
|
@property
|
|
154
158
|
def last_event_at(self) -> float | None:
|
|
159
|
+
ensure_current_process(self)
|
|
155
160
|
with self._lock:
|
|
156
161
|
return self._last_event_at
|
|
157
162
|
|
|
@@ -175,6 +180,7 @@ class DebugBundleSdk:
|
|
|
175
180
|
before_send: BeforeSendHook | None = None,
|
|
176
181
|
probes_poll_interval: int = DEFAULT_PROBES_POLL_INTERVAL_MS,
|
|
177
182
|
) -> None:
|
|
183
|
+
ensure_current_process(self)
|
|
178
184
|
with self._lock:
|
|
179
185
|
self.dispose()
|
|
180
186
|
self._project_token = project_token.strip()
|
|
@@ -193,6 +199,10 @@ class DebugBundleSdk:
|
|
|
193
199
|
self._max_probe_entries_per_label = max(1, max_probe_entries_per_label)
|
|
194
200
|
self._probe_flush_on_error = probe_flush_on_error
|
|
195
201
|
self._buffer = []
|
|
202
|
+
self._buffer_sizes = {}
|
|
203
|
+
self._buffer_bytes = 0
|
|
204
|
+
self._pending_low_priority = 0
|
|
205
|
+
self._generation += 1
|
|
196
206
|
self._context = {}
|
|
197
207
|
self._probe_buffers = {}
|
|
198
208
|
self._suppression = EventSuppressionTracker()
|
|
@@ -205,14 +215,25 @@ class DebugBundleSdk:
|
|
|
205
215
|
self._configured_probes_poll_interval_ms = max(1, int(probes_poll_interval))
|
|
206
216
|
self._remote_config_etag = None
|
|
207
217
|
self._remote_config_snapshot = None
|
|
208
|
-
self._capture_policy = BALANCED_CAPTURE_POLICY
|
|
218
|
+
self._capture_policy = MINIMAL_CAPTURE_POLICY if fetch_impl is not None else BALANCED_CAPTURE_POLICY
|
|
209
219
|
self._transport = self._transport_override
|
|
210
220
|
if self._transport is None and self._enabled:
|
|
211
221
|
self._http_transport = HttpTransport(self._endpoint)
|
|
212
222
|
self._transport = self._http_transport
|
|
213
223
|
self.capture_exceptions()
|
|
214
224
|
if self._enabled and self._fetch_impl is not None:
|
|
215
|
-
self.
|
|
225
|
+
self._initial_config_ready.clear()
|
|
226
|
+
self._remote_config_timer = threading.Timer(0.0, self._refresh_initial_remote_config)
|
|
227
|
+
self._remote_config_timer.daemon = True
|
|
228
|
+
self._remote_config_timer.start()
|
|
229
|
+
else:
|
|
230
|
+
self._initial_config_ready.set()
|
|
231
|
+
|
|
232
|
+
def _refresh_initial_remote_config(self) -> None:
|
|
233
|
+
try:
|
|
234
|
+
self._refresh_remote_config(initial=True)
|
|
235
|
+
finally:
|
|
236
|
+
self._initial_config_ready.set()
|
|
216
237
|
|
|
217
238
|
def capture_exception(self, error: BaseException, context: Mapping[str, object] | None = None) -> None:
|
|
218
239
|
self._capture_exception(error, context=context, handled=True)
|
|
@@ -223,31 +244,33 @@ class DebugBundleSdk:
|
|
|
223
244
|
context: Mapping[str, object] | None = None,
|
|
224
245
|
handled: bool = True,
|
|
225
246
|
) -> None:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
247
|
+
ensure_current_process(self)
|
|
248
|
+
if not self._enabled or not may_prepare_event(self, True, MAX_PENDING_EVENTS, MAX_PENDING_BYTES):
|
|
249
|
+
return
|
|
250
|
+
try:
|
|
251
|
+
name, message, stack = exception_details(error)
|
|
230
252
|
redacted_context = redact_mapping(dict(context or {}), self._redact_fields)
|
|
231
253
|
request_payload = backend_exception_request_payload(redacted_context.get("request"))
|
|
232
254
|
response_payload = backend_exception_response_payload(redacted_context.get("response"))
|
|
233
255
|
|
|
234
256
|
payload: dict[str, object] = {
|
|
235
|
-
"name":
|
|
236
|
-
"message":
|
|
237
|
-
"stack":
|
|
257
|
+
"name": name,
|
|
258
|
+
"message": message,
|
|
259
|
+
"stack": stack,
|
|
238
260
|
"handled": handled,
|
|
239
261
|
"request": request_payload,
|
|
240
262
|
"response": response_payload,
|
|
241
263
|
"runtime": runtime_process_facts(),
|
|
242
264
|
}
|
|
243
|
-
if self._probe_flush_on_error:
|
|
244
|
-
|
|
265
|
+
if self._probe_flush_on_error and try_capture_lock(self):
|
|
266
|
+
try:
|
|
267
|
+
probe_data = self._build_probe_data()
|
|
268
|
+
finally:
|
|
269
|
+
self._lock.release()
|
|
245
270
|
if probe_data is not None:
|
|
246
271
|
payload["probe_data"] = probe_data
|
|
247
272
|
|
|
248
|
-
event = self.
|
|
249
|
-
self._base_event("backend_exception", payload, context=redacted_context)
|
|
250
|
-
)
|
|
273
|
+
event = self._protect_event(self._base_event("backend_exception", payload, context=redacted_context))
|
|
251
274
|
if event is None or not self._passes_sample_rate():
|
|
252
275
|
return
|
|
253
276
|
event_payload = cast(dict[str, object], event["payload"])
|
|
@@ -255,9 +278,17 @@ class DebugBundleSdk:
|
|
|
255
278
|
f"{event['event_type']}:{event_payload.get('name', '')}:"
|
|
256
279
|
f"{event_payload.get('message', '')}:{event_payload.get('stack', '')}"
|
|
257
280
|
)
|
|
258
|
-
|
|
281
|
+
event_bytes = self._event_bytes(event)
|
|
282
|
+
if not try_capture_lock(self):
|
|
283
|
+
self._pressure_drops += 1
|
|
259
284
|
return
|
|
260
|
-
|
|
285
|
+
try:
|
|
286
|
+
if self._enabled and self._suppression.should_capture(suppression_key, self._time_provider()):
|
|
287
|
+
self._offer_protected_event(event, event_bytes)
|
|
288
|
+
finally:
|
|
289
|
+
self._lock.release()
|
|
290
|
+
except BaseException:
|
|
291
|
+
return
|
|
261
292
|
|
|
262
293
|
def capture_error(self, error: BaseException, context: Mapping[str, object] | None = None) -> None:
|
|
263
294
|
self.capture_exception(error, context=context)
|
|
@@ -268,9 +299,16 @@ class DebugBundleSdk:
|
|
|
268
299
|
level: str = DEFAULT_LOG_LEVEL,
|
|
269
300
|
context: Mapping[str, object] | None = None,
|
|
270
301
|
) -> None:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if not self._enabled
|
|
302
|
+
try:
|
|
303
|
+
normalized_level = normalize_level(level)
|
|
304
|
+
if not self._enabled or self._capture_policy.capture_logs == "off" or not level_enabled(
|
|
305
|
+
normalized_level, self._effective_log_threshold()
|
|
306
|
+
):
|
|
307
|
+
return
|
|
308
|
+
ensure_current_process(self)
|
|
309
|
+
if not may_prepare_event(
|
|
310
|
+
self, normalized_level in ("error", "critical"), MAX_PENDING_EVENTS, MAX_PENDING_BYTES
|
|
311
|
+
):
|
|
274
312
|
return
|
|
275
313
|
payload: dict[str, object] = {
|
|
276
314
|
"message": message,
|
|
@@ -279,15 +317,23 @@ class DebugBundleSdk:
|
|
|
279
317
|
}
|
|
280
318
|
if context:
|
|
281
319
|
payload["attributes"] = redact_mapping(dict(context), self._redact_fields)
|
|
282
|
-
event = self.
|
|
320
|
+
event = self._protect_event(self._base_event("log_event", payload, context=context))
|
|
283
321
|
if (
|
|
284
322
|
event is None
|
|
285
323
|
or not self._passes_sample_rate()
|
|
286
|
-
or self._capture_policy.capture_logs == "off"
|
|
287
|
-
or not level_enabled(normalized_level, self._effective_log_threshold())
|
|
288
324
|
):
|
|
289
325
|
return
|
|
290
|
-
self.
|
|
326
|
+
event_bytes = self._event_bytes(event)
|
|
327
|
+
if not try_capture_lock(self):
|
|
328
|
+
self._pressure_drops += 1
|
|
329
|
+
return
|
|
330
|
+
try:
|
|
331
|
+
if self._log_level_eligible(normalized_level):
|
|
332
|
+
self._offer_protected_event(event, event_bytes)
|
|
333
|
+
finally:
|
|
334
|
+
self._lock.release()
|
|
335
|
+
except BaseException:
|
|
336
|
+
return
|
|
291
337
|
|
|
292
338
|
def capture_request(
|
|
293
339
|
self,
|
|
@@ -295,22 +341,34 @@ class DebugBundleSdk:
|
|
|
295
341
|
response: Mapping[str, object] | None = None,
|
|
296
342
|
context: Mapping[str, object] | None = None,
|
|
297
343
|
) -> None:
|
|
298
|
-
|
|
299
|
-
if not self._enabled:
|
|
344
|
+
try:
|
|
345
|
+
if not self._enabled or not should_capture_request_event(self._capture_policy, request, response):
|
|
346
|
+
return
|
|
347
|
+
ensure_current_process(self)
|
|
348
|
+
status = None if response is None else response.get("status_code") or response.get("response_status")
|
|
349
|
+
if not may_prepare_event(
|
|
350
|
+
self, type(status) is int and status >= 500, MAX_PENDING_EVENTS, MAX_PENDING_BYTES
|
|
351
|
+
):
|
|
300
352
|
return
|
|
301
353
|
payload = request_event_payload(
|
|
302
354
|
redact_mapping(dict(request), self._redact_fields),
|
|
303
355
|
redact_mapping(dict(response or {}), self._redact_fields),
|
|
304
356
|
redact_mapping(dict(context or {}), self._redact_fields),
|
|
305
357
|
)
|
|
306
|
-
event = self.
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
358
|
+
event = self._protect_event(self._base_event("request_event", payload, context=context))
|
|
359
|
+
if event is None or not self._passes_sample_rate():
|
|
360
|
+
return
|
|
361
|
+
event_bytes = self._event_bytes(event)
|
|
362
|
+
if not try_capture_lock(self):
|
|
363
|
+
self._pressure_drops += 1
|
|
312
364
|
return
|
|
313
|
-
|
|
365
|
+
try:
|
|
366
|
+
if self._enabled:
|
|
367
|
+
self._offer_protected_event(event, event_bytes)
|
|
368
|
+
finally:
|
|
369
|
+
self._lock.release()
|
|
370
|
+
except BaseException:
|
|
371
|
+
return
|
|
314
372
|
|
|
315
373
|
def capture_message(
|
|
316
374
|
self,
|
|
@@ -321,138 +379,196 @@ class DebugBundleSdk:
|
|
|
321
379
|
self.capture_log(message, level=level or DEFAULT_LOG_LEVEL, context=context)
|
|
322
380
|
|
|
323
381
|
def set_context(self, key: str, value: object) -> None:
|
|
324
|
-
|
|
325
|
-
|
|
382
|
+
try:
|
|
383
|
+
ensure_current_process(self)
|
|
384
|
+
protected = sanitize_telemetry({key: value}, self._redact_fields)
|
|
385
|
+
if not isinstance(protected, dict) or not try_capture_lock(self):
|
|
386
|
+
return
|
|
387
|
+
try:
|
|
388
|
+
next_context = dict(self._context)
|
|
389
|
+
next_context[key] = protected.get(key)
|
|
390
|
+
self._context = next_context
|
|
391
|
+
finally:
|
|
392
|
+
self._lock.release()
|
|
393
|
+
except BaseException:
|
|
394
|
+
return
|
|
326
395
|
|
|
327
396
|
def _bind_scoped_context(self, context: Mapping[str, object]) -> Token[dict[str, object] | None]:
|
|
397
|
+
ensure_current_process(self)
|
|
328
398
|
scoped_context = dict(self._scoped_context.get() or {})
|
|
329
399
|
for key, value in context.items():
|
|
330
400
|
if value is None:
|
|
331
401
|
continue
|
|
332
402
|
scoped_context[str(key)] = value
|
|
333
|
-
|
|
403
|
+
try:
|
|
404
|
+
protected = sanitize_telemetry(scoped_context, self._redact_fields)
|
|
405
|
+
return self._scoped_context.set(cast(dict[str, object], protected))
|
|
406
|
+
except UnsafeTelemetry:
|
|
407
|
+
return self._scoped_context.set({})
|
|
334
408
|
|
|
335
409
|
def _reset_scoped_context(self, token: Token[dict[str, object] | None]) -> None:
|
|
336
410
|
self._scoped_context.reset(token)
|
|
337
411
|
|
|
338
412
|
def flush(self) -> None:
|
|
413
|
+
ensure_current_process(self)
|
|
339
414
|
with self._lock:
|
|
340
|
-
if not self._enabled or self._transport is None:
|
|
341
|
-
return
|
|
342
|
-
|
|
343
|
-
self._append_suppression_aggregates()
|
|
344
|
-
if not self._buffer:
|
|
415
|
+
if not self._enabled or self._transport is None or self._sending:
|
|
345
416
|
return
|
|
346
|
-
|
|
417
|
+
if self._timer is not None:
|
|
418
|
+
self._timer.cancel()
|
|
419
|
+
self._timer = None
|
|
347
420
|
now = self._time_provider()
|
|
348
421
|
if now < self._retry_after:
|
|
422
|
+
self._schedule_flush_locked(delay=self._retry_after - now)
|
|
349
423
|
return
|
|
424
|
+
self._sending = True
|
|
425
|
+
generation = self._generation
|
|
426
|
+
suppression = self._suppression.drain_aggregates(now)
|
|
427
|
+
pressure_count = (
|
|
428
|
+
self._pressure_drops
|
|
429
|
+
if self._pressure_drops and now - self._last_pressure_report_at >= 60.0
|
|
430
|
+
and len(self._buffer) < MAX_PENDING_EVENTS else 0
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
aggregates, pressure = prepare_flush_aggregates(self, suppression, pressure_count)
|
|
435
|
+
except BaseException:
|
|
436
|
+
aggregates, pressure = [], None
|
|
350
437
|
|
|
351
|
-
|
|
438
|
+
with self._lock:
|
|
439
|
+
if generation != self._generation:
|
|
440
|
+
self._sending = False
|
|
441
|
+
self._inflight_count = 0
|
|
442
|
+
if self._buffer:
|
|
443
|
+
self._schedule_flush_locked()
|
|
444
|
+
return
|
|
445
|
+
for aggregate, aggregate_bytes in aggregates:
|
|
446
|
+
self._offer_protected_event(aggregate, aggregate_bytes)
|
|
447
|
+
if (pressure is not None and len(self._buffer) < MAX_PENDING_EVENTS
|
|
448
|
+
and self._buffer_bytes + pressure[1] <= MAX_PENDING_BYTES
|
|
449
|
+
and self._offer_protected_event(*pressure)):
|
|
450
|
+
self._pressure_drops = max(0, self._pressure_drops - pressure_count)
|
|
451
|
+
self._last_pressure_report_at = now
|
|
452
|
+
if not self._buffer:
|
|
453
|
+
self._sending = False
|
|
454
|
+
self._schedule_pressure_report_locked()
|
|
455
|
+
return
|
|
456
|
+
batch = list(self._buffer)
|
|
457
|
+
self._inflight_count = len(batch)
|
|
458
|
+
self._pending_low_priority = 0
|
|
459
|
+
transport = self._transport
|
|
352
460
|
request = {
|
|
353
461
|
"project_token": self._project_token,
|
|
354
462
|
"events": batch,
|
|
355
463
|
}
|
|
356
464
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
self.
|
|
465
|
+
finalized = finalize_batch(self, batch, generation, MAX_PENDING_BYTES)
|
|
466
|
+
|
|
467
|
+
with self._lock:
|
|
468
|
+
if generation != self._generation or finalized is None:
|
|
469
|
+
self._sending = False
|
|
470
|
+
self._inflight_count = 0
|
|
471
|
+
if self._buffer:
|
|
472
|
+
self._schedule_flush_locked()
|
|
473
|
+
return
|
|
474
|
+
batch = finalized
|
|
475
|
+
self._inflight_count = len(batch)
|
|
476
|
+
if not batch:
|
|
477
|
+
self._sending = False
|
|
478
|
+
if self._buffer:
|
|
479
|
+
self._schedule_flush_locked()
|
|
362
480
|
return
|
|
481
|
+
request["events"] = batch
|
|
363
482
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
483
|
+
try:
|
|
484
|
+
response = coerce_transport_response(transport(request))
|
|
485
|
+
except BaseException:
|
|
486
|
+
response = None
|
|
487
|
+
|
|
488
|
+
with self._lock:
|
|
489
|
+
self._sending = False
|
|
490
|
+
self._inflight_count = 0
|
|
491
|
+
if generation != self._generation:
|
|
492
|
+
if self._buffer:
|
|
493
|
+
self._schedule_flush_locked()
|
|
494
|
+
return
|
|
495
|
+
try:
|
|
496
|
+
if response is None:
|
|
367
497
|
self._consecutive_failures += 1
|
|
368
|
-
|
|
369
|
-
self._retry_after = now + (retry_after_ms / 1000)
|
|
370
|
-
self._emit_diagnostic(
|
|
371
|
-
"ingestion_acknowledgement_invalid",
|
|
372
|
-
"sdk-python retained a batch after an invalid ingestion acknowledgement",
|
|
373
|
-
metadata={"reason": acknowledgement.reason or "invalid"},
|
|
374
|
-
)
|
|
375
|
-
self._schedule_flush_locked(delay=retry_after_ms / 1000)
|
|
498
|
+
self._schedule_flush_locked()
|
|
376
499
|
return
|
|
377
|
-
|
|
378
|
-
|
|
500
|
+
|
|
501
|
+
if 200 <= response.status_code < 300:
|
|
502
|
+
acknowledgement = decide_acknowledgement(response.body, len(batch))
|
|
503
|
+
if acknowledgement.kind == "protocol_failure":
|
|
504
|
+
self._consecutive_failures += 1
|
|
505
|
+
retry_after_ms = response.retry_after_ms if response.retry_after_ms is not None else 1_000
|
|
506
|
+
self._retry_after = now + (retry_after_ms / 1000)
|
|
507
|
+
self._emit_diagnostic(
|
|
508
|
+
"ingestion_acknowledgement_invalid",
|
|
509
|
+
"sdk-python retained a batch after an invalid ingestion acknowledgement",
|
|
510
|
+
metadata={"reason": acknowledgement.reason or "invalid"},
|
|
511
|
+
)
|
|
512
|
+
self._schedule_flush_locked(delay=retry_after_ms / 1000)
|
|
513
|
+
return
|
|
514
|
+
if acknowledgement.kind == "legacy":
|
|
515
|
+
self._buffer = self._buffer[len(batch) :]
|
|
516
|
+
self._retry_after = 0.0
|
|
517
|
+
self._last_event_at = self._time_provider() * 1000
|
|
518
|
+
self._consecutive_failures = 0
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
trailing_events = self._buffer[len(batch) :]
|
|
522
|
+
self._buffer = [
|
|
523
|
+
batch[index] for index in acknowledgement.retryable_indices if 0 <= index < len(batch)
|
|
524
|
+
] + trailing_events
|
|
525
|
+
if acknowledgement.terminal_errors:
|
|
526
|
+
self._emit_diagnostic(
|
|
527
|
+
"ingestion_events_rejected",
|
|
528
|
+
"sdk-python removed terminally rejected ingestion events",
|
|
529
|
+
metadata={
|
|
530
|
+
"rejected_count": len(acknowledgement.terminal_errors),
|
|
531
|
+
"reasons": sorted({reason for _, reason in acknowledgement.terminal_errors}),
|
|
532
|
+
},
|
|
533
|
+
)
|
|
534
|
+
if acknowledgement.accepted > 0:
|
|
535
|
+
self._last_event_at = self._time_provider() * 1000
|
|
536
|
+
if acknowledgement.retryable_indices:
|
|
537
|
+
self._consecutive_failures += 1
|
|
538
|
+
retry_after_ms = response.retry_after_ms if response.retry_after_ms is not None else 1_000
|
|
539
|
+
self._retry_after = now + (retry_after_ms / 1000)
|
|
540
|
+
self._schedule_flush_locked(delay=retry_after_ms / 1000)
|
|
541
|
+
return
|
|
379
542
|
self._retry_after = 0.0
|
|
380
|
-
self.
|
|
381
|
-
self._consecutive_failures = 0
|
|
543
|
+
self._consecutive_failures = 0 if acknowledgement.accepted > 0 else 3
|
|
382
544
|
return
|
|
383
545
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
batch[index] for index in acknowledgement.retryable_indices if 0 <= index < len(batch)
|
|
387
|
-
] + trailing_events
|
|
388
|
-
if acknowledgement.terminal_errors:
|
|
389
|
-
self._emit_diagnostic(
|
|
390
|
-
"ingestion_events_rejected",
|
|
391
|
-
"sdk-python removed terminally rejected ingestion events",
|
|
392
|
-
metadata={
|
|
393
|
-
"rejected_count": len(acknowledgement.terminal_errors),
|
|
394
|
-
"reasons": sorted({reason for _, reason in acknowledgement.terminal_errors}),
|
|
395
|
-
},
|
|
396
|
-
)
|
|
397
|
-
if acknowledgement.accepted > 0:
|
|
398
|
-
self._last_event_at = self._time_provider() * 1000
|
|
399
|
-
if acknowledgement.retryable_indices:
|
|
400
|
-
self._consecutive_failures += 1
|
|
546
|
+
self._consecutive_failures += 1
|
|
547
|
+
if response.status_code == 429:
|
|
401
548
|
retry_after_ms = response.retry_after_ms if response.retry_after_ms is not None else 1_000
|
|
402
549
|
self._retry_after = now + (retry_after_ms / 1000)
|
|
403
550
|
self._schedule_flush_locked(delay=retry_after_ms / 1000)
|
|
404
551
|
return
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
self.
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
self._buffer
|
|
418
|
-
|
|
419
|
-
return
|
|
420
|
-
|
|
421
|
-
self._schedule_flush_locked()
|
|
552
|
+
if 400 <= response.status_code < 500:
|
|
553
|
+
self._buffer = self._buffer[len(batch) :]
|
|
554
|
+
self._retry_after = 0.0
|
|
555
|
+
return
|
|
556
|
+
self._schedule_flush_locked()
|
|
557
|
+
finally:
|
|
558
|
+
self._buffer_sizes = {id(event): self._buffer_sizes[id(event)]
|
|
559
|
+
for event in self._buffer}
|
|
560
|
+
self._buffer_bytes = sum(self._buffer_sizes.values())
|
|
561
|
+
self._pending_low_priority = sum(not high_priority_event(event) for event in self._buffer)
|
|
562
|
+
if self._buffer and self._timer is None and self._retry_after <= self._time_provider():
|
|
563
|
+
self._schedule_flush_locked()
|
|
564
|
+
elif not self._buffer and self._timer is None:
|
|
565
|
+
self._schedule_pressure_report_locked()
|
|
422
566
|
|
|
423
567
|
def probe(self, label: str, data: object | Callable[[], object], opts: Mapping[str, object] | None = None) -> None:
|
|
424
|
-
|
|
425
|
-
if not self._enabled:
|
|
426
|
-
return
|
|
427
|
-
options = dict(opts or {})
|
|
428
|
-
now_ms = int(self._time_provider() * 1000)
|
|
429
|
-
matching_directives = self._find_matching_probe_directives(label, now_ms)
|
|
430
|
-
is_heavy = options.get("heavy") is True
|
|
431
|
-
if is_heavy and not matching_directives:
|
|
432
|
-
return
|
|
433
|
-
if label not in self._probe_buffers and len(self._probe_buffers) >= self._max_probe_labels:
|
|
434
|
-
return
|
|
435
|
-
|
|
436
|
-
value = data() if callable(data) else data
|
|
437
|
-
if not isinstance(value, Mapping):
|
|
438
|
-
value = {"value": value}
|
|
439
|
-
|
|
440
|
-
redacted_value = redact_mapping(dict(value), self._redact_fields)
|
|
441
|
-
|
|
442
|
-
if is_heavy:
|
|
443
|
-
self._emit_probe_events(label, redacted_value, matching_directives)
|
|
444
|
-
return
|
|
445
|
-
|
|
446
|
-
entry = ProbeEntry(
|
|
447
|
-
label=label,
|
|
448
|
-
data=redacted_value,
|
|
449
|
-
timestamp=iso_now(self._time_provider),
|
|
450
|
-
)
|
|
451
|
-
bucket = self._probe_buffers.setdefault(label, deque(maxlen=self._max_probe_entries_per_label))
|
|
452
|
-
bucket.append(entry)
|
|
453
|
-
self._emit_probe_events(label, redacted_value, matching_directives)
|
|
568
|
+
capture_probe(self, label, data, opts)
|
|
454
569
|
|
|
455
570
|
def capture_exceptions(self) -> None:
|
|
571
|
+
ensure_current_process(self)
|
|
456
572
|
with self._lock:
|
|
457
573
|
if self._original_excepthook is None:
|
|
458
574
|
self._original_excepthook = sys.excepthook
|
|
@@ -465,6 +581,7 @@ class DebugBundleSdk:
|
|
|
465
581
|
sys.excepthook = handler
|
|
466
582
|
|
|
467
583
|
def capture_logging(self, logger: logging.Logger | None = None) -> None:
|
|
584
|
+
ensure_current_process(self)
|
|
468
585
|
with self._lock:
|
|
469
586
|
target_logger = logger or logging.getLogger()
|
|
470
587
|
logger_id = id(target_logger)
|
|
@@ -479,6 +596,7 @@ class DebugBundleSdk:
|
|
|
479
596
|
self._optional_logging_restorers = attach_optional_integrations(self, self._on_diagnostic)
|
|
480
597
|
|
|
481
598
|
def capture_async(self, loop: asyncio.AbstractEventLoop | None = None) -> None:
|
|
599
|
+
ensure_current_process(self)
|
|
482
600
|
with self._lock:
|
|
483
601
|
target_loop = loop or asyncio.get_event_loop()
|
|
484
602
|
if target_loop in self._async_handlers:
|
|
@@ -496,6 +614,7 @@ class DebugBundleSdk:
|
|
|
496
614
|
target_loop.set_exception_handler(handler)
|
|
497
615
|
|
|
498
616
|
def dispose(self) -> None:
|
|
617
|
+
ensure_current_process(self)
|
|
499
618
|
with self._lock:
|
|
500
619
|
if self._timer is not None:
|
|
501
620
|
self._timer.cancel()
|
|
@@ -523,32 +642,43 @@ class DebugBundleSdk:
|
|
|
523
642
|
with self._lock:
|
|
524
643
|
if not self._enabled or self._fetch_impl is None:
|
|
525
644
|
return
|
|
526
|
-
|
|
527
645
|
request_headers: dict[str, str] = {}
|
|
528
646
|
if self._remote_config_etag is not None:
|
|
529
647
|
request_headers["if-none-match"] = self._remote_config_etag
|
|
648
|
+
fetch = self._fetch_impl
|
|
649
|
+
endpoint = sdk_config_endpoint(self._endpoint)
|
|
650
|
+
poll_interval = self._configured_probes_poll_interval_ms
|
|
651
|
+
generation = self._generation
|
|
530
652
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
},
|
|
538
|
-
)
|
|
539
|
-
status_code = getattr(response, "status_code", None)
|
|
540
|
-
if status_code == 304:
|
|
541
|
-
self._schedule_next_remote_config_refresh()
|
|
542
|
-
return
|
|
543
|
-
if status_code != 200:
|
|
544
|
-
raise RuntimeError(f"unexpected config status {status_code}")
|
|
545
|
-
|
|
546
|
-
payload = response.json()
|
|
653
|
+
try:
|
|
654
|
+
response = fetch(endpoint, {"method": "GET", "headers": request_headers})
|
|
655
|
+
status_code = getattr(response, "status_code", None)
|
|
656
|
+
if status_code == 304:
|
|
657
|
+
snapshot = None
|
|
658
|
+
elif status_code == 200:
|
|
547
659
|
snapshot = parse_remote_config(
|
|
548
|
-
|
|
549
|
-
|
|
660
|
+
response.json(),
|
|
661
|
+
poll_interval,
|
|
550
662
|
int(self._time_provider() * 1000),
|
|
551
663
|
)
|
|
664
|
+
else:
|
|
665
|
+
raise RuntimeError(f"unexpected config status {status_code}")
|
|
666
|
+
headers = response.headers or {}
|
|
667
|
+
etag = headers.get("etag")
|
|
668
|
+
failure: BaseException | None = None
|
|
669
|
+
except BaseException as error:
|
|
670
|
+
status_code = None
|
|
671
|
+
snapshot = None
|
|
672
|
+
etag = None
|
|
673
|
+
failure = error
|
|
674
|
+
|
|
675
|
+
with self._lock:
|
|
676
|
+
if generation != self._generation or not self._enabled:
|
|
677
|
+
return
|
|
678
|
+
if failure is None and status_code == 304:
|
|
679
|
+
self._schedule_next_remote_config_refresh()
|
|
680
|
+
return
|
|
681
|
+
if failure is None:
|
|
552
682
|
if snapshot is None:
|
|
553
683
|
self._emit_diagnostic(
|
|
554
684
|
"remote_probe_config_invalid",
|
|
@@ -561,16 +691,14 @@ class DebugBundleSdk:
|
|
|
561
691
|
|
|
562
692
|
self._remote_config_snapshot = snapshot
|
|
563
693
|
self._capture_policy = snapshot.capture_policy
|
|
564
|
-
headers = response.headers or {}
|
|
565
|
-
etag = headers.get("etag")
|
|
566
694
|
if isinstance(etag, str) and len(etag) > 0:
|
|
567
695
|
self._remote_config_etag = etag
|
|
568
696
|
self._schedule_next_remote_config_refresh()
|
|
569
|
-
|
|
697
|
+
else:
|
|
570
698
|
self._emit_diagnostic(
|
|
571
699
|
"remote_probe_config_failed",
|
|
572
700
|
"sdk-python failed to refresh remote probe config",
|
|
573
|
-
metadata={"error": serialize_error(
|
|
701
|
+
metadata={"error": serialize_error(failure) if isinstance(failure, Exception) else "fetch_failed"},
|
|
574
702
|
)
|
|
575
703
|
if initial:
|
|
576
704
|
self._capture_policy = MINIMAL_CAPTURE_POLICY
|
|
@@ -613,39 +741,81 @@ class DebugBundleSdk:
|
|
|
613
741
|
merged.update({str(key): value for key, value in context.items()})
|
|
614
742
|
return cast(dict[str, object], redact_value(merged, self._redact_fields))
|
|
615
743
|
|
|
616
|
-
def
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
744
|
+
def _protect_event(self, event: dict[str, object]) -> dict[str, object] | None:
|
|
745
|
+
try:
|
|
746
|
+
if not has_safe_event_identity(event, self._redact_fields):
|
|
747
|
+
return None
|
|
748
|
+
candidate = dict(event)
|
|
749
|
+
for key in ("payload", "context", "service"):
|
|
750
|
+
if key in candidate:
|
|
751
|
+
candidate[key] = sanitize_telemetry(candidate[key], self._redact_fields)
|
|
752
|
+
return candidate
|
|
753
|
+
except UnsafeTelemetry:
|
|
754
|
+
return None
|
|
755
|
+
|
|
756
|
+
def _enqueue_event(self, event: dict[str, object]) -> bool:
|
|
757
|
+
protected = self._protect_event(event)
|
|
758
|
+
if protected is None:
|
|
759
|
+
return False
|
|
760
|
+
return self._offer_protected_event(protected, self._event_bytes(protected))
|
|
761
|
+
|
|
762
|
+
def _offer_protected_event(self, protected: dict[str, object], event_bytes: int) -> bool:
|
|
763
|
+
if event_bytes > MAX_PENDING_BYTES:
|
|
764
|
+
self._pressure_drops += 1
|
|
765
|
+
return False
|
|
766
|
+
high_priority = high_priority_event(protected)
|
|
767
|
+
under_pressure = len(self._buffer) >= MAX_PENDING_EVENTS or self._buffer_bytes + event_bytes > MAX_PENDING_BYTES
|
|
768
|
+
while len(self._buffer) >= MAX_PENDING_EVENTS or self._buffer_bytes + event_bytes > MAX_PENDING_BYTES:
|
|
769
|
+
if not high_priority or not evict_low_priority(self):
|
|
770
|
+
self._pressure_drops += 1
|
|
771
|
+
return False
|
|
772
|
+
insertion = self._inflight_count
|
|
773
|
+
if high_priority and (protected.get("event_type") != "request_event" or under_pressure):
|
|
774
|
+
while insertion < len(self._buffer) and high_priority_event(self._buffer[insertion]):
|
|
775
|
+
insertion += 1
|
|
776
|
+
self._buffer.insert(insertion, protected)
|
|
777
|
+
else:
|
|
778
|
+
self._buffer.append(protected)
|
|
779
|
+
self._pending_low_priority += 1
|
|
780
|
+
self._buffer_bytes += event_bytes
|
|
781
|
+
self._buffer_sizes[id(protected)] = event_bytes
|
|
782
|
+
self._schedule_flush_locked(delay=0.0 if len(self._buffer) >= self._batch_size else None)
|
|
783
|
+
return True
|
|
784
|
+
|
|
785
|
+
@staticmethod
|
|
786
|
+
def _event_bytes(event: dict[str, object]) -> int:
|
|
787
|
+
return len(json.dumps(event, separators=(",", ":")).encode("utf-8"))
|
|
622
788
|
|
|
623
789
|
def _schedule_flush_locked(self, delay: float | None = None) -> None:
|
|
624
|
-
if self.
|
|
625
|
-
|
|
790
|
+
if self._sending:
|
|
791
|
+
return
|
|
626
792
|
next_delay = self._flush_interval if delay is None else max(delay, 0.0)
|
|
793
|
+
due_at = time.monotonic() + next_delay
|
|
794
|
+
if self._timer is not None and self._timer.is_alive():
|
|
795
|
+
if self._timer_due_at <= due_at:
|
|
796
|
+
return
|
|
797
|
+
self._timer.cancel()
|
|
627
798
|
self._timer = threading.Timer(next_delay, self.flush)
|
|
628
799
|
self._timer.daemon = True
|
|
800
|
+
self._timer_due_at = due_at
|
|
629
801
|
self._timer.start()
|
|
630
802
|
|
|
631
|
-
def
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if not isinstance(event_type, str) or not isinstance(payload, dict):
|
|
637
|
-
continue
|
|
638
|
-
aggregate.update(self._base_event(event_type, cast(dict[str, object], payload)))
|
|
639
|
-
prepared = self._apply_before_send_event(aggregate)
|
|
640
|
-
if prepared is not None:
|
|
641
|
-
self._buffer.append(prepared)
|
|
803
|
+
def _schedule_pressure_report_locked(self) -> None:
|
|
804
|
+
if self._pressure_drops == 0:
|
|
805
|
+
return
|
|
806
|
+
delay = max(self._flush_interval, self._last_pressure_report_at + 60.0 - self._time_provider())
|
|
807
|
+
self._schedule_flush_locked(delay=delay)
|
|
642
808
|
|
|
643
809
|
def _apply_before_send_event(self, event: dict[str, object]) -> dict[str, object] | None:
|
|
644
|
-
|
|
645
|
-
|
|
810
|
+
protected = self._protect_event(event)
|
|
811
|
+
if protected is None:
|
|
812
|
+
return None
|
|
813
|
+
result = apply_before_send(
|
|
814
|
+
protected,
|
|
646
815
|
self._before_send,
|
|
647
816
|
lambda code, message: self._emit_diagnostic(code, message),
|
|
648
817
|
)
|
|
818
|
+
return self._protect_event(result) if result is not None else None
|
|
649
819
|
|
|
650
820
|
def _build_probe_data(self) -> dict[str, object] | None:
|
|
651
821
|
items: list[dict[str, object]] = []
|
|
@@ -670,60 +840,15 @@ class DebugBundleSdk:
|
|
|
670
840
|
policy_threshold = self._capture_policy.capture_logs
|
|
671
841
|
return self._log_level if LEVEL_RANKS[self._log_level] >= LEVEL_RANKS[policy_threshold] else policy_threshold
|
|
672
842
|
|
|
673
|
-
def
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
status_code = None
|
|
680
|
-
if response is not None:
|
|
681
|
-
candidate = response.get("status_code") or response.get("response_status")
|
|
682
|
-
if isinstance(candidate, int):
|
|
683
|
-
status_code = candidate
|
|
684
|
-
request_path = None
|
|
685
|
-
http_method = None
|
|
686
|
-
if request is not None:
|
|
687
|
-
path_candidate = request.get("path") or request.get("url")
|
|
688
|
-
method_candidate = request.get("method")
|
|
689
|
-
request_path = path_candidate if isinstance(path_candidate, str) else None
|
|
690
|
-
http_method = method_candidate if isinstance(method_candidate, str) else None
|
|
691
|
-
if is_immediate_request_incident_status(
|
|
692
|
-
status_code,
|
|
693
|
-
self._capture_policy.preset,
|
|
694
|
-
self._capture_policy.immediate_client_error_statuses,
|
|
695
|
-
request_path,
|
|
696
|
-
http_method,
|
|
697
|
-
self._capture_policy.immediate_client_error_path_rules,
|
|
698
|
-
):
|
|
699
|
-
return True
|
|
700
|
-
if policy == "off":
|
|
701
|
-
return False
|
|
702
|
-
if policy == "all":
|
|
703
|
-
return True
|
|
704
|
-
if response is None:
|
|
705
|
-
return policy == "filtered"
|
|
706
|
-
if status_code is None:
|
|
707
|
-
return policy == "filtered"
|
|
708
|
-
if policy == "failures_only":
|
|
709
|
-
return status_code >= 500
|
|
710
|
-
if policy == "filtered":
|
|
711
|
-
return False
|
|
712
|
-
return True
|
|
713
|
-
|
|
714
|
-
def _emit_probe_events(self, label: str, data: dict[str, object], directives: list[RemoteProbeDirective]) -> None:
|
|
715
|
-
for directive in directives:
|
|
716
|
-
payload = {
|
|
717
|
-
"label": label,
|
|
718
|
-
"activation_id": getattr(directive, "id"),
|
|
719
|
-
"probe_label_pattern": getattr(directive, "label_pattern"),
|
|
720
|
-
"data": dict(data),
|
|
721
|
-
}
|
|
722
|
-
event = self._apply_before_send_event(self._base_event("probe_event", payload))
|
|
723
|
-
if event is not None and self._capture_policy.capture_probe_events == "standalone_when_activated":
|
|
724
|
-
self._enqueue_event(event)
|
|
843
|
+
def _log_level_eligible(self, level: str) -> bool:
|
|
844
|
+
return (
|
|
845
|
+
self._enabled
|
|
846
|
+
and self._capture_policy.capture_logs != "off"
|
|
847
|
+
and level_enabled(normalize_level(level), self._effective_log_threshold())
|
|
848
|
+
)
|
|
725
849
|
|
|
726
850
|
def begin_request(self, request: dict[str, Any]) -> Token[list[RemoteProbeDirective] | None]:
|
|
851
|
+
ensure_current_process(self)
|
|
727
852
|
trigger_token_key = (
|
|
728
853
|
self._remote_config_snapshot.trigger_token_key if self._remote_config_snapshot is not None else None
|
|
729
854
|
)
|
|
@@ -780,7 +905,10 @@ class DebugBundleSdk:
|
|
|
780
905
|
return
|
|
781
906
|
diagnostic: dict[str, object] = {"code": code, "message": message}
|
|
782
907
|
if metadata is not None:
|
|
783
|
-
|
|
908
|
+
try:
|
|
909
|
+
diagnostic["metadata"] = sanitize_telemetry(metadata, self._redact_fields)
|
|
910
|
+
except UnsafeTelemetry:
|
|
911
|
+
pass
|
|
784
912
|
try:
|
|
785
913
|
self._on_diagnostic(diagnostic)
|
|
786
914
|
except Exception:
|