Skip to content

Commit 83e206a

Browse files
Merge branch 'master' into webb/log-batcher-upper-bound
2 parents 90520d3 + faa327c commit 83e206a

File tree

8 files changed

+112
-28
lines changed

8 files changed

+112
-28
lines changed

sentry_sdk/_metrics_batcher.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,18 @@
1313

1414
class MetricsBatcher:
1515
MAX_METRICS_BEFORE_FLUSH = 1000
16+
MAX_METRICS_BEFORE_DROP = 10_000
1617
FLUSH_WAIT_TIME = 5.0
1718

1819
def __init__(
1920
self,
2021
capture_func, # type: Callable[[Envelope], None]
22+
record_lost_func, # type: Callable[..., None]
2123
):
2224
# type: (...) -> None
2325
self._metric_buffer = [] # type: List[Metric]
2426
self._capture_func = capture_func
27+
self._record_lost_func = record_lost_func
2528
self._running = True
2629
self._lock = threading.Lock()
2730

@@ -72,6 +75,14 @@ def add(
7275
return None
7376

7477
with self._lock:
78+
if len(self._metric_buffer) >= self.MAX_METRICS_BEFORE_DROP:
79+
self._record_lost_func(
80+
reason="queue_overflow",
81+
data_category="trace_metric",
82+
quantity=1,
83+
)
84+
return None
85+
7586
self._metric_buffer.append(metric)
7687
if len(self._metric_buffer) >= self.MAX_METRICS_BEFORE_FLUSH:
7788
self._flush_event.set()

sentry_sdk/client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,10 @@ def _record_lost_event(
393393

394394
self.metrics_batcher = None
395395
if has_metrics_enabled(self.options):
396-
self.metrics_batcher = MetricsBatcher(capture_func=_capture_envelope)
396+
self.metrics_batcher = MetricsBatcher(
397+
capture_func=_capture_envelope,
398+
record_lost_func=_record_lost_event,
399+
)
397400

398401
max_request_body_size = ("always", "never", "small", "medium")
399402
if self.options["max_request_body_size"] not in max_request_body_size:

sentry_sdk/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,7 @@ def __init__(
10121012
before_send_log=None, # type: Optional[Callable[[Log, Hint], Optional[Log]]]
10131013
trace_ignore_status_codes=frozenset(), # type: AbstractSet[int]
10141014
enable_metrics=True, # type: bool
1015+
before_send_metric=None, # type: Optional[Callable[[Metric, Hint], Optional[Metric]]]
10151016
):
10161017
# type: (...) -> None
10171018
"""Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`.

sentry_sdk/integrations/pydantic_ai/patches/agent_run.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
from functools import wraps
22

33
import sentry_sdk
4-
from sentry_sdk.tracing_utils import set_span_errored
5-
from sentry_sdk.utils import event_from_exception
64

75
from ..spans import invoke_agent_span, update_invoke_agent_span
6+
from ..utils import _capture_exception
87

98
from typing import TYPE_CHECKING
109
from pydantic_ai.agent import Agent # type: ignore
@@ -13,18 +12,6 @@
1312
from typing import Any, Callable, Optional
1413

1514

16-
def _capture_exception(exc):
17-
# type: (Any) -> None
18-
set_span_errored()
19-
20-
event, hint = event_from_exception(
21-
exc,
22-
client_options=sentry_sdk.get_client().options,
23-
mechanism={"type": "pydantic_ai", "handled": False},
24-
)
25-
sentry_sdk.capture_event(event, hint=hint)
26-
27-
2815
class _StreamingContextManagerWrapper:
2916
"""Wrapper for streaming methods that return async context managers."""
3017

sentry_sdk/integrations/pydantic_ai/patches/tools.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sentry_sdk
66

77
from ..spans import execute_tool_span, update_execute_tool_span
8+
from ..utils import _capture_exception
89

910
from typing import TYPE_CHECKING
1011

@@ -56,16 +57,21 @@ async def wrapped_call_tool(self, call, allow_partial, wrap_validation_errors):
5657
)
5758
agent = agent_data.get("_agent")
5859

59-
# Get args for span (before validation)
60-
# call.args can be a string (JSON) or dict
61-
args_dict = call.args if isinstance(call.args, dict) else {}
60+
try:
61+
args_dict = call.args_as_dict()
62+
except Exception:
63+
args_dict = call.args if isinstance(call.args, dict) else {}
6264

6365
with execute_tool_span(name, args_dict, agent, tool_type=tool_type) as span:
64-
result = await original_call_tool(
65-
self, call, allow_partial, wrap_validation_errors
66-
)
67-
update_execute_tool_span(span, result)
68-
return result
66+
try:
67+
result = await original_call_tool(
68+
self, call, allow_partial, wrap_validation_errors
69+
)
70+
update_execute_tool_span(span, result)
71+
return result
72+
except Exception as exc:
73+
_capture_exception(exc)
74+
raise exc from None
6975

7076
# No span context - just call original
7177
return await original_call_tool(

sentry_sdk/integrations/pydantic_ai/utils.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import sentry_sdk
2-
from sentry_sdk.ai.utils import set_data_normalized
32
from sentry_sdk.consts import SPANDATA
43
from sentry_sdk.scope import should_send_default_pii
5-
from sentry_sdk.utils import safe_serialize
4+
from sentry_sdk.tracing_utils import set_span_errored
5+
from sentry_sdk.utils import event_from_exception, safe_serialize
66

77
from typing import TYPE_CHECKING
88

99
if TYPE_CHECKING:
10-
from typing import Any, List, Dict
11-
from pydantic_ai.usage import RequestUsage # type: ignore
10+
from typing import Any
1211

1312

1413
def _should_send_prompts():
@@ -173,3 +172,15 @@ def _set_available_tools(span, agent):
173172
except Exception:
174173
# If we can't extract tools, just skip it
175174
pass
175+
176+
177+
def _capture_exception(exc):
178+
# type: (Any) -> None
179+
set_span_errored()
180+
181+
event, hint = event_from_exception(
182+
exc,
183+
client_options=sentry_sdk.get_client().options,
184+
mechanism={"type": "pydantic_ai", "handled": False},
185+
)
186+
sentry_sdk.capture_event(event, hint=hint)

sentry_sdk/utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2060,4 +2060,6 @@ def get_before_send_metric(options):
20602060
if options is None:
20612061
return None
20622062

2063-
return options["_experiments"].get("before_send_metric")
2063+
return options.get("before_send_metric") or options["_experiments"].get(
2064+
"before_send_metric"
2065+
)

tests/test_metrics.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,45 @@ def test_metrics_tracing_without_performance(sentry_init, capture_envelopes):
168168
def test_metrics_before_send(sentry_init, capture_envelopes):
169169
before_metric_called = False
170170

171+
def _before_metric(record, hint):
172+
nonlocal before_metric_called
173+
174+
assert set(record.keys()) == {
175+
"timestamp",
176+
"trace_id",
177+
"span_id",
178+
"name",
179+
"type",
180+
"value",
181+
"unit",
182+
"attributes",
183+
}
184+
185+
if record["name"] == "test.skip":
186+
return None
187+
188+
before_metric_called = True
189+
return record
190+
191+
sentry_init(
192+
before_send_metric=_before_metric,
193+
)
194+
envelopes = capture_envelopes()
195+
196+
sentry_sdk.metrics.count("test.skip", 1)
197+
sentry_sdk.metrics.count("test.keep", 1)
198+
199+
get_client().flush()
200+
201+
metrics = envelopes_to_metrics(envelopes)
202+
assert len(metrics) == 1
203+
assert metrics[0]["name"] == "test.keep"
204+
assert before_metric_called
205+
206+
207+
def test_metrics_experimental_before_send(sentry_init, capture_envelopes):
208+
before_metric_called = False
209+
171210
def _before_metric(record, hint):
172211
nonlocal before_metric_called
173212

@@ -204,3 +243,27 @@ def _before_metric(record, hint):
204243
assert len(metrics) == 1
205244
assert metrics[0]["name"] == "test.keep"
206245
assert before_metric_called
246+
247+
248+
def test_batcher_drops_metrics(sentry_init, monkeypatch):
249+
sentry_init()
250+
client = sentry_sdk.get_client()
251+
252+
def no_op_flush():
253+
pass
254+
255+
monkeypatch.setattr(client.metrics_batcher, "_flush", no_op_flush)
256+
257+
lost_event_calls = []
258+
259+
def record_lost_event(reason, data_category, quantity):
260+
lost_event_calls.append((reason, data_category, quantity))
261+
262+
monkeypatch.setattr(client.metrics_batcher, "_record_lost_func", record_lost_event)
263+
264+
for i in range(10_005): # 5 metrics over the hard limit
265+
sentry_sdk.metrics.count("test.counter", 1)
266+
267+
assert len(lost_event_calls) == 5
268+
for lost_event_call in lost_event_calls:
269+
assert lost_event_call == ("queue_overflow", "trace_metric", 1)

0 commit comments

Comments
 (0)