sentry-sdk 2.40.0__py2.py3-none-any.whl → 2.42.0__py2.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.
Potentially problematic release.
This version of sentry-sdk might be problematic. Click here for more details.
- sentry_sdk/_metrics.py +81 -0
- sentry_sdk/_metrics_batcher.py +156 -0
- sentry_sdk/_types.py +27 -22
- sentry_sdk/ai/__init__.py +7 -0
- sentry_sdk/ai/utils.py +48 -0
- sentry_sdk/client.py +81 -30
- sentry_sdk/consts.py +13 -8
- sentry_sdk/envelope.py +3 -3
- sentry_sdk/integrations/__init__.py +1 -0
- sentry_sdk/integrations/aiohttp.py +4 -1
- sentry_sdk/integrations/anthropic.py +10 -2
- sentry_sdk/integrations/google_genai/__init__.py +298 -0
- sentry_sdk/integrations/google_genai/consts.py +16 -0
- sentry_sdk/integrations/google_genai/streaming.py +155 -0
- sentry_sdk/integrations/google_genai/utils.py +566 -0
- sentry_sdk/integrations/httpx.py +16 -5
- sentry_sdk/integrations/langchain.py +29 -4
- sentry_sdk/integrations/langgraph.py +5 -3
- sentry_sdk/integrations/logging.py +1 -1
- sentry_sdk/integrations/loguru.py +1 -1
- sentry_sdk/integrations/openai.py +3 -2
- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +10 -2
- sentry_sdk/integrations/openai_agents/utils.py +35 -18
- sentry_sdk/integrations/ray.py +20 -4
- sentry_sdk/integrations/stdlib.py +8 -1
- sentry_sdk/integrations/threading.py +52 -8
- sentry_sdk/logger.py +1 -1
- sentry_sdk/tracing.py +0 -26
- sentry_sdk/tracing_utils.py +64 -24
- sentry_sdk/transport.py +1 -17
- sentry_sdk/types.py +3 -0
- sentry_sdk/utils.py +17 -1
- {sentry_sdk-2.40.0.dist-info → sentry_sdk-2.42.0.dist-info}/METADATA +3 -1
- {sentry_sdk-2.40.0.dist-info → sentry_sdk-2.42.0.dist-info}/RECORD +38 -33
- sentry_sdk/metrics.py +0 -971
- {sentry_sdk-2.40.0.dist-info → sentry_sdk-2.42.0.dist-info}/WHEEL +0 -0
- {sentry_sdk-2.40.0.dist-info → sentry_sdk-2.42.0.dist-info}/entry_points.txt +0 -0
- {sentry_sdk-2.40.0.dist-info → sentry_sdk-2.42.0.dist-info}/licenses/LICENSE +0 -0
- {sentry_sdk-2.40.0.dist-info → sentry_sdk-2.42.0.dist-info}/top_level.txt +0 -0
sentry_sdk/_metrics.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NOTE: This file contains experimental code that may be changed or removed at any
|
|
3
|
+
time without prior notice.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Optional, TYPE_CHECKING, Union
|
|
8
|
+
|
|
9
|
+
import sentry_sdk
|
|
10
|
+
from sentry_sdk.utils import safe_repr
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from sentry_sdk._types import Metric, MetricType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _capture_metric(
|
|
17
|
+
name, # type: str
|
|
18
|
+
metric_type, # type: MetricType
|
|
19
|
+
value, # type: float
|
|
20
|
+
unit=None, # type: Optional[str]
|
|
21
|
+
attributes=None, # type: Optional[dict[str, Any]]
|
|
22
|
+
):
|
|
23
|
+
# type: (...) -> None
|
|
24
|
+
client = sentry_sdk.get_client()
|
|
25
|
+
|
|
26
|
+
attrs = {} # type: dict[str, Union[str, bool, float, int]]
|
|
27
|
+
if attributes:
|
|
28
|
+
for k, v in attributes.items():
|
|
29
|
+
attrs[k] = (
|
|
30
|
+
v
|
|
31
|
+
if (
|
|
32
|
+
isinstance(v, str)
|
|
33
|
+
or isinstance(v, int)
|
|
34
|
+
or isinstance(v, bool)
|
|
35
|
+
or isinstance(v, float)
|
|
36
|
+
)
|
|
37
|
+
else safe_repr(v)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
metric = {
|
|
41
|
+
"timestamp": time.time(),
|
|
42
|
+
"trace_id": None,
|
|
43
|
+
"span_id": None,
|
|
44
|
+
"name": name,
|
|
45
|
+
"type": metric_type,
|
|
46
|
+
"value": float(value),
|
|
47
|
+
"unit": unit,
|
|
48
|
+
"attributes": attrs,
|
|
49
|
+
} # type: Metric
|
|
50
|
+
|
|
51
|
+
client._capture_metric(metric)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def count(
|
|
55
|
+
name, # type: str
|
|
56
|
+
value, # type: float
|
|
57
|
+
unit=None, # type: Optional[str]
|
|
58
|
+
attributes=None, # type: Optional[dict[str, Any]]
|
|
59
|
+
):
|
|
60
|
+
# type: (...) -> None
|
|
61
|
+
_capture_metric(name, "counter", value, unit, attributes)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def gauge(
|
|
65
|
+
name, # type: str
|
|
66
|
+
value, # type: float
|
|
67
|
+
unit=None, # type: Optional[str]
|
|
68
|
+
attributes=None, # type: Optional[dict[str, Any]]
|
|
69
|
+
):
|
|
70
|
+
# type: (...) -> None
|
|
71
|
+
_capture_metric(name, "gauge", value, unit, attributes)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def distribution(
|
|
75
|
+
name, # type: str
|
|
76
|
+
value, # type: float
|
|
77
|
+
unit=None, # type: Optional[str]
|
|
78
|
+
attributes=None, # type: Optional[dict[str, Any]]
|
|
79
|
+
):
|
|
80
|
+
# type: (...) -> None
|
|
81
|
+
_capture_metric(name, "distribution", value, unit, attributes)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import random
|
|
3
|
+
import threading
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Optional, List, Callable, TYPE_CHECKING, Any, Union
|
|
6
|
+
|
|
7
|
+
from sentry_sdk.utils import format_timestamp, safe_repr
|
|
8
|
+
from sentry_sdk.envelope import Envelope, Item, PayloadRef
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from sentry_sdk._types import Metric
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MetricsBatcher:
|
|
15
|
+
MAX_METRICS_BEFORE_FLUSH = 100
|
|
16
|
+
FLUSH_WAIT_TIME = 5.0
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
capture_func, # type: Callable[[Envelope], None]
|
|
21
|
+
):
|
|
22
|
+
# type: (...) -> None
|
|
23
|
+
self._metric_buffer = [] # type: List[Metric]
|
|
24
|
+
self._capture_func = capture_func
|
|
25
|
+
self._running = True
|
|
26
|
+
self._lock = threading.Lock()
|
|
27
|
+
|
|
28
|
+
self._flush_event = threading.Event() # type: threading.Event
|
|
29
|
+
|
|
30
|
+
self._flusher = None # type: Optional[threading.Thread]
|
|
31
|
+
self._flusher_pid = None # type: Optional[int]
|
|
32
|
+
|
|
33
|
+
def _ensure_thread(self):
|
|
34
|
+
# type: (...) -> bool
|
|
35
|
+
if not self._running:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
pid = os.getpid()
|
|
39
|
+
if self._flusher_pid == pid:
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
with self._lock:
|
|
43
|
+
if self._flusher_pid == pid:
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
self._flusher_pid = pid
|
|
47
|
+
|
|
48
|
+
self._flusher = threading.Thread(target=self._flush_loop)
|
|
49
|
+
self._flusher.daemon = True
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
self._flusher.start()
|
|
53
|
+
except RuntimeError:
|
|
54
|
+
self._running = False
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
def _flush_loop(self):
|
|
60
|
+
# type: (...) -> None
|
|
61
|
+
while self._running:
|
|
62
|
+
self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random())
|
|
63
|
+
self._flush_event.clear()
|
|
64
|
+
self._flush()
|
|
65
|
+
|
|
66
|
+
def add(
|
|
67
|
+
self,
|
|
68
|
+
metric, # type: Metric
|
|
69
|
+
):
|
|
70
|
+
# type: (...) -> None
|
|
71
|
+
if not self._ensure_thread() or self._flusher is None:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
with self._lock:
|
|
75
|
+
self._metric_buffer.append(metric)
|
|
76
|
+
if len(self._metric_buffer) >= self.MAX_METRICS_BEFORE_FLUSH:
|
|
77
|
+
self._flush_event.set()
|
|
78
|
+
|
|
79
|
+
def kill(self):
|
|
80
|
+
# type: (...) -> None
|
|
81
|
+
if self._flusher is None:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
self._running = False
|
|
85
|
+
self._flush_event.set()
|
|
86
|
+
self._flusher = None
|
|
87
|
+
|
|
88
|
+
def flush(self):
|
|
89
|
+
# type: (...) -> None
|
|
90
|
+
self._flush()
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _metric_to_transport_format(metric):
|
|
94
|
+
# type: (Metric) -> Any
|
|
95
|
+
def format_attribute(val):
|
|
96
|
+
# type: (Union[int, float, str, bool]) -> Any
|
|
97
|
+
if isinstance(val, bool):
|
|
98
|
+
return {"value": val, "type": "boolean"}
|
|
99
|
+
if isinstance(val, int):
|
|
100
|
+
return {"value": val, "type": "integer"}
|
|
101
|
+
if isinstance(val, float):
|
|
102
|
+
return {"value": val, "type": "double"}
|
|
103
|
+
if isinstance(val, str):
|
|
104
|
+
return {"value": val, "type": "string"}
|
|
105
|
+
return {"value": safe_repr(val), "type": "string"}
|
|
106
|
+
|
|
107
|
+
res = {
|
|
108
|
+
"timestamp": metric["timestamp"],
|
|
109
|
+
"trace_id": metric["trace_id"],
|
|
110
|
+
"name": metric["name"],
|
|
111
|
+
"type": metric["type"],
|
|
112
|
+
"value": metric["value"],
|
|
113
|
+
"attributes": {
|
|
114
|
+
k: format_attribute(v) for (k, v) in metric["attributes"].items()
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if metric.get("span_id") is not None:
|
|
119
|
+
res["span_id"] = metric["span_id"]
|
|
120
|
+
|
|
121
|
+
if metric.get("unit") is not None:
|
|
122
|
+
res["unit"] = metric["unit"]
|
|
123
|
+
|
|
124
|
+
return res
|
|
125
|
+
|
|
126
|
+
def _flush(self):
|
|
127
|
+
# type: (...) -> Optional[Envelope]
|
|
128
|
+
|
|
129
|
+
envelope = Envelope(
|
|
130
|
+
headers={"sent_at": format_timestamp(datetime.now(timezone.utc))}
|
|
131
|
+
)
|
|
132
|
+
with self._lock:
|
|
133
|
+
if len(self._metric_buffer) == 0:
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
envelope.add_item(
|
|
137
|
+
Item(
|
|
138
|
+
type="trace_metric",
|
|
139
|
+
content_type="application/vnd.sentry.items.trace-metric+json",
|
|
140
|
+
headers={
|
|
141
|
+
"item_count": len(self._metric_buffer),
|
|
142
|
+
},
|
|
143
|
+
payload=PayloadRef(
|
|
144
|
+
json={
|
|
145
|
+
"items": [
|
|
146
|
+
self._metric_to_transport_format(metric)
|
|
147
|
+
for metric in self._metric_buffer
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
self._metric_buffer.clear()
|
|
154
|
+
|
|
155
|
+
self._capture_func(envelope)
|
|
156
|
+
return envelope
|
sentry_sdk/_types.py
CHANGED
|
@@ -210,7 +210,6 @@ if TYPE_CHECKING:
|
|
|
210
210
|
"type": Literal["check_in", "transaction"],
|
|
211
211
|
"user": dict[str, object],
|
|
212
212
|
"_dropped_spans": int,
|
|
213
|
-
"_metrics_summary": dict[str, object],
|
|
214
213
|
},
|
|
215
214
|
total=False,
|
|
216
215
|
)
|
|
@@ -235,6 +234,32 @@ if TYPE_CHECKING:
|
|
|
235
234
|
},
|
|
236
235
|
)
|
|
237
236
|
|
|
237
|
+
MetricType = Literal["counter", "gauge", "distribution"]
|
|
238
|
+
|
|
239
|
+
MetricAttributeValue = TypedDict(
|
|
240
|
+
"MetricAttributeValue",
|
|
241
|
+
{
|
|
242
|
+
"value": Union[str, bool, float, int],
|
|
243
|
+
"type": Literal["string", "boolean", "double", "integer"],
|
|
244
|
+
},
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
Metric = TypedDict(
|
|
248
|
+
"Metric",
|
|
249
|
+
{
|
|
250
|
+
"timestamp": float,
|
|
251
|
+
"trace_id": Optional[str],
|
|
252
|
+
"span_id": Optional[str],
|
|
253
|
+
"name": str,
|
|
254
|
+
"type": MetricType,
|
|
255
|
+
"value": float,
|
|
256
|
+
"unit": Optional[str],
|
|
257
|
+
"attributes": dict[str, str | bool | float | int],
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
MetricProcessor = Callable[[Metric, Hint], Optional[Metric]]
|
|
262
|
+
|
|
238
263
|
# TODO: Make a proper type definition for this (PRs welcome!)
|
|
239
264
|
Breadcrumb = Dict[str, Any]
|
|
240
265
|
|
|
@@ -266,36 +291,16 @@ if TYPE_CHECKING:
|
|
|
266
291
|
"internal",
|
|
267
292
|
"profile",
|
|
268
293
|
"profile_chunk",
|
|
269
|
-
"metric_bucket",
|
|
270
294
|
"monitor",
|
|
271
295
|
"span",
|
|
272
296
|
"log_item",
|
|
297
|
+
"trace_metric",
|
|
273
298
|
]
|
|
274
299
|
SessionStatus = Literal["ok", "exited", "crashed", "abnormal"]
|
|
275
300
|
|
|
276
301
|
ContinuousProfilerMode = Literal["thread", "gevent", "unknown"]
|
|
277
302
|
ProfilerMode = Union[ContinuousProfilerMode, Literal["sleep"]]
|
|
278
303
|
|
|
279
|
-
# Type of the metric.
|
|
280
|
-
MetricType = Literal["d", "s", "g", "c"]
|
|
281
|
-
|
|
282
|
-
# Value of the metric.
|
|
283
|
-
MetricValue = Union[int, float, str]
|
|
284
|
-
|
|
285
|
-
# Internal representation of tags as a tuple of tuples (this is done in order to allow for the same key to exist
|
|
286
|
-
# multiple times).
|
|
287
|
-
MetricTagsInternal = Tuple[Tuple[str, str], ...]
|
|
288
|
-
|
|
289
|
-
# External representation of tags as a dictionary.
|
|
290
|
-
MetricTagValue = Union[str, int, float, None]
|
|
291
|
-
MetricTags = Mapping[str, MetricTagValue]
|
|
292
|
-
|
|
293
|
-
# Value inside the generator for the metric value.
|
|
294
|
-
FlushedMetricValue = Union[int, float]
|
|
295
|
-
|
|
296
|
-
BucketKey = Tuple[MetricType, str, MeasurementUnit, MetricTagsInternal]
|
|
297
|
-
MetricMetaKey = Tuple[MetricType, str, MeasurementUnit]
|
|
298
|
-
|
|
299
304
|
MonitorConfigScheduleType = Literal["crontab", "interval"]
|
|
300
305
|
MonitorConfigScheduleUnit = Literal[
|
|
301
306
|
"year",
|
sentry_sdk/ai/__init__.py
CHANGED
sentry_sdk/ai/utils.py
CHANGED
|
@@ -10,6 +10,26 @@ import sentry_sdk
|
|
|
10
10
|
from sentry_sdk.utils import logger
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class GEN_AI_ALLOWED_MESSAGE_ROLES:
|
|
14
|
+
SYSTEM = "system"
|
|
15
|
+
USER = "user"
|
|
16
|
+
ASSISTANT = "assistant"
|
|
17
|
+
TOOL = "tool"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING = {
|
|
21
|
+
GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM: ["system"],
|
|
22
|
+
GEN_AI_ALLOWED_MESSAGE_ROLES.USER: ["user", "human"],
|
|
23
|
+
GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT: ["assistant", "ai"],
|
|
24
|
+
GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL: ["tool", "tool_call"],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
GEN_AI_MESSAGE_ROLE_MAPPING = {}
|
|
28
|
+
for target_role, source_roles in GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING.items():
|
|
29
|
+
for source_role in source_roles:
|
|
30
|
+
GEN_AI_MESSAGE_ROLE_MAPPING[source_role] = target_role
|
|
31
|
+
|
|
32
|
+
|
|
13
33
|
def _normalize_data(data, unpack=True):
|
|
14
34
|
# type: (Any, bool) -> Any
|
|
15
35
|
# convert pydantic data (e.g. OpenAI v1+) to json compatible format
|
|
@@ -40,6 +60,34 @@ def set_data_normalized(span, key, value, unpack=True):
|
|
|
40
60
|
span.set_data(key, json.dumps(normalized))
|
|
41
61
|
|
|
42
62
|
|
|
63
|
+
def normalize_message_role(role):
|
|
64
|
+
# type: (str) -> str
|
|
65
|
+
"""
|
|
66
|
+
Normalize a message role to one of the 4 allowed gen_ai role values.
|
|
67
|
+
Maps "ai" -> "assistant" and keeps other standard roles unchanged.
|
|
68
|
+
"""
|
|
69
|
+
return GEN_AI_MESSAGE_ROLE_MAPPING.get(role, role)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def normalize_message_roles(messages):
|
|
73
|
+
# type: (list[dict[str, Any]]) -> list[dict[str, Any]]
|
|
74
|
+
"""
|
|
75
|
+
Normalize roles in a list of messages to use standard gen_ai role values.
|
|
76
|
+
Creates a deep copy to avoid modifying the original messages.
|
|
77
|
+
"""
|
|
78
|
+
normalized_messages = []
|
|
79
|
+
for message in messages:
|
|
80
|
+
if not isinstance(message, dict):
|
|
81
|
+
normalized_messages.append(message)
|
|
82
|
+
continue
|
|
83
|
+
normalized_message = message.copy()
|
|
84
|
+
if "role" in message:
|
|
85
|
+
normalized_message["role"] = normalize_message_role(message["role"])
|
|
86
|
+
normalized_messages.append(normalized_message)
|
|
87
|
+
|
|
88
|
+
return normalized_messages
|
|
89
|
+
|
|
90
|
+
|
|
43
91
|
def get_start_span_function():
|
|
44
92
|
# type: () -> Callable[..., Any]
|
|
45
93
|
current_span = sentry_sdk.get_current_span()
|
sentry_sdk/client.py
CHANGED
|
@@ -24,7 +24,9 @@ from sentry_sdk.utils import (
|
|
|
24
24
|
is_gevent,
|
|
25
25
|
logger,
|
|
26
26
|
get_before_send_log,
|
|
27
|
+
get_before_send_metric,
|
|
27
28
|
has_logs_enabled,
|
|
29
|
+
has_metrics_enabled,
|
|
28
30
|
)
|
|
29
31
|
from sentry_sdk.serializer import serialize
|
|
30
32
|
from sentry_sdk.tracing import trace
|
|
@@ -59,14 +61,14 @@ if TYPE_CHECKING:
|
|
|
59
61
|
from typing import Union
|
|
60
62
|
from typing import TypeVar
|
|
61
63
|
|
|
62
|
-
from sentry_sdk._types import Event, Hint, SDKInfo, Log
|
|
64
|
+
from sentry_sdk._types import Event, Hint, SDKInfo, Log, Metric
|
|
63
65
|
from sentry_sdk.integrations import Integration
|
|
64
|
-
from sentry_sdk.metrics import MetricsAggregator
|
|
65
66
|
from sentry_sdk.scope import Scope
|
|
66
67
|
from sentry_sdk.session import Session
|
|
67
68
|
from sentry_sdk.spotlight import SpotlightClient
|
|
68
69
|
from sentry_sdk.transport import Transport
|
|
69
70
|
from sentry_sdk._log_batcher import LogBatcher
|
|
71
|
+
from sentry_sdk._metrics_batcher import MetricsBatcher
|
|
70
72
|
|
|
71
73
|
I = TypeVar("I", bound=Integration) # noqa: E741
|
|
72
74
|
|
|
@@ -182,8 +184,8 @@ class BaseClient:
|
|
|
182
184
|
|
|
183
185
|
self.transport = None # type: Optional[Transport]
|
|
184
186
|
self.monitor = None # type: Optional[Monitor]
|
|
185
|
-
self.metrics_aggregator = None # type: Optional[MetricsAggregator]
|
|
186
187
|
self.log_batcher = None # type: Optional[LogBatcher]
|
|
188
|
+
self.metrics_batcher = None # type: Optional[MetricsBatcher]
|
|
187
189
|
|
|
188
190
|
def __getstate__(self, *args, **kwargs):
|
|
189
191
|
# type: (*Any, **Any) -> Any
|
|
@@ -215,10 +217,14 @@ class BaseClient:
|
|
|
215
217
|
# type: (*Any, **Any) -> Optional[str]
|
|
216
218
|
return None
|
|
217
219
|
|
|
218
|
-
def
|
|
220
|
+
def _capture_log(self, log):
|
|
219
221
|
# type: (Log) -> None
|
|
220
222
|
pass
|
|
221
223
|
|
|
224
|
+
def _capture_metric(self, metric):
|
|
225
|
+
# type: (Metric) -> None
|
|
226
|
+
pass
|
|
227
|
+
|
|
222
228
|
def capture_session(self, *args, **kwargs):
|
|
223
229
|
# type: (*Any, **Any) -> None
|
|
224
230
|
return None
|
|
@@ -361,26 +367,6 @@ class _Client(BaseClient):
|
|
|
361
367
|
|
|
362
368
|
self.session_flusher = SessionFlusher(capture_func=_capture_envelope)
|
|
363
369
|
|
|
364
|
-
self.metrics_aggregator = None # type: Optional[MetricsAggregator]
|
|
365
|
-
experiments = self.options.get("_experiments", {})
|
|
366
|
-
if experiments.get("enable_metrics", True):
|
|
367
|
-
# Context vars are not working correctly on Python <=3.6
|
|
368
|
-
# with gevent.
|
|
369
|
-
metrics_supported = not is_gevent() or PY37
|
|
370
|
-
if metrics_supported:
|
|
371
|
-
from sentry_sdk.metrics import MetricsAggregator
|
|
372
|
-
|
|
373
|
-
self.metrics_aggregator = MetricsAggregator(
|
|
374
|
-
capture_func=_capture_envelope,
|
|
375
|
-
enable_code_locations=bool(
|
|
376
|
-
experiments.get("metric_code_locations", True)
|
|
377
|
-
),
|
|
378
|
-
)
|
|
379
|
-
else:
|
|
380
|
-
logger.info(
|
|
381
|
-
"Metrics not supported on Python 3.6 and lower with gevent."
|
|
382
|
-
)
|
|
383
|
-
|
|
384
370
|
self.log_batcher = None
|
|
385
371
|
|
|
386
372
|
if has_logs_enabled(self.options):
|
|
@@ -388,6 +374,13 @@ class _Client(BaseClient):
|
|
|
388
374
|
|
|
389
375
|
self.log_batcher = LogBatcher(capture_func=_capture_envelope)
|
|
390
376
|
|
|
377
|
+
self.metrics_batcher = None
|
|
378
|
+
|
|
379
|
+
if has_metrics_enabled(self.options):
|
|
380
|
+
from sentry_sdk._metrics_batcher import MetricsBatcher
|
|
381
|
+
|
|
382
|
+
self.metrics_batcher = MetricsBatcher(capture_func=_capture_envelope)
|
|
383
|
+
|
|
391
384
|
max_request_body_size = ("always", "never", "small", "medium")
|
|
392
385
|
if self.options["max_request_body_size"] not in max_request_body_size:
|
|
393
386
|
raise ValueError(
|
|
@@ -467,7 +460,6 @@ class _Client(BaseClient):
|
|
|
467
460
|
|
|
468
461
|
if (
|
|
469
462
|
self.monitor
|
|
470
|
-
or self.metrics_aggregator
|
|
471
463
|
or self.log_batcher
|
|
472
464
|
or has_profiling_enabled(self.options)
|
|
473
465
|
or isinstance(self.transport, BaseHttpTransport)
|
|
@@ -900,7 +892,7 @@ class _Client(BaseClient):
|
|
|
900
892
|
|
|
901
893
|
return return_value
|
|
902
894
|
|
|
903
|
-
def
|
|
895
|
+
def _capture_log(self, log):
|
|
904
896
|
# type: (Optional[Log]) -> None
|
|
905
897
|
if not has_logs_enabled(self.options) or log is None:
|
|
906
898
|
return
|
|
@@ -967,6 +959,65 @@ class _Client(BaseClient):
|
|
|
967
959
|
if self.log_batcher:
|
|
968
960
|
self.log_batcher.add(log)
|
|
969
961
|
|
|
962
|
+
def _capture_metric(self, metric):
|
|
963
|
+
# type: (Optional[Metric]) -> None
|
|
964
|
+
if not has_metrics_enabled(self.options) or metric is None:
|
|
965
|
+
return
|
|
966
|
+
|
|
967
|
+
isolation_scope = sentry_sdk.get_isolation_scope()
|
|
968
|
+
|
|
969
|
+
metric["attributes"]["sentry.sdk.name"] = SDK_INFO["name"]
|
|
970
|
+
metric["attributes"]["sentry.sdk.version"] = SDK_INFO["version"]
|
|
971
|
+
|
|
972
|
+
environment = self.options.get("environment")
|
|
973
|
+
if environment is not None and "sentry.environment" not in metric["attributes"]:
|
|
974
|
+
metric["attributes"]["sentry.environment"] = environment
|
|
975
|
+
|
|
976
|
+
release = self.options.get("release")
|
|
977
|
+
if release is not None and "sentry.release" not in metric["attributes"]:
|
|
978
|
+
metric["attributes"]["sentry.release"] = release
|
|
979
|
+
|
|
980
|
+
span = sentry_sdk.get_current_span()
|
|
981
|
+
metric["trace_id"] = "00000000-0000-0000-0000-000000000000"
|
|
982
|
+
|
|
983
|
+
if span:
|
|
984
|
+
metric["trace_id"] = span.trace_id
|
|
985
|
+
metric["span_id"] = span.span_id
|
|
986
|
+
else:
|
|
987
|
+
propagation_context = isolation_scope.get_active_propagation_context()
|
|
988
|
+
if propagation_context and propagation_context.trace_id:
|
|
989
|
+
metric["trace_id"] = propagation_context.trace_id
|
|
990
|
+
|
|
991
|
+
if isolation_scope._user is not None:
|
|
992
|
+
for metric_attribute, user_attribute in (
|
|
993
|
+
("user.id", "id"),
|
|
994
|
+
("user.name", "username"),
|
|
995
|
+
("user.email", "email"),
|
|
996
|
+
):
|
|
997
|
+
if (
|
|
998
|
+
user_attribute in isolation_scope._user
|
|
999
|
+
and metric_attribute not in metric["attributes"]
|
|
1000
|
+
):
|
|
1001
|
+
metric["attributes"][metric_attribute] = isolation_scope._user[
|
|
1002
|
+
user_attribute
|
|
1003
|
+
]
|
|
1004
|
+
|
|
1005
|
+
debug = self.options.get("debug", False)
|
|
1006
|
+
if debug:
|
|
1007
|
+
logger.debug(
|
|
1008
|
+
f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}"
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
before_send_metric = get_before_send_metric(self.options)
|
|
1012
|
+
if before_send_metric is not None:
|
|
1013
|
+
metric = before_send_metric(metric, {})
|
|
1014
|
+
|
|
1015
|
+
if metric is None:
|
|
1016
|
+
return
|
|
1017
|
+
|
|
1018
|
+
if self.metrics_batcher:
|
|
1019
|
+
self.metrics_batcher.add(metric)
|
|
1020
|
+
|
|
970
1021
|
def capture_session(
|
|
971
1022
|
self,
|
|
972
1023
|
session, # type: Session
|
|
@@ -1019,10 +1070,10 @@ class _Client(BaseClient):
|
|
|
1019
1070
|
if self.transport is not None:
|
|
1020
1071
|
self.flush(timeout=timeout, callback=callback)
|
|
1021
1072
|
self.session_flusher.kill()
|
|
1022
|
-
if self.metrics_aggregator is not None:
|
|
1023
|
-
self.metrics_aggregator.kill()
|
|
1024
1073
|
if self.log_batcher is not None:
|
|
1025
1074
|
self.log_batcher.kill()
|
|
1075
|
+
if self.metrics_batcher is not None:
|
|
1076
|
+
self.metrics_batcher.kill()
|
|
1026
1077
|
if self.monitor:
|
|
1027
1078
|
self.monitor.kill()
|
|
1028
1079
|
self.transport.kill()
|
|
@@ -1045,10 +1096,10 @@ class _Client(BaseClient):
|
|
|
1045
1096
|
if timeout is None:
|
|
1046
1097
|
timeout = self.options["shutdown_timeout"]
|
|
1047
1098
|
self.session_flusher.flush()
|
|
1048
|
-
if self.metrics_aggregator is not None:
|
|
1049
|
-
self.metrics_aggregator.flush()
|
|
1050
1099
|
if self.log_batcher is not None:
|
|
1051
1100
|
self.log_batcher.flush()
|
|
1101
|
+
if self.metrics_batcher is not None:
|
|
1102
|
+
self.metrics_batcher.flush()
|
|
1052
1103
|
self.transport.flush(timeout=timeout, callback=callback)
|
|
1053
1104
|
|
|
1054
1105
|
def __enter__(self):
|
sentry_sdk/consts.py
CHANGED
|
@@ -52,11 +52,10 @@ if TYPE_CHECKING:
|
|
|
52
52
|
Hint,
|
|
53
53
|
Log,
|
|
54
54
|
MeasurementUnit,
|
|
55
|
+
Metric,
|
|
55
56
|
ProfilerMode,
|
|
56
57
|
TracesSampler,
|
|
57
58
|
TransactionProcessor,
|
|
58
|
-
MetricTags,
|
|
59
|
-
MetricValue,
|
|
60
59
|
)
|
|
61
60
|
|
|
62
61
|
# Experiments are feature flags to enable and disable certain unstable SDK
|
|
@@ -77,13 +76,10 @@ if TYPE_CHECKING:
|
|
|
77
76
|
"transport_compression_algo": Optional[CompressionAlgo],
|
|
78
77
|
"transport_num_pools": Optional[int],
|
|
79
78
|
"transport_http2": Optional[bool],
|
|
80
|
-
"enable_metrics": Optional[bool],
|
|
81
|
-
"before_emit_metric": Optional[
|
|
82
|
-
Callable[[str, MetricValue, MeasurementUnit, MetricTags], bool]
|
|
83
|
-
],
|
|
84
|
-
"metric_code_locations": Optional[bool],
|
|
85
79
|
"enable_logs": Optional[bool],
|
|
86
80
|
"before_send_log": Optional[Callable[[Log, Hint], Optional[Log]]],
|
|
81
|
+
"enable_metrics": Optional[bool],
|
|
82
|
+
"before_send_metric": Optional[Callable[[Metric, Hint], Optional[Metric]]],
|
|
87
83
|
},
|
|
88
84
|
total=False,
|
|
89
85
|
)
|
|
@@ -913,6 +909,8 @@ class ClientConstructor:
|
|
|
913
909
|
error_sampler=None, # type: Optional[Callable[[Event, Hint], Union[float, bool]]]
|
|
914
910
|
enable_db_query_source=True, # type: bool
|
|
915
911
|
db_query_source_threshold_ms=100, # type: int
|
|
912
|
+
enable_http_request_source=False, # type: bool
|
|
913
|
+
http_request_source_threshold_ms=100, # type: int
|
|
916
914
|
spotlight=None, # type: Optional[Union[bool, str]]
|
|
917
915
|
cert_file=None, # type: Optional[str]
|
|
918
916
|
key_file=None, # type: Optional[str]
|
|
@@ -1268,6 +1266,13 @@ class ClientConstructor:
|
|
|
1268
1266
|
|
|
1269
1267
|
The query location will be added to the query for queries slower than the specified threshold.
|
|
1270
1268
|
|
|
1269
|
+
:param enable_http_request_source: When enabled, the source location will be added to outgoing HTTP requests.
|
|
1270
|
+
|
|
1271
|
+
:param http_request_source_threshold_ms: The threshold in milliseconds for adding the source location to an
|
|
1272
|
+
outgoing HTTP request.
|
|
1273
|
+
|
|
1274
|
+
The request location will be added to the request for requests slower than the specified threshold.
|
|
1275
|
+
|
|
1271
1276
|
:param custom_repr: A custom `repr <https://docs.python.org/3/library/functions.html#repr>`_ function to run
|
|
1272
1277
|
while serializing an object.
|
|
1273
1278
|
|
|
@@ -1343,4 +1348,4 @@ DEFAULT_OPTIONS = _get_default_options()
|
|
|
1343
1348
|
del _get_default_options
|
|
1344
1349
|
|
|
1345
1350
|
|
|
1346
|
-
VERSION = "2.
|
|
1351
|
+
VERSION = "2.42.0"
|
sentry_sdk/envelope.py
CHANGED
|
@@ -285,14 +285,14 @@ class Item:
|
|
|
285
285
|
return "error"
|
|
286
286
|
elif ty == "log":
|
|
287
287
|
return "log_item"
|
|
288
|
+
elif ty == "trace_metric":
|
|
289
|
+
return "trace_metric"
|
|
288
290
|
elif ty == "client_report":
|
|
289
291
|
return "internal"
|
|
290
292
|
elif ty == "profile":
|
|
291
293
|
return "profile"
|
|
292
294
|
elif ty == "profile_chunk":
|
|
293
295
|
return "profile_chunk"
|
|
294
|
-
elif ty == "statsd":
|
|
295
|
-
return "metric_bucket"
|
|
296
296
|
elif ty == "check_in":
|
|
297
297
|
return "monitor"
|
|
298
298
|
else:
|
|
@@ -354,7 +354,7 @@ class Item:
|
|
|
354
354
|
# if no length was specified we need to read up to the end of line
|
|
355
355
|
# and remove it (if it is present, i.e. not the very last char in an eof terminated envelope)
|
|
356
356
|
payload = f.readline().rstrip(b"\n")
|
|
357
|
-
if headers.get("type") in ("event", "transaction"
|
|
357
|
+
if headers.get("type") in ("event", "transaction"):
|
|
358
358
|
rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload)))
|
|
359
359
|
else:
|
|
360
360
|
rv = cls(headers=headers, payload=payload)
|
|
@@ -22,7 +22,7 @@ from sentry_sdk.tracing import (
|
|
|
22
22
|
SOURCE_FOR_STYLE,
|
|
23
23
|
TransactionSource,
|
|
24
24
|
)
|
|
25
|
-
from sentry_sdk.tracing_utils import should_propagate_trace
|
|
25
|
+
from sentry_sdk.tracing_utils import should_propagate_trace, add_http_request_source
|
|
26
26
|
from sentry_sdk.utils import (
|
|
27
27
|
capture_internal_exceptions,
|
|
28
28
|
ensure_integration_enabled,
|
|
@@ -279,6 +279,9 @@ def create_trace_config():
|
|
|
279
279
|
span.set_data("reason", params.response.reason)
|
|
280
280
|
span.finish()
|
|
281
281
|
|
|
282
|
+
with capture_internal_exceptions():
|
|
283
|
+
add_http_request_source(span)
|
|
284
|
+
|
|
282
285
|
trace_config = TraceConfig()
|
|
283
286
|
|
|
284
287
|
trace_config.on_request_start.append(on_request_start)
|