sentry-sdk 0.7.5__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 -30
- sentry_sdk/_compat.py +74 -61
- 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 +289 -0
- sentry_sdk/_types.py +338 -0
- 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 +496 -80
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +1023 -103
- sentry_sdk/consts.py +1438 -66
- 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 +15 -14
- sentry_sdk/envelope.py +369 -0
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +611 -280
- sentry_sdk/integrations/__init__.py +276 -49
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +180 -44
- sentry_sdk/integrations/aiohttp.py +291 -42
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +9 -8
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +341 -0
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +17 -10
- sentry_sdk/integrations/aws_lambda.py +377 -62
- sentry_sdk/integrations/beam.py +176 -0
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +221 -0
- 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 +134 -0
- 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 +48 -14
- sentry_sdk/integrations/django/__init__.py +584 -191
- sentry_sdk/integrations/django/asgi.py +245 -0
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +187 -0
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +79 -5
- sentry_sdk/integrations/django/transactions.py +49 -22
- sentry_sdk/integrations/django/views.py +96 -0
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +50 -13
- sentry_sdk/integrations/executing.py +67 -0
- sentry_sdk/integrations/falcon.py +272 -0
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +142 -88
- sentry_sdk/integrations/gcp.py +239 -0
- sentry_sdk/integrations/gnu_backtrace.py +99 -0
- 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 +307 -96
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +14 -31
- 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 +141 -0
- 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 +112 -68
- 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 +95 -37
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +294 -123
- sentry_sdk/integrations/serverless.py +48 -19
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/__init__.py +4 -0
- sentry_sdk/integrations/spark/spark_driver.py +316 -0
- sentry_sdk/integrations/spark/spark_worker.py +116 -0
- sentry_sdk/integrations/sqlalchemy.py +142 -0
- 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 +235 -29
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +158 -28
- sentry_sdk/integrations/tornado.py +84 -52
- sentry_sdk/integrations/trytond.py +50 -0
- 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 +201 -119
- 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/py.typed +0 -0
- sentry_sdk/scope.py +1713 -85
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +405 -0
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +275 -0
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1486 -0
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +806 -134
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1625 -465
- sentry_sdk/worker.py +54 -25
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.7.5.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/integrations/celery.py +0 -119
- sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
- sentry_sdk-0.7.5.dist-info/METADATA +0 -36
- sentry_sdk-0.7.5.dist-info/RECORD +0 -39
- {sentry_sdk-0.7.5.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())
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
|
|
3
|
+
import sentry_sdk
|
|
4
|
+
from sentry_sdk import serializer
|
|
5
|
+
from sentry_sdk.integrations import Integration, DidNotEnable
|
|
6
|
+
from sentry_sdk.scope import add_global_event_processor
|
|
7
|
+
from sentry_sdk.utils import walk_exception_chain, iter_stacks
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from typing import Optional, Dict, Any, Tuple, List
|
|
13
|
+
from types import FrameType
|
|
14
|
+
|
|
15
|
+
from sentry_sdk._types import Event, Hint
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import executing
|
|
19
|
+
except ImportError:
|
|
20
|
+
raise DidNotEnable("executing is not installed")
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import pure_eval
|
|
24
|
+
except ImportError:
|
|
25
|
+
raise DidNotEnable("pure_eval is not installed")
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
# Used implicitly, just testing it's available
|
|
29
|
+
import asttokens # noqa
|
|
30
|
+
except ImportError:
|
|
31
|
+
raise DidNotEnable("asttokens is not installed")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PureEvalIntegration(Integration):
|
|
35
|
+
identifier = "pure_eval"
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def setup_once():
|
|
39
|
+
# type: () -> None
|
|
40
|
+
|
|
41
|
+
@add_global_event_processor
|
|
42
|
+
def add_executing_info(event, hint):
|
|
43
|
+
# type: (Event, Optional[Hint]) -> Optional[Event]
|
|
44
|
+
if sentry_sdk.get_client().get_integration(PureEvalIntegration) is None:
|
|
45
|
+
return event
|
|
46
|
+
|
|
47
|
+
if hint is None:
|
|
48
|
+
return event
|
|
49
|
+
|
|
50
|
+
exc_info = hint.get("exc_info", None)
|
|
51
|
+
|
|
52
|
+
if exc_info is None:
|
|
53
|
+
return event
|
|
54
|
+
|
|
55
|
+
exception = event.get("exception", None)
|
|
56
|
+
|
|
57
|
+
if exception is None:
|
|
58
|
+
return event
|
|
59
|
+
|
|
60
|
+
values = exception.get("values", None)
|
|
61
|
+
|
|
62
|
+
if values is None:
|
|
63
|
+
return event
|
|
64
|
+
|
|
65
|
+
for exception, (_exc_type, _exc_value, exc_tb) in zip(
|
|
66
|
+
reversed(values), walk_exception_chain(exc_info)
|
|
67
|
+
):
|
|
68
|
+
sentry_frames = [
|
|
69
|
+
frame
|
|
70
|
+
for frame in exception.get("stacktrace", {}).get("frames", [])
|
|
71
|
+
if frame.get("function")
|
|
72
|
+
]
|
|
73
|
+
tbs = list(iter_stacks(exc_tb))
|
|
74
|
+
if len(sentry_frames) != len(tbs):
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
for sentry_frame, tb in zip(sentry_frames, tbs):
|
|
78
|
+
sentry_frame["vars"] = (
|
|
79
|
+
pure_eval_frame(tb.tb_frame) or sentry_frame["vars"]
|
|
80
|
+
)
|
|
81
|
+
return event
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def pure_eval_frame(frame):
|
|
85
|
+
# type: (FrameType) -> Dict[str, Any]
|
|
86
|
+
source = executing.Source.for_frame(frame)
|
|
87
|
+
if not source.tree:
|
|
88
|
+
return {}
|
|
89
|
+
|
|
90
|
+
statements = source.statements_at_line(frame.f_lineno)
|
|
91
|
+
if not statements:
|
|
92
|
+
return {}
|
|
93
|
+
|
|
94
|
+
scope = stmt = list(statements)[0]
|
|
95
|
+
while True:
|
|
96
|
+
# Get the parent first in case the original statement is already
|
|
97
|
+
# a function definition, e.g. if we're calling a decorator
|
|
98
|
+
# In that case we still want the surrounding scope, not that function
|
|
99
|
+
scope = scope.parent
|
|
100
|
+
if isinstance(scope, (ast.FunctionDef, ast.ClassDef, ast.Module)):
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
evaluator = pure_eval.Evaluator.from_frame(frame)
|
|
104
|
+
expressions = evaluator.interesting_expressions_grouped(scope)
|
|
105
|
+
|
|
106
|
+
def closeness(expression):
|
|
107
|
+
# type: (Tuple[List[Any], Any]) -> Tuple[int, int]
|
|
108
|
+
# Prioritise expressions with a node closer to the statement executed
|
|
109
|
+
# without being after that statement
|
|
110
|
+
# A higher return value is better - the expression will appear
|
|
111
|
+
# earlier in the list of values and is less likely to be trimmed
|
|
112
|
+
nodes, _value = expression
|
|
113
|
+
|
|
114
|
+
def start(n):
|
|
115
|
+
# type: (ast.expr) -> Tuple[int, int]
|
|
116
|
+
return (n.lineno, n.col_offset)
|
|
117
|
+
|
|
118
|
+
nodes_before_stmt = [
|
|
119
|
+
node
|
|
120
|
+
for node in nodes
|
|
121
|
+
if start(node) < stmt.last_token.end # type: ignore
|
|
122
|
+
]
|
|
123
|
+
if nodes_before_stmt:
|
|
124
|
+
# The position of the last node before or in the statement
|
|
125
|
+
return max(start(node) for node in nodes_before_stmt)
|
|
126
|
+
else:
|
|
127
|
+
# The position of the first node after the statement
|
|
128
|
+
# Negative means it's always lower priority than nodes that come before
|
|
129
|
+
# Less negative means closer to the statement and higher priority
|
|
130
|
+
lineno, col_offset = min(start(node) for node in nodes)
|
|
131
|
+
return (-lineno, -col_offset)
|
|
132
|
+
|
|
133
|
+
# This adds the first_token and last_token attributes to nodes
|
|
134
|
+
atok = source.asttokens()
|
|
135
|
+
|
|
136
|
+
expressions.sort(key=closeness, reverse=True)
|
|
137
|
+
vars = {
|
|
138
|
+
atok.get_text(nodes[0]): value
|
|
139
|
+
for nodes, value in expressions[: serializer.MAX_DATABAG_BREADTH]
|
|
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"
|