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