sentry-sdk 0.18.0__py2.py3-none-any.whl → 2.46.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.
- sentry_sdk/__init__.py +48 -6
- sentry_sdk/_compat.py +64 -56
- sentry_sdk/_init_implementation.py +84 -0
- sentry_sdk/_log_batcher.py +172 -0
- sentry_sdk/_lru_cache.py +47 -0
- sentry_sdk/_metrics_batcher.py +167 -0
- sentry_sdk/_queue.py +81 -19
- sentry_sdk/_types.py +311 -11
- sentry_sdk/_werkzeug.py +98 -0
- sentry_sdk/ai/__init__.py +7 -0
- sentry_sdk/ai/monitoring.py +137 -0
- sentry_sdk/ai/utils.py +144 -0
- sentry_sdk/api.py +409 -67
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +849 -103
- sentry_sdk/consts.py +1389 -34
- sentry_sdk/crons/__init__.py +10 -0
- sentry_sdk/crons/api.py +62 -0
- sentry_sdk/crons/consts.py +4 -0
- sentry_sdk/crons/decorator.py +135 -0
- sentry_sdk/debug.py +12 -15
- sentry_sdk/envelope.py +112 -61
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +442 -386
- sentry_sdk/integrations/__init__.py +228 -58
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +131 -40
- sentry_sdk/integrations/aiohttp.py +221 -72
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +4 -6
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +237 -135
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +13 -18
- sentry_sdk/integrations/aws_lambda.py +233 -80
- sentry_sdk/integrations/beam.py +27 -35
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +91 -69
- sentry_sdk/integrations/celery/__init__.py +529 -0
- sentry_sdk/integrations/celery/beat.py +293 -0
- sentry_sdk/integrations/celery/utils.py +43 -0
- sentry_sdk/integrations/chalice.py +35 -28
- sentry_sdk/integrations/clickhouse_driver.py +177 -0
- sentry_sdk/integrations/cloud_resource_context.py +280 -0
- sentry_sdk/integrations/cohere.py +274 -0
- sentry_sdk/integrations/dedupe.py +32 -8
- sentry_sdk/integrations/django/__init__.py +343 -89
- sentry_sdk/integrations/django/asgi.py +201 -22
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +80 -32
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +69 -2
- sentry_sdk/integrations/django/transactions.py +39 -14
- sentry_sdk/integrations/django/views.py +69 -16
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +19 -13
- sentry_sdk/integrations/executing.py +5 -6
- sentry_sdk/integrations/falcon.py +128 -65
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +114 -75
- sentry_sdk/integrations/gcp.py +67 -36
- sentry_sdk/integrations/gnu_backtrace.py +14 -22
- sentry_sdk/integrations/google_genai/__init__.py +301 -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 +576 -0
- sentry_sdk/integrations/gql.py +162 -0
- sentry_sdk/integrations/graphene.py +151 -0
- sentry_sdk/integrations/grpc/__init__.py +168 -0
- sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
- sentry_sdk/integrations/grpc/aio/client.py +95 -0
- sentry_sdk/integrations/grpc/aio/server.py +100 -0
- sentry_sdk/integrations/grpc/client.py +91 -0
- sentry_sdk/integrations/grpc/consts.py +1 -0
- sentry_sdk/integrations/grpc/server.py +66 -0
- sentry_sdk/integrations/httpx.py +178 -0
- sentry_sdk/integrations/huey.py +174 -0
- sentry_sdk/integrations/huggingface_hub.py +378 -0
- sentry_sdk/integrations/langchain.py +1132 -0
- sentry_sdk/integrations/langgraph.py +337 -0
- sentry_sdk/integrations/launchdarkly.py +61 -0
- sentry_sdk/integrations/litellm.py +287 -0
- sentry_sdk/integrations/litestar.py +315 -0
- sentry_sdk/integrations/logging.py +261 -85
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +6 -33
- sentry_sdk/integrations/openai.py +725 -0
- sentry_sdk/integrations/openai_agents/__init__.py +61 -0
- sentry_sdk/integrations/openai_agents/consts.py +1 -0
- sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
- sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
- sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
- sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
- sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
- sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
- sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
- sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
- sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
- sentry_sdk/integrations/openai_agents/utils.py +199 -0
- sentry_sdk/integrations/openfeature.py +35 -0
- sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
- sentry_sdk/integrations/opentelemetry/consts.py +5 -0
- sentry_sdk/integrations/opentelemetry/integration.py +58 -0
- sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
- sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
- sentry_sdk/integrations/otlp.py +82 -0
- sentry_sdk/integrations/pure_eval.py +20 -11
- sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
- sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
- sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
- sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
- sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
- sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
- sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
- sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
- sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
- sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
- sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
- sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
- sentry_sdk/integrations/pymongo.py +214 -0
- sentry_sdk/integrations/pyramid.py +71 -60
- sentry_sdk/integrations/quart.py +237 -0
- sentry_sdk/integrations/ray.py +165 -0
- sentry_sdk/integrations/redis/__init__.py +48 -0
- sentry_sdk/integrations/redis/_async_common.py +116 -0
- sentry_sdk/integrations/redis/_sync_common.py +119 -0
- sentry_sdk/integrations/redis/consts.py +19 -0
- sentry_sdk/integrations/redis/modules/__init__.py +0 -0
- sentry_sdk/integrations/redis/modules/caches.py +118 -0
- sentry_sdk/integrations/redis/modules/queries.py +65 -0
- sentry_sdk/integrations/redis/rb.py +32 -0
- sentry_sdk/integrations/redis/redis.py +69 -0
- sentry_sdk/integrations/redis/redis_cluster.py +107 -0
- sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
- sentry_sdk/integrations/redis/utils.py +148 -0
- sentry_sdk/integrations/rq.py +62 -52
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +248 -114
- sentry_sdk/integrations/serverless.py +13 -22
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/spark_driver.py +115 -62
- sentry_sdk/integrations/spark/spark_worker.py +42 -50
- sentry_sdk/integrations/sqlalchemy.py +82 -37
- sentry_sdk/integrations/starlette.py +737 -0
- sentry_sdk/integrations/starlite.py +292 -0
- sentry_sdk/integrations/statsig.py +37 -0
- sentry_sdk/integrations/stdlib.py +100 -58
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +142 -38
- sentry_sdk/integrations/tornado.py +68 -53
- sentry_sdk/integrations/trytond.py +15 -20
- sentry_sdk/integrations/typer.py +60 -0
- sentry_sdk/integrations/unleash.py +33 -0
- sentry_sdk/integrations/unraisablehook.py +53 -0
- sentry_sdk/integrations/wsgi.py +126 -125
- sentry_sdk/logger.py +96 -0
- sentry_sdk/metrics.py +81 -0
- sentry_sdk/monitor.py +120 -0
- sentry_sdk/profiler/__init__.py +49 -0
- sentry_sdk/profiler/continuous_profiler.py +730 -0
- sentry_sdk/profiler/transaction_profiler.py +839 -0
- sentry_sdk/profiler/utils.py +195 -0
- sentry_sdk/scope.py +1542 -112
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +152 -210
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +202 -179
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1202 -294
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +693 -189
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1395 -228
- sentry_sdk/worker.py +30 -17
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
- sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
- sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
- sentry_sdk/_functools.py +0 -66
- sentry_sdk/integrations/celery.py +0 -275
- sentry_sdk/integrations/redis.py +0 -103
- sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
- sentry_sdk-0.18.0.dist-info/METADATA +0 -66
- sentry_sdk-0.18.0.dist-info/RECORD +0 -65
- {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from time import time
|
|
3
|
+
from typing import TYPE_CHECKING, cast
|
|
4
|
+
|
|
5
|
+
from opentelemetry.context import get_value
|
|
6
|
+
from opentelemetry.sdk.trace import SpanProcessor, ReadableSpan as OTelSpan
|
|
7
|
+
from opentelemetry.semconv.trace import SpanAttributes
|
|
8
|
+
from opentelemetry.trace import (
|
|
9
|
+
format_span_id,
|
|
10
|
+
format_trace_id,
|
|
11
|
+
get_current_span,
|
|
12
|
+
SpanKind,
|
|
13
|
+
)
|
|
14
|
+
from opentelemetry.trace.span import (
|
|
15
|
+
INVALID_SPAN_ID,
|
|
16
|
+
INVALID_TRACE_ID,
|
|
17
|
+
)
|
|
18
|
+
from sentry_sdk import get_client, start_transaction
|
|
19
|
+
from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS
|
|
20
|
+
from sentry_sdk.integrations.opentelemetry.consts import (
|
|
21
|
+
SENTRY_BAGGAGE_KEY,
|
|
22
|
+
SENTRY_TRACE_KEY,
|
|
23
|
+
)
|
|
24
|
+
from sentry_sdk.scope import add_global_event_processor
|
|
25
|
+
from sentry_sdk.tracing import Transaction, Span as SentrySpan
|
|
26
|
+
from sentry_sdk.utils import Dsn
|
|
27
|
+
|
|
28
|
+
from urllib3.util import parse_url as urlparse
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from typing import Any, Optional, Union
|
|
32
|
+
from opentelemetry import context as context_api
|
|
33
|
+
from sentry_sdk._types import Event, Hint
|
|
34
|
+
|
|
35
|
+
OPEN_TELEMETRY_CONTEXT = "otel"
|
|
36
|
+
SPAN_MAX_TIME_OPEN_MINUTES = 10
|
|
37
|
+
SPAN_ORIGIN = "auto.otel"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def link_trace_context_to_error_event(event, otel_span_map):
|
|
41
|
+
# type: (Event, dict[str, Union[Transaction, SentrySpan]]) -> Event
|
|
42
|
+
client = get_client()
|
|
43
|
+
|
|
44
|
+
if client.options["instrumenter"] != INSTRUMENTER.OTEL:
|
|
45
|
+
return event
|
|
46
|
+
|
|
47
|
+
if hasattr(event, "type") and event["type"] == "transaction":
|
|
48
|
+
return event
|
|
49
|
+
|
|
50
|
+
otel_span = get_current_span()
|
|
51
|
+
if not otel_span:
|
|
52
|
+
return event
|
|
53
|
+
|
|
54
|
+
ctx = otel_span.get_span_context()
|
|
55
|
+
|
|
56
|
+
if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID:
|
|
57
|
+
return event
|
|
58
|
+
|
|
59
|
+
sentry_span = otel_span_map.get(format_span_id(ctx.span_id), None)
|
|
60
|
+
if not sentry_span:
|
|
61
|
+
return event
|
|
62
|
+
|
|
63
|
+
contexts = event.setdefault("contexts", {})
|
|
64
|
+
contexts.setdefault("trace", {}).update(sentry_span.get_trace_context())
|
|
65
|
+
|
|
66
|
+
return event
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SentrySpanProcessor(SpanProcessor):
|
|
70
|
+
"""
|
|
71
|
+
Converts OTel spans into Sentry spans so they can be sent to the Sentry backend.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# The mapping from otel span ids to sentry spans
|
|
75
|
+
otel_span_map = {} # type: dict[str, Union[Transaction, SentrySpan]]
|
|
76
|
+
|
|
77
|
+
# The currently open spans. Elements will be discarded after SPAN_MAX_TIME_OPEN_MINUTES
|
|
78
|
+
open_spans = {} # type: dict[int, set[str]]
|
|
79
|
+
|
|
80
|
+
def __new__(cls):
|
|
81
|
+
# type: () -> SentrySpanProcessor
|
|
82
|
+
if not hasattr(cls, "instance"):
|
|
83
|
+
cls.instance = super().__new__(cls)
|
|
84
|
+
|
|
85
|
+
return cls.instance
|
|
86
|
+
|
|
87
|
+
def __init__(self):
|
|
88
|
+
# type: () -> None
|
|
89
|
+
@add_global_event_processor
|
|
90
|
+
def global_event_processor(event, hint):
|
|
91
|
+
# type: (Event, Hint) -> Event
|
|
92
|
+
return link_trace_context_to_error_event(event, self.otel_span_map)
|
|
93
|
+
|
|
94
|
+
def _prune_old_spans(self):
|
|
95
|
+
# type: (SentrySpanProcessor) -> None
|
|
96
|
+
"""
|
|
97
|
+
Prune spans that have been open for too long.
|
|
98
|
+
"""
|
|
99
|
+
current_time_minutes = int(time() / 60)
|
|
100
|
+
for span_start_minutes in list(
|
|
101
|
+
self.open_spans.keys()
|
|
102
|
+
): # making a list because we change the dict
|
|
103
|
+
# prune empty open spans buckets
|
|
104
|
+
if self.open_spans[span_start_minutes] == set():
|
|
105
|
+
self.open_spans.pop(span_start_minutes)
|
|
106
|
+
|
|
107
|
+
# prune old buckets
|
|
108
|
+
elif current_time_minutes - span_start_minutes > SPAN_MAX_TIME_OPEN_MINUTES:
|
|
109
|
+
for span_id in self.open_spans.pop(span_start_minutes):
|
|
110
|
+
self.otel_span_map.pop(span_id, None)
|
|
111
|
+
|
|
112
|
+
def on_start(self, otel_span, parent_context=None):
|
|
113
|
+
# type: (OTelSpan, Optional[context_api.Context]) -> None
|
|
114
|
+
client = get_client()
|
|
115
|
+
|
|
116
|
+
if not client.dsn:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
_ = Dsn(client.dsn)
|
|
121
|
+
except Exception:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
if client.options["instrumenter"] != INSTRUMENTER.OTEL:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
if not otel_span.get_span_context().is_valid:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
if self._is_sentry_span(otel_span):
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
trace_data = self._get_trace_data(otel_span, parent_context)
|
|
134
|
+
|
|
135
|
+
parent_span_id = trace_data["parent_span_id"]
|
|
136
|
+
sentry_parent_span = (
|
|
137
|
+
self.otel_span_map.get(parent_span_id) if parent_span_id else None
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
start_timestamp = None
|
|
141
|
+
if otel_span.start_time is not None:
|
|
142
|
+
start_timestamp = datetime.fromtimestamp(
|
|
143
|
+
otel_span.start_time / 1e9, timezone.utc
|
|
144
|
+
) # OTel spans have nanosecond precision
|
|
145
|
+
|
|
146
|
+
sentry_span = None
|
|
147
|
+
if sentry_parent_span:
|
|
148
|
+
sentry_span = sentry_parent_span.start_child(
|
|
149
|
+
span_id=trace_data["span_id"],
|
|
150
|
+
name=otel_span.name,
|
|
151
|
+
start_timestamp=start_timestamp,
|
|
152
|
+
instrumenter=INSTRUMENTER.OTEL,
|
|
153
|
+
origin=SPAN_ORIGIN,
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
sentry_span = start_transaction(
|
|
157
|
+
name=otel_span.name,
|
|
158
|
+
span_id=trace_data["span_id"],
|
|
159
|
+
parent_span_id=parent_span_id,
|
|
160
|
+
trace_id=trace_data["trace_id"],
|
|
161
|
+
baggage=trace_data["baggage"],
|
|
162
|
+
start_timestamp=start_timestamp,
|
|
163
|
+
instrumenter=INSTRUMENTER.OTEL,
|
|
164
|
+
origin=SPAN_ORIGIN,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
self.otel_span_map[trace_data["span_id"]] = sentry_span
|
|
168
|
+
|
|
169
|
+
if otel_span.start_time is not None:
|
|
170
|
+
span_start_in_minutes = int(
|
|
171
|
+
otel_span.start_time / 1e9 / 60
|
|
172
|
+
) # OTel spans have nanosecond precision
|
|
173
|
+
self.open_spans.setdefault(span_start_in_minutes, set()).add(
|
|
174
|
+
trace_data["span_id"]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
self._prune_old_spans()
|
|
178
|
+
|
|
179
|
+
def on_end(self, otel_span):
|
|
180
|
+
# type: (OTelSpan) -> None
|
|
181
|
+
client = get_client()
|
|
182
|
+
|
|
183
|
+
if client.options["instrumenter"] != INSTRUMENTER.OTEL:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
span_context = otel_span.get_span_context()
|
|
187
|
+
if not span_context.is_valid:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
span_id = format_span_id(span_context.span_id)
|
|
191
|
+
sentry_span = self.otel_span_map.pop(span_id, None)
|
|
192
|
+
if not sentry_span:
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
sentry_span.op = otel_span.name
|
|
196
|
+
|
|
197
|
+
self._update_span_with_otel_status(sentry_span, otel_span)
|
|
198
|
+
|
|
199
|
+
if isinstance(sentry_span, Transaction):
|
|
200
|
+
sentry_span.name = otel_span.name
|
|
201
|
+
sentry_span.set_context(
|
|
202
|
+
OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span)
|
|
203
|
+
)
|
|
204
|
+
self._update_transaction_with_otel_data(sentry_span, otel_span)
|
|
205
|
+
|
|
206
|
+
else:
|
|
207
|
+
self._update_span_with_otel_data(sentry_span, otel_span)
|
|
208
|
+
|
|
209
|
+
end_timestamp = None
|
|
210
|
+
if otel_span.end_time is not None:
|
|
211
|
+
end_timestamp = datetime.fromtimestamp(
|
|
212
|
+
otel_span.end_time / 1e9, timezone.utc
|
|
213
|
+
) # OTel spans have nanosecond precision
|
|
214
|
+
|
|
215
|
+
sentry_span.finish(end_timestamp=end_timestamp)
|
|
216
|
+
|
|
217
|
+
if otel_span.start_time is not None:
|
|
218
|
+
span_start_in_minutes = int(
|
|
219
|
+
otel_span.start_time / 1e9 / 60
|
|
220
|
+
) # OTel spans have nanosecond precision
|
|
221
|
+
self.open_spans.setdefault(span_start_in_minutes, set()).discard(span_id)
|
|
222
|
+
|
|
223
|
+
self._prune_old_spans()
|
|
224
|
+
|
|
225
|
+
def _is_sentry_span(self, otel_span):
|
|
226
|
+
# type: (OTelSpan) -> bool
|
|
227
|
+
"""
|
|
228
|
+
Break infinite loop:
|
|
229
|
+
HTTP requests to Sentry are caught by OTel and send again to Sentry.
|
|
230
|
+
"""
|
|
231
|
+
otel_span_url = None
|
|
232
|
+
if otel_span.attributes is not None:
|
|
233
|
+
otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL)
|
|
234
|
+
otel_span_url = cast("Optional[str]", otel_span_url)
|
|
235
|
+
|
|
236
|
+
dsn_url = None
|
|
237
|
+
client = get_client()
|
|
238
|
+
if client.dsn:
|
|
239
|
+
dsn_url = Dsn(client.dsn).netloc
|
|
240
|
+
|
|
241
|
+
if otel_span_url and dsn_url and dsn_url in otel_span_url:
|
|
242
|
+
return True
|
|
243
|
+
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
def _get_otel_context(self, otel_span):
|
|
247
|
+
# type: (OTelSpan) -> dict[str, Any]
|
|
248
|
+
"""
|
|
249
|
+
Returns the OTel context for Sentry.
|
|
250
|
+
See: https://develop.sentry.dev/sdk/performance/opentelemetry/#step-5-add-opentelemetry-context
|
|
251
|
+
"""
|
|
252
|
+
ctx = {}
|
|
253
|
+
|
|
254
|
+
if otel_span.attributes:
|
|
255
|
+
ctx["attributes"] = dict(otel_span.attributes)
|
|
256
|
+
|
|
257
|
+
if otel_span.resource.attributes:
|
|
258
|
+
ctx["resource"] = dict(otel_span.resource.attributes)
|
|
259
|
+
|
|
260
|
+
return ctx
|
|
261
|
+
|
|
262
|
+
def _get_trace_data(self, otel_span, parent_context):
|
|
263
|
+
# type: (OTelSpan, Optional[context_api.Context]) -> dict[str, Any]
|
|
264
|
+
"""
|
|
265
|
+
Extracts tracing information from one OTel span and its parent OTel context.
|
|
266
|
+
"""
|
|
267
|
+
trace_data = {} # type: dict[str, Any]
|
|
268
|
+
span_context = otel_span.get_span_context()
|
|
269
|
+
|
|
270
|
+
span_id = format_span_id(span_context.span_id)
|
|
271
|
+
trace_data["span_id"] = span_id
|
|
272
|
+
|
|
273
|
+
trace_id = format_trace_id(span_context.trace_id)
|
|
274
|
+
trace_data["trace_id"] = trace_id
|
|
275
|
+
|
|
276
|
+
parent_span_id = (
|
|
277
|
+
format_span_id(otel_span.parent.span_id) if otel_span.parent else None
|
|
278
|
+
)
|
|
279
|
+
trace_data["parent_span_id"] = parent_span_id
|
|
280
|
+
|
|
281
|
+
sentry_trace_data = get_value(SENTRY_TRACE_KEY, parent_context)
|
|
282
|
+
sentry_trace_data = cast("dict[str, Union[str, bool, None]]", sentry_trace_data)
|
|
283
|
+
trace_data["parent_sampled"] = (
|
|
284
|
+
sentry_trace_data["parent_sampled"] if sentry_trace_data else None
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
baggage = get_value(SENTRY_BAGGAGE_KEY, parent_context)
|
|
288
|
+
trace_data["baggage"] = baggage
|
|
289
|
+
|
|
290
|
+
return trace_data
|
|
291
|
+
|
|
292
|
+
def _update_span_with_otel_status(self, sentry_span, otel_span):
|
|
293
|
+
# type: (SentrySpan, OTelSpan) -> None
|
|
294
|
+
"""
|
|
295
|
+
Set the Sentry span status from the OTel span
|
|
296
|
+
"""
|
|
297
|
+
if otel_span.status.is_unset:
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
if otel_span.status.is_ok:
|
|
301
|
+
sentry_span.set_status(SPANSTATUS.OK)
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
sentry_span.set_status(SPANSTATUS.INTERNAL_ERROR)
|
|
305
|
+
|
|
306
|
+
def _update_span_with_otel_data(self, sentry_span, otel_span):
|
|
307
|
+
# type: (SentrySpan, OTelSpan) -> None
|
|
308
|
+
"""
|
|
309
|
+
Convert OTel span data and update the Sentry span with it.
|
|
310
|
+
This should eventually happen on the server when ingesting the spans.
|
|
311
|
+
"""
|
|
312
|
+
sentry_span.set_data("otel.kind", otel_span.kind)
|
|
313
|
+
|
|
314
|
+
op = otel_span.name
|
|
315
|
+
description = otel_span.name
|
|
316
|
+
|
|
317
|
+
if otel_span.attributes is not None:
|
|
318
|
+
for key, val in otel_span.attributes.items():
|
|
319
|
+
sentry_span.set_data(key, val)
|
|
320
|
+
|
|
321
|
+
http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD)
|
|
322
|
+
http_method = cast("Optional[str]", http_method)
|
|
323
|
+
|
|
324
|
+
db_query = otel_span.attributes.get(SpanAttributes.DB_SYSTEM)
|
|
325
|
+
|
|
326
|
+
if http_method:
|
|
327
|
+
op = "http"
|
|
328
|
+
|
|
329
|
+
if otel_span.kind == SpanKind.SERVER:
|
|
330
|
+
op += ".server"
|
|
331
|
+
elif otel_span.kind == SpanKind.CLIENT:
|
|
332
|
+
op += ".client"
|
|
333
|
+
|
|
334
|
+
description = http_method
|
|
335
|
+
|
|
336
|
+
peer_name = otel_span.attributes.get(SpanAttributes.NET_PEER_NAME, None)
|
|
337
|
+
if peer_name:
|
|
338
|
+
description += " {}".format(peer_name)
|
|
339
|
+
|
|
340
|
+
target = otel_span.attributes.get(SpanAttributes.HTTP_TARGET, None)
|
|
341
|
+
if target:
|
|
342
|
+
description += " {}".format(target)
|
|
343
|
+
|
|
344
|
+
if not peer_name and not target:
|
|
345
|
+
url = otel_span.attributes.get(SpanAttributes.HTTP_URL, None)
|
|
346
|
+
url = cast("Optional[str]", url)
|
|
347
|
+
if url:
|
|
348
|
+
parsed_url = urlparse(url)
|
|
349
|
+
url = "{}://{}{}".format(
|
|
350
|
+
parsed_url.scheme, parsed_url.netloc, parsed_url.path
|
|
351
|
+
)
|
|
352
|
+
description += " {}".format(url)
|
|
353
|
+
|
|
354
|
+
status_code = otel_span.attributes.get(
|
|
355
|
+
SpanAttributes.HTTP_STATUS_CODE, None
|
|
356
|
+
)
|
|
357
|
+
status_code = cast("Optional[int]", status_code)
|
|
358
|
+
if status_code:
|
|
359
|
+
sentry_span.set_http_status(status_code)
|
|
360
|
+
|
|
361
|
+
elif db_query:
|
|
362
|
+
op = "db"
|
|
363
|
+
statement = otel_span.attributes.get(SpanAttributes.DB_STATEMENT, None)
|
|
364
|
+
statement = cast("Optional[str]", statement)
|
|
365
|
+
if statement:
|
|
366
|
+
description = statement
|
|
367
|
+
|
|
368
|
+
sentry_span.op = op
|
|
369
|
+
sentry_span.description = description
|
|
370
|
+
|
|
371
|
+
def _update_transaction_with_otel_data(self, sentry_span, otel_span):
|
|
372
|
+
# type: (SentrySpan, OTelSpan) -> None
|
|
373
|
+
if otel_span.attributes is None:
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD)
|
|
377
|
+
|
|
378
|
+
if http_method:
|
|
379
|
+
status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE)
|
|
380
|
+
status_code = cast("Optional[int]", status_code)
|
|
381
|
+
if status_code:
|
|
382
|
+
sentry_span.set_http_status(status_code)
|
|
383
|
+
|
|
384
|
+
op = "http"
|
|
385
|
+
|
|
386
|
+
if otel_span.kind == SpanKind.SERVER:
|
|
387
|
+
op += ".server"
|
|
388
|
+
elif otel_span.kind == SpanKind.CLIENT:
|
|
389
|
+
op += ".client"
|
|
390
|
+
|
|
391
|
+
sentry_span.op = op
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from sentry_sdk.integrations import Integration, DidNotEnable
|
|
2
|
+
from sentry_sdk.scope import register_external_propagation_context
|
|
3
|
+
from sentry_sdk.utils import logger, Dsn
|
|
4
|
+
from sentry_sdk.consts import VERSION, EndpointType
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from opentelemetry import trace
|
|
8
|
+
from opentelemetry.propagate import set_global_textmap
|
|
9
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
10
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
11
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
12
|
+
|
|
13
|
+
from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator
|
|
14
|
+
except ImportError:
|
|
15
|
+
raise DidNotEnable("opentelemetry-distro[otlp] is not installed")
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from typing import Optional, Dict, Any, Tuple
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def otel_propagation_context():
|
|
24
|
+
# type: () -> Optional[Tuple[str, str]]
|
|
25
|
+
"""
|
|
26
|
+
Get the (trace_id, span_id) from opentelemetry if exists.
|
|
27
|
+
"""
|
|
28
|
+
ctx = trace.get_current_span().get_span_context()
|
|
29
|
+
|
|
30
|
+
if ctx.trace_id == trace.INVALID_TRACE_ID or ctx.span_id == trace.INVALID_SPAN_ID:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
return (trace.format_trace_id(ctx.trace_id), trace.format_span_id(ctx.span_id))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def setup_otlp_exporter(dsn=None):
|
|
37
|
+
# type: (Optional[str]) -> None
|
|
38
|
+
tracer_provider = trace.get_tracer_provider()
|
|
39
|
+
|
|
40
|
+
if not isinstance(tracer_provider, TracerProvider):
|
|
41
|
+
logger.debug("[OTLP] No TracerProvider configured by user, creating a new one")
|
|
42
|
+
tracer_provider = TracerProvider()
|
|
43
|
+
trace.set_tracer_provider(tracer_provider)
|
|
44
|
+
|
|
45
|
+
endpoint = None
|
|
46
|
+
headers = None
|
|
47
|
+
if dsn:
|
|
48
|
+
auth = Dsn(dsn).to_auth(f"sentry.python/{VERSION}")
|
|
49
|
+
endpoint = auth.get_api_url(EndpointType.OTLP_TRACES)
|
|
50
|
+
headers = {"X-Sentry-Auth": auth.to_header()}
|
|
51
|
+
logger.debug(f"[OTLP] Sending traces to {endpoint}")
|
|
52
|
+
|
|
53
|
+
otlp_exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers)
|
|
54
|
+
span_processor = BatchSpanProcessor(otlp_exporter)
|
|
55
|
+
tracer_provider.add_span_processor(span_processor)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class OTLPIntegration(Integration):
|
|
59
|
+
identifier = "otlp"
|
|
60
|
+
|
|
61
|
+
def __init__(self, setup_otlp_exporter=True, setup_propagator=True):
|
|
62
|
+
# type: (bool, bool) -> None
|
|
63
|
+
self.setup_otlp_exporter = setup_otlp_exporter
|
|
64
|
+
self.setup_propagator = setup_propagator
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def setup_once():
|
|
68
|
+
# type: () -> None
|
|
69
|
+
logger.debug("[OTLP] Setting up trace linking for all events")
|
|
70
|
+
register_external_propagation_context(otel_propagation_context)
|
|
71
|
+
|
|
72
|
+
def setup_once_with_options(self, options=None):
|
|
73
|
+
# type: (Optional[Dict[str, Any]]) -> None
|
|
74
|
+
if self.setup_otlp_exporter:
|
|
75
|
+
logger.debug("[OTLP] Setting up OTLP exporter")
|
|
76
|
+
dsn = options.get("dsn") if options else None # type: Optional[str]
|
|
77
|
+
setup_otlp_exporter(dsn)
|
|
78
|
+
|
|
79
|
+
if self.setup_propagator:
|
|
80
|
+
logger.debug("[OTLP] Setting up propagator for distributed tracing")
|
|
81
|
+
# TODO-neel better propagator support, chain with existing ones if possible instead of replacing
|
|
82
|
+
set_global_textmap(SentryPropagator())
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
from __future__ import absolute_import
|
|
2
|
-
|
|
3
1
|
import ast
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
from sentry_sdk
|
|
3
|
+
import sentry_sdk
|
|
4
|
+
from sentry_sdk import serializer
|
|
7
5
|
from sentry_sdk.integrations import Integration, DidNotEnable
|
|
8
6
|
from sentry_sdk.scope import add_global_event_processor
|
|
9
7
|
from sentry_sdk.utils import walk_exception_chain, iter_stacks
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
12
|
from typing import Optional, Dict, Any, Tuple, List
|
|
13
13
|
from types import FrameType
|
|
14
14
|
|
|
@@ -41,7 +41,7 @@ class PureEvalIntegration(Integration):
|
|
|
41
41
|
@add_global_event_processor
|
|
42
42
|
def add_executing_info(event, hint):
|
|
43
43
|
# type: (Event, Optional[Hint]) -> Optional[Event]
|
|
44
|
-
if
|
|
44
|
+
if sentry_sdk.get_client().get_integration(PureEvalIntegration) is None:
|
|
45
45
|
return event
|
|
46
46
|
|
|
47
47
|
if hint is None:
|
|
@@ -104,29 +104,38 @@ def pure_eval_frame(frame):
|
|
|
104
104
|
expressions = evaluator.interesting_expressions_grouped(scope)
|
|
105
105
|
|
|
106
106
|
def closeness(expression):
|
|
107
|
-
# type: (Tuple[List[Any], Any]) -> int
|
|
107
|
+
# type: (Tuple[List[Any], Any]) -> Tuple[int, int]
|
|
108
108
|
# Prioritise expressions with a node closer to the statement executed
|
|
109
109
|
# without being after that statement
|
|
110
110
|
# A higher return value is better - the expression will appear
|
|
111
111
|
# earlier in the list of values and is less likely to be trimmed
|
|
112
112
|
nodes, _value = expression
|
|
113
|
+
|
|
114
|
+
def start(n):
|
|
115
|
+
# type: (ast.expr) -> Tuple[int, int]
|
|
116
|
+
return (n.lineno, n.col_offset)
|
|
117
|
+
|
|
113
118
|
nodes_before_stmt = [
|
|
114
|
-
node
|
|
119
|
+
node
|
|
120
|
+
for node in nodes
|
|
121
|
+
if start(node) < stmt.last_token.end # type: ignore
|
|
115
122
|
]
|
|
116
123
|
if nodes_before_stmt:
|
|
117
124
|
# The position of the last node before or in the statement
|
|
118
|
-
return max(node
|
|
125
|
+
return max(start(node) for node in nodes_before_stmt)
|
|
119
126
|
else:
|
|
120
127
|
# The position of the first node after the statement
|
|
121
128
|
# Negative means it's always lower priority than nodes that come before
|
|
122
129
|
# Less negative means closer to the statement and higher priority
|
|
123
|
-
|
|
130
|
+
lineno, col_offset = min(start(node) for node in nodes)
|
|
131
|
+
return (-lineno, -col_offset)
|
|
124
132
|
|
|
125
133
|
# This adds the first_token and last_token attributes to nodes
|
|
126
134
|
atok = source.asttokens()
|
|
127
135
|
|
|
128
136
|
expressions.sort(key=closeness, reverse=True)
|
|
129
|
-
|
|
137
|
+
vars = {
|
|
130
138
|
atok.get_text(nodes[0]): value
|
|
131
139
|
for nodes, value in expressions[: serializer.MAX_DATABAG_BREADTH]
|
|
132
140
|
}
|
|
141
|
+
return serializer.serialize(vars, is_vars=True)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from sentry_sdk.integrations import DidNotEnable, Integration
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
import pydantic_ai # type: ignore
|
|
6
|
+
except ImportError:
|
|
7
|
+
raise DidNotEnable("pydantic-ai not installed")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from .patches import (
|
|
11
|
+
_patch_agent_run,
|
|
12
|
+
_patch_graph_nodes,
|
|
13
|
+
_patch_model_request,
|
|
14
|
+
_patch_tool_execution,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PydanticAIIntegration(Integration):
|
|
19
|
+
identifier = "pydantic_ai"
|
|
20
|
+
origin = f"auto.ai.{identifier}"
|
|
21
|
+
|
|
22
|
+
def __init__(self, include_prompts=True):
|
|
23
|
+
# type: (bool) -> None
|
|
24
|
+
"""
|
|
25
|
+
Initialize the Pydantic AI integration.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
include_prompts: Whether to include prompts and messages in span data.
|
|
29
|
+
Requires send_default_pii=True. Defaults to True.
|
|
30
|
+
"""
|
|
31
|
+
self.include_prompts = include_prompts
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def setup_once():
|
|
35
|
+
# type: () -> None
|
|
36
|
+
"""
|
|
37
|
+
Set up the pydantic-ai integration.
|
|
38
|
+
|
|
39
|
+
This patches the key methods in pydantic-ai to create Sentry spans for:
|
|
40
|
+
- Agent invocations (Agent.run methods)
|
|
41
|
+
- Model requests (AI client calls)
|
|
42
|
+
- Tool executions
|
|
43
|
+
"""
|
|
44
|
+
_patch_agent_run()
|
|
45
|
+
_patch_graph_nodes()
|
|
46
|
+
_patch_model_request()
|
|
47
|
+
_patch_tool_execution()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
SPAN_ORIGIN = "auto.ai.pydantic_ai"
|