debugbundle-python 2.0.1__py3-none-any.whl → 2.1.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/__init__.py +16 -0
- debugbundle/before_send.py +35 -191
- debugbundle/canonical_event.py +202 -0
- debugbundle/config.py +6 -0
- debugbundle/core.py +76 -318
- debugbundle/delivery_hooks.py +14 -0
- debugbundle/diagnostic_budget.py +191 -0
- debugbundle/event-envelope.schema.json +4904 -0
- debugbundle/evidence_delivery.py +115 -0
- debugbundle/evidence_http.py +45 -0
- debugbundle/evidence_json.py +68 -0
- debugbundle/evidence_wire.py +409 -0
- debugbundle/exception_capture.py +92 -0
- debugbundle/exception_evidence.py +199 -0
- debugbundle/exception_snapshot.py +1 -1
- debugbundle/flush_delivery.py +203 -0
- debugbundle/ingestion_wire.py +75 -0
- debugbundle/integrations/relay_django.py +4 -1
- debugbundle/integrations/relay_fastapi.py +10 -2
- debugbundle/integrations/relay_flask.py +4 -1
- debugbundle/operation_context.py +121 -0
- debugbundle/process_support.py +7 -0
- debugbundle/queue_admission.py +42 -2
- debugbundle/recovery_capture.py +43 -0
- debugbundle/relay.py +93 -36
- debugbundle/relay_delivery.py +108 -17
- debugbundle/remote_config_client.py +81 -0
- debugbundle/scope_ownership.py +99 -0
- debugbundle/transport.py +20 -14
- {debugbundle_python-2.0.1.dist-info → debugbundle_python-2.1.0.dist-info}/METADATA +116 -1
- debugbundle_python-2.1.0.dist-info/RECORD +51 -0
- {debugbundle_python-2.0.1.dist-info → debugbundle_python-2.1.0.dist-info}/WHEEL +1 -1
- debugbundle_python-2.0.1.dist-info/RECORD +0 -36
- {debugbundle_python-2.0.1.dist-info → debugbundle_python-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {debugbundle_python-2.0.1.dist-info → debugbundle_python-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Bound tracker metadata and pending payload ownership to 64 KiB per destination."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
from dataclasses import dataclass, field, replace
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Window:
|
|
13
|
+
starts: float
|
|
14
|
+
failure_at: float | None = None
|
|
15
|
+
failure_id: str | None = None
|
|
16
|
+
attempts: set[str] = field(default_factory=set)
|
|
17
|
+
observations: int = 0
|
|
18
|
+
resource: bool = False
|
|
19
|
+
ambiguous: bool = False
|
|
20
|
+
pending: int = 0
|
|
21
|
+
cost: int = 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Pending:
|
|
26
|
+
scope: str
|
|
27
|
+
kind: str
|
|
28
|
+
attempt: str | None
|
|
29
|
+
size: int
|
|
30
|
+
cost: int
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _key(payload: dict[str, Any]) -> str:
|
|
34
|
+
return json.dumps([payload["failure_ref"], payload["scope"]], sort_keys=True, separators=(",", ":"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _cost(key: str, value: Window) -> int:
|
|
38
|
+
return len(
|
|
39
|
+
json.dumps(
|
|
40
|
+
[
|
|
41
|
+
key,
|
|
42
|
+
value.starts,
|
|
43
|
+
value.failure_at,
|
|
44
|
+
value.failure_id,
|
|
45
|
+
sorted(value.attempts),
|
|
46
|
+
value.observations,
|
|
47
|
+
value.resource,
|
|
48
|
+
value.ambiguous,
|
|
49
|
+
value.pending,
|
|
50
|
+
],
|
|
51
|
+
separators=(",", ":"),
|
|
52
|
+
).encode("utf-8")
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _size(value: int) -> bool:
|
|
57
|
+
return type(value) is int and 0 <= value <= 65536
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DiagnosticCaptureBudget:
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
self._windows: dict[str, Window] = {}
|
|
63
|
+
self._pending: dict[str, Pending] = {}
|
|
64
|
+
self._held = 0
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def accounted_bytes(self) -> int:
|
|
68
|
+
return self._held + sum(window.cost for window in self._windows.values())
|
|
69
|
+
|
|
70
|
+
def clear(self) -> None:
|
|
71
|
+
self._windows.clear()
|
|
72
|
+
self._pending.clear()
|
|
73
|
+
self._held = 0
|
|
74
|
+
|
|
75
|
+
def _prune(self, now: float) -> None:
|
|
76
|
+
self._windows = {
|
|
77
|
+
key: value for key, value in self._windows.items() if value.pending or now < value.starts + 300_000
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def register_failure(self, payload: dict[str, Any], event_id: str, now: float) -> bool:
|
|
81
|
+
if not math.isfinite(now):
|
|
82
|
+
return False
|
|
83
|
+
self._prune(now)
|
|
84
|
+
key = _key(payload)
|
|
85
|
+
previous = self._windows.get(key)
|
|
86
|
+
if previous is not None and previous.failure_id is not None and previous.failure_id != event_id:
|
|
87
|
+
previous.ambiguous = True
|
|
88
|
+
previous.cost = _cost(key, previous)
|
|
89
|
+
return False
|
|
90
|
+
if previous is None and len(self._windows) >= 50:
|
|
91
|
+
return False
|
|
92
|
+
value = replace(previous) if previous is not None else Window(now)
|
|
93
|
+
value.failure_id = event_id
|
|
94
|
+
value.failure_at = value.failure_at if value.failure_at is not None else now
|
|
95
|
+
value.cost = _cost(key, value)
|
|
96
|
+
if self.accounted_bytes - (previous.cost if previous is not None else 0) + value.cost > 65536:
|
|
97
|
+
return False
|
|
98
|
+
self._windows[key] = value
|
|
99
|
+
return not value.ambiguous
|
|
100
|
+
|
|
101
|
+
def reserve(self, event_id: str, payload: dict[str, Any], size: int, now: float) -> bool:
|
|
102
|
+
if not _size(size) or not math.isfinite(now) or event_id in self._pending:
|
|
103
|
+
return False
|
|
104
|
+
self._prune(now)
|
|
105
|
+
key = _key(payload)
|
|
106
|
+
previous = self._windows.get(key)
|
|
107
|
+
if (previous is None and len(self._windows) >= 50) or (previous is not None and previous.ambiguous):
|
|
108
|
+
return False
|
|
109
|
+
value = replace(previous, attempts=set(previous.attempts)) if previous is not None else Window(now)
|
|
110
|
+
if (
|
|
111
|
+
now < value.starts
|
|
112
|
+
or now > (value.failure_at if value.failure_at is not None else value.starts) + 300_000
|
|
113
|
+
or value.observations >= 20
|
|
114
|
+
):
|
|
115
|
+
return False
|
|
116
|
+
attempt = payload["attempt_id"] if payload["kind"] == "recovery" else None
|
|
117
|
+
if attempt is not None:
|
|
118
|
+
if attempt not in value.attempts and len(value.attempts) >= 5:
|
|
119
|
+
return False
|
|
120
|
+
value.attempts.add(attempt)
|
|
121
|
+
else:
|
|
122
|
+
if (
|
|
123
|
+
value.resource
|
|
124
|
+
or value.failure_id != payload["source_event_id"]
|
|
125
|
+
or value.failure_at is None
|
|
126
|
+
or now > value.failure_at + 30_000
|
|
127
|
+
):
|
|
128
|
+
return False
|
|
129
|
+
value.resource = True
|
|
130
|
+
value.pending += 1
|
|
131
|
+
value.observations += 1
|
|
132
|
+
value.cost = _cost(key, value)
|
|
133
|
+
cost = size + len(event_id) + len(key) + 64
|
|
134
|
+
if self.accounted_bytes - (previous.cost if previous is not None else 0) + value.cost + cost > 65536:
|
|
135
|
+
return False
|
|
136
|
+
self._windows[key] = value
|
|
137
|
+
self._pending[event_id] = Pending(key, payload["kind"], attempt, size, cost)
|
|
138
|
+
self._held += cost
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
def replace(self, original_id: str, event_id: str, payload: dict[str, Any], size: int, now: float) -> bool:
|
|
142
|
+
entry = self._pending.get(original_id)
|
|
143
|
+
key, attempt = _key(payload), payload.get("attempt_id")
|
|
144
|
+
if entry is not None and entry.scope == key and entry.kind == payload["kind"] and entry.attempt == attempt:
|
|
145
|
+
if event_id != original_id:
|
|
146
|
+
if event_id in self._pending:
|
|
147
|
+
self.release([original_id])
|
|
148
|
+
return False
|
|
149
|
+
self._pending[event_id] = self._pending.pop(original_id)
|
|
150
|
+
delta = len(event_id) - len(original_id)
|
|
151
|
+
entry.cost += delta
|
|
152
|
+
self._held += delta
|
|
153
|
+
if not self.adjust(event_id, size) or not self.eligible(event_id, payload):
|
|
154
|
+
self.release([event_id])
|
|
155
|
+
return False
|
|
156
|
+
return True
|
|
157
|
+
self.release([original_id])
|
|
158
|
+
return self.reserve(event_id, payload, size, now)
|
|
159
|
+
|
|
160
|
+
def adjust(self, event_id: str, size: int) -> bool:
|
|
161
|
+
entry = self._pending.get(event_id)
|
|
162
|
+
if entry is None or not _size(size) or self.accounted_bytes + size - entry.size > 65536:
|
|
163
|
+
return False
|
|
164
|
+
self._held += size - entry.size
|
|
165
|
+
entry.cost += size - entry.size
|
|
166
|
+
entry.size = size
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
def eligible(self, event_id: str, payload: dict[str, Any]) -> bool:
|
|
170
|
+
entry, value = self._pending.get(event_id), self._windows.get(_key(payload))
|
|
171
|
+
return (
|
|
172
|
+
entry is not None
|
|
173
|
+
and entry.scope == _key(payload)
|
|
174
|
+
and value is not None
|
|
175
|
+
and not value.ambiguous
|
|
176
|
+
and (payload["kind"] == "recovery" or value.failure_id == payload["source_event_id"])
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def release(self, event_ids: list[str]) -> None:
|
|
180
|
+
for event_id in event_ids:
|
|
181
|
+
entry = self._pending.pop(event_id, None)
|
|
182
|
+
if entry is None:
|
|
183
|
+
continue
|
|
184
|
+
self._held -= entry.cost
|
|
185
|
+
value = self._windows.get(entry.scope)
|
|
186
|
+
if value is not None:
|
|
187
|
+
value.pending = max(0, value.pending - 1)
|
|
188
|
+
value.cost = _cost(entry.scope, value)
|
|
189
|
+
|
|
190
|
+
def retain(self, event_ids: set[str]) -> None:
|
|
191
|
+
self.release([event_id for event_id in self._pending if event_id not in event_ids])
|