sentry-sdk 2.39.0__py2.py3-none-any.whl → 2.41.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 +87 -36
- sentry_sdk/consts.py +15 -9
- sentry_sdk/envelope.py +31 -17
- sentry_sdk/feature_flags.py +0 -1
- sentry_sdk/hub.py +17 -9
- sentry_sdk/integrations/__init__.py +1 -0
- sentry_sdk/integrations/anthropic.py +10 -2
- sentry_sdk/integrations/asgi.py +3 -2
- sentry_sdk/integrations/dramatiq.py +89 -31
- sentry_sdk/integrations/grpc/aio/client.py +2 -1
- sentry_sdk/integrations/grpc/client.py +3 -4
- sentry_sdk/integrations/langchain.py +29 -5
- sentry_sdk/integrations/langgraph.py +5 -3
- sentry_sdk/integrations/launchdarkly.py +0 -1
- sentry_sdk/integrations/litellm.py +251 -0
- sentry_sdk/integrations/litestar.py +4 -4
- 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/ai_client.py +4 -1
- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +10 -2
- sentry_sdk/integrations/openai_agents/utils.py +60 -19
- sentry_sdk/integrations/pure_eval.py +3 -1
- sentry_sdk/integrations/spark/spark_driver.py +2 -1
- sentry_sdk/integrations/sqlalchemy.py +2 -6
- sentry_sdk/integrations/starlette.py +1 -3
- sentry_sdk/integrations/starlite.py +4 -4
- sentry_sdk/integrations/threading.py +52 -8
- sentry_sdk/integrations/wsgi.py +3 -2
- sentry_sdk/logger.py +1 -1
- sentry_sdk/profiler/utils.py +2 -6
- sentry_sdk/scope.py +6 -3
- sentry_sdk/serializer.py +1 -3
- sentry_sdk/session.py +4 -2
- sentry_sdk/sessions.py +4 -2
- sentry_sdk/tracing.py +36 -33
- sentry_sdk/tracing_utils.py +1 -3
- sentry_sdk/transport.py +9 -26
- sentry_sdk/types.py +3 -0
- sentry_sdk/utils.py +22 -4
- {sentry_sdk-2.39.0.dist-info → sentry_sdk-2.41.0.dist-info}/METADATA +3 -1
- {sentry_sdk-2.39.0.dist-info → sentry_sdk-2.41.0.dist-info}/RECORD +51 -49
- sentry_sdk/metrics.py +0 -965
- {sentry_sdk-2.39.0.dist-info → sentry_sdk-2.41.0.dist-info}/WHEEL +0 -0
- {sentry_sdk-2.39.0.dist-info → sentry_sdk-2.41.0.dist-info}/entry_points.txt +0 -0
- {sentry_sdk-2.39.0.dist-info → sentry_sdk-2.41.0.dist-info}/licenses/LICENSE +0 -0
- {sentry_sdk-2.39.0.dist-info → sentry_sdk-2.41.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
|
|
|
@@ -178,14 +180,12 @@ class BaseClient:
|
|
|
178
180
|
|
|
179
181
|
def __init__(self, options=None):
|
|
180
182
|
# type: (Optional[Dict[str, Any]]) -> None
|
|
181
|
-
self.options =
|
|
182
|
-
options if options is not None else DEFAULT_OPTIONS
|
|
183
|
-
) # type: Dict[str, Any]
|
|
183
|
+
self.options = options if options is not None else DEFAULT_OPTIONS # type: Dict[str, Any]
|
|
184
184
|
|
|
185
185
|
self.transport = None # type: Optional[Transport]
|
|
186
186
|
self.monitor = None # type: Optional[Monitor]
|
|
187
|
-
self.metrics_aggregator = None # type: Optional[MetricsAggregator]
|
|
188
187
|
self.log_batcher = None # type: Optional[LogBatcher]
|
|
188
|
+
self.metrics_batcher = None # type: Optional[MetricsBatcher]
|
|
189
189
|
|
|
190
190
|
def __getstate__(self, *args, **kwargs):
|
|
191
191
|
# type: (*Any, **Any) -> Any
|
|
@@ -217,10 +217,14 @@ class BaseClient:
|
|
|
217
217
|
# type: (*Any, **Any) -> Optional[str]
|
|
218
218
|
return None
|
|
219
219
|
|
|
220
|
-
def
|
|
220
|
+
def _capture_log(self, log):
|
|
221
221
|
# type: (Log) -> None
|
|
222
222
|
pass
|
|
223
223
|
|
|
224
|
+
def _capture_metric(self, metric):
|
|
225
|
+
# type: (Metric) -> None
|
|
226
|
+
pass
|
|
227
|
+
|
|
224
228
|
def capture_session(self, *args, **kwargs):
|
|
225
229
|
# type: (*Any, **Any) -> None
|
|
226
230
|
return None
|
|
@@ -363,26 +367,6 @@ class _Client(BaseClient):
|
|
|
363
367
|
|
|
364
368
|
self.session_flusher = SessionFlusher(capture_func=_capture_envelope)
|
|
365
369
|
|
|
366
|
-
self.metrics_aggregator = None # type: Optional[MetricsAggregator]
|
|
367
|
-
experiments = self.options.get("_experiments", {})
|
|
368
|
-
if experiments.get("enable_metrics", True):
|
|
369
|
-
# Context vars are not working correctly on Python <=3.6
|
|
370
|
-
# with gevent.
|
|
371
|
-
metrics_supported = not is_gevent() or PY37
|
|
372
|
-
if metrics_supported:
|
|
373
|
-
from sentry_sdk.metrics import MetricsAggregator
|
|
374
|
-
|
|
375
|
-
self.metrics_aggregator = MetricsAggregator(
|
|
376
|
-
capture_func=_capture_envelope,
|
|
377
|
-
enable_code_locations=bool(
|
|
378
|
-
experiments.get("metric_code_locations", True)
|
|
379
|
-
),
|
|
380
|
-
)
|
|
381
|
-
else:
|
|
382
|
-
logger.info(
|
|
383
|
-
"Metrics not supported on Python 3.6 and lower with gevent."
|
|
384
|
-
)
|
|
385
|
-
|
|
386
370
|
self.log_batcher = None
|
|
387
371
|
|
|
388
372
|
if has_logs_enabled(self.options):
|
|
@@ -390,6 +374,13 @@ class _Client(BaseClient):
|
|
|
390
374
|
|
|
391
375
|
self.log_batcher = LogBatcher(capture_func=_capture_envelope)
|
|
392
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
|
+
|
|
393
384
|
max_request_body_size = ("always", "never", "small", "medium")
|
|
394
385
|
if self.options["max_request_body_size"] not in max_request_body_size:
|
|
395
386
|
raise ValueError(
|
|
@@ -469,7 +460,6 @@ class _Client(BaseClient):
|
|
|
469
460
|
|
|
470
461
|
if (
|
|
471
462
|
self.monitor
|
|
472
|
-
or self.metrics_aggregator
|
|
473
463
|
or self.log_batcher
|
|
474
464
|
or has_profiling_enabled(self.options)
|
|
475
465
|
or isinstance(self.transport, BaseHttpTransport)
|
|
@@ -902,7 +892,7 @@ class _Client(BaseClient):
|
|
|
902
892
|
|
|
903
893
|
return return_value
|
|
904
894
|
|
|
905
|
-
def
|
|
895
|
+
def _capture_log(self, log):
|
|
906
896
|
# type: (Optional[Log]) -> None
|
|
907
897
|
if not has_logs_enabled(self.options) or log is None:
|
|
908
898
|
return
|
|
@@ -956,7 +946,7 @@ class _Client(BaseClient):
|
|
|
956
946
|
debug = self.options.get("debug", False)
|
|
957
947
|
if debug:
|
|
958
948
|
logger.debug(
|
|
959
|
-
f
|
|
949
|
+
f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}"
|
|
960
950
|
)
|
|
961
951
|
|
|
962
952
|
before_send_log = get_before_send_log(self.options)
|
|
@@ -969,8 +959,68 @@ class _Client(BaseClient):
|
|
|
969
959
|
if self.log_batcher:
|
|
970
960
|
self.log_batcher.add(log)
|
|
971
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
|
+
|
|
972
1021
|
def capture_session(
|
|
973
|
-
self,
|
|
1022
|
+
self,
|
|
1023
|
+
session, # type: Session
|
|
974
1024
|
):
|
|
975
1025
|
# type: (...) -> None
|
|
976
1026
|
if not session.release:
|
|
@@ -991,7 +1041,8 @@ class _Client(BaseClient):
|
|
|
991
1041
|
...
|
|
992
1042
|
|
|
993
1043
|
def get_integration(
|
|
994
|
-
self,
|
|
1044
|
+
self,
|
|
1045
|
+
name_or_class, # type: Union[str, Type[Integration]]
|
|
995
1046
|
):
|
|
996
1047
|
# type: (...) -> Optional[Integration]
|
|
997
1048
|
"""Returns the integration for this client by name or class.
|
|
@@ -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
|
@@ -40,6 +40,7 @@ if TYPE_CHECKING:
|
|
|
40
40
|
from typing import Any
|
|
41
41
|
from typing import Sequence
|
|
42
42
|
from typing import Tuple
|
|
43
|
+
from typing import AbstractSet
|
|
43
44
|
from typing_extensions import Literal
|
|
44
45
|
from typing_extensions import TypedDict
|
|
45
46
|
|
|
@@ -51,11 +52,10 @@ if TYPE_CHECKING:
|
|
|
51
52
|
Hint,
|
|
52
53
|
Log,
|
|
53
54
|
MeasurementUnit,
|
|
55
|
+
Metric,
|
|
54
56
|
ProfilerMode,
|
|
55
57
|
TracesSampler,
|
|
56
58
|
TransactionProcessor,
|
|
57
|
-
MetricTags,
|
|
58
|
-
MetricValue,
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
# Experiments are feature flags to enable and disable certain unstable SDK
|
|
@@ -76,13 +76,10 @@ if TYPE_CHECKING:
|
|
|
76
76
|
"transport_compression_algo": Optional[CompressionAlgo],
|
|
77
77
|
"transport_num_pools": Optional[int],
|
|
78
78
|
"transport_http2": Optional[bool],
|
|
79
|
-
"enable_metrics": Optional[bool],
|
|
80
|
-
"before_emit_metric": Optional[
|
|
81
|
-
Callable[[str, MetricValue, MeasurementUnit, MetricTags], bool]
|
|
82
|
-
],
|
|
83
|
-
"metric_code_locations": Optional[bool],
|
|
84
79
|
"enable_logs": Optional[bool],
|
|
85
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]]],
|
|
86
83
|
},
|
|
87
84
|
total=False,
|
|
88
85
|
)
|
|
@@ -838,6 +835,7 @@ class OP:
|
|
|
838
835
|
QUEUE_TASK_HUEY = "queue.task.huey"
|
|
839
836
|
QUEUE_SUBMIT_RAY = "queue.submit.ray"
|
|
840
837
|
QUEUE_TASK_RAY = "queue.task.ray"
|
|
838
|
+
QUEUE_TASK_DRAMATIQ = "queue.task.dramatiq"
|
|
841
839
|
SUBPROCESS = "subprocess"
|
|
842
840
|
SUBPROCESS_WAIT = "subprocess.wait"
|
|
843
841
|
SUBPROCESS_COMMUNICATE = "subprocess.communicate"
|
|
@@ -852,7 +850,6 @@ class OP:
|
|
|
852
850
|
# This type exists to trick mypy and PyCharm into thinking `init` and `Client`
|
|
853
851
|
# take these arguments (even though they take opaque **kwargs)
|
|
854
852
|
class ClientConstructor:
|
|
855
|
-
|
|
856
853
|
def __init__(
|
|
857
854
|
self,
|
|
858
855
|
dsn=None, # type: Optional[str]
|
|
@@ -920,6 +917,7 @@ class ClientConstructor:
|
|
|
920
917
|
max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int]
|
|
921
918
|
enable_logs=False, # type: bool
|
|
922
919
|
before_send_log=None, # type: Optional[Callable[[Log, Hint], Optional[Log]]]
|
|
920
|
+
trace_ignore_status_codes=frozenset(), # type: AbstractSet[int]
|
|
923
921
|
):
|
|
924
922
|
# type: (...) -> None
|
|
925
923
|
"""Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`.
|
|
@@ -1308,6 +1306,14 @@ class ClientConstructor:
|
|
|
1308
1306
|
function will be retained. If the function returns None, the log will
|
|
1309
1307
|
not be sent to Sentry.
|
|
1310
1308
|
|
|
1309
|
+
:param trace_ignore_status_codes: An optional property that disables tracing for
|
|
1310
|
+
HTTP requests with certain status codes.
|
|
1311
|
+
|
|
1312
|
+
Requests are not traced if the status code is contained in the provided set.
|
|
1313
|
+
|
|
1314
|
+
If `trace_ignore_status_codes` is not provided, requests with any status code
|
|
1315
|
+
may be traced.
|
|
1316
|
+
|
|
1311
1317
|
:param _experiments:
|
|
1312
1318
|
"""
|
|
1313
1319
|
pass
|
|
@@ -1333,4 +1339,4 @@ DEFAULT_OPTIONS = _get_default_options()
|
|
|
1333
1339
|
del _get_default_options
|
|
1334
1340
|
|
|
1335
1341
|
|
|
1336
|
-
VERSION = "2.
|
|
1342
|
+
VERSION = "2.41.0"
|