sentry-sdk 3.0.0a1__py2.py3-none-any.whl → 3.0.0a3__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/__init__.py +2 -0
- sentry_sdk/_compat.py +5 -12
- sentry_sdk/_init_implementation.py +7 -7
- sentry_sdk/_log_batcher.py +17 -29
- sentry_sdk/_lru_cache.py +7 -9
- sentry_sdk/_queue.py +2 -4
- sentry_sdk/_types.py +11 -18
- sentry_sdk/_werkzeug.py +5 -7
- sentry_sdk/ai/monitoring.py +44 -31
- sentry_sdk/ai/utils.py +3 -4
- sentry_sdk/api.py +75 -87
- sentry_sdk/attachments.py +10 -12
- sentry_sdk/client.py +137 -155
- sentry_sdk/consts.py +430 -174
- sentry_sdk/crons/api.py +16 -17
- sentry_sdk/crons/decorator.py +25 -27
- sentry_sdk/debug.py +4 -6
- sentry_sdk/envelope.py +46 -112
- sentry_sdk/feature_flags.py +9 -15
- sentry_sdk/integrations/__init__.py +24 -19
- sentry_sdk/integrations/_asgi_common.py +15 -18
- sentry_sdk/integrations/_wsgi_common.py +22 -33
- sentry_sdk/integrations/aiohttp.py +32 -30
- sentry_sdk/integrations/anthropic.py +42 -37
- sentry_sdk/integrations/argv.py +3 -4
- sentry_sdk/integrations/ariadne.py +16 -18
- sentry_sdk/integrations/arq.py +21 -29
- sentry_sdk/integrations/asgi.py +63 -37
- sentry_sdk/integrations/asyncio.py +14 -16
- sentry_sdk/integrations/atexit.py +6 -10
- sentry_sdk/integrations/aws_lambda.py +26 -36
- sentry_sdk/integrations/beam.py +10 -18
- sentry_sdk/integrations/boto3.py +18 -16
- sentry_sdk/integrations/bottle.py +25 -34
- sentry_sdk/integrations/celery/__init__.py +41 -61
- sentry_sdk/integrations/celery/beat.py +23 -27
- sentry_sdk/integrations/celery/utils.py +15 -17
- sentry_sdk/integrations/chalice.py +8 -10
- sentry_sdk/integrations/clickhouse_driver.py +21 -31
- sentry_sdk/integrations/cloud_resource_context.py +9 -16
- sentry_sdk/integrations/cohere.py +27 -33
- sentry_sdk/integrations/dedupe.py +5 -8
- sentry_sdk/integrations/django/__init__.py +57 -72
- sentry_sdk/integrations/django/asgi.py +26 -34
- sentry_sdk/integrations/django/caching.py +23 -19
- sentry_sdk/integrations/django/middleware.py +17 -20
- sentry_sdk/integrations/django/signals_handlers.py +11 -10
- sentry_sdk/integrations/django/templates.py +19 -16
- sentry_sdk/integrations/django/transactions.py +16 -11
- sentry_sdk/integrations/django/views.py +6 -10
- sentry_sdk/integrations/dramatiq.py +21 -21
- sentry_sdk/integrations/excepthook.py +10 -10
- sentry_sdk/integrations/executing.py +3 -4
- sentry_sdk/integrations/falcon.py +27 -42
- sentry_sdk/integrations/fastapi.py +13 -16
- sentry_sdk/integrations/flask.py +31 -38
- sentry_sdk/integrations/gcp.py +13 -16
- sentry_sdk/integrations/gnu_backtrace.py +4 -6
- sentry_sdk/integrations/gql.py +16 -17
- sentry_sdk/integrations/graphene.py +13 -12
- sentry_sdk/integrations/grpc/__init__.py +19 -1
- sentry_sdk/integrations/grpc/aio/server.py +15 -14
- sentry_sdk/integrations/grpc/client.py +19 -9
- sentry_sdk/integrations/grpc/consts.py +2 -0
- sentry_sdk/integrations/grpc/server.py +12 -8
- sentry_sdk/integrations/httpx.py +9 -12
- sentry_sdk/integrations/huey.py +13 -20
- sentry_sdk/integrations/huggingface_hub.py +18 -18
- sentry_sdk/integrations/langchain.py +203 -113
- sentry_sdk/integrations/launchdarkly.py +13 -10
- sentry_sdk/integrations/litestar.py +37 -35
- sentry_sdk/integrations/logging.py +52 -65
- sentry_sdk/integrations/loguru.py +127 -57
- sentry_sdk/integrations/modules.py +3 -4
- sentry_sdk/integrations/openai.py +100 -88
- sentry_sdk/integrations/openai_agents/__init__.py +49 -0
- sentry_sdk/integrations/openai_agents/consts.py +1 -0
- sentry_sdk/integrations/openai_agents/patches/__init__.py +4 -0
- sentry_sdk/integrations/openai_agents/patches/agent_run.py +152 -0
- sentry_sdk/integrations/openai_agents/patches/models.py +52 -0
- sentry_sdk/integrations/openai_agents/patches/runner.py +42 -0
- sentry_sdk/integrations/openai_agents/patches/tools.py +84 -0
- sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +20 -0
- sentry_sdk/integrations/openai_agents/spans/ai_client.py +46 -0
- sentry_sdk/integrations/openai_agents/spans/execute_tool.py +47 -0
- sentry_sdk/integrations/openai_agents/spans/handoff.py +24 -0
- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +41 -0
- sentry_sdk/integrations/openai_agents/utils.py +201 -0
- sentry_sdk/integrations/openfeature.py +11 -6
- sentry_sdk/integrations/pure_eval.py +6 -10
- sentry_sdk/integrations/pymongo.py +13 -17
- sentry_sdk/integrations/pyramid.py +31 -36
- sentry_sdk/integrations/quart.py +23 -28
- sentry_sdk/integrations/ray.py +73 -64
- sentry_sdk/integrations/redis/__init__.py +7 -4
- sentry_sdk/integrations/redis/_async_common.py +25 -12
- sentry_sdk/integrations/redis/_sync_common.py +19 -13
- sentry_sdk/integrations/redis/modules/caches.py +17 -8
- sentry_sdk/integrations/redis/modules/queries.py +9 -8
- sentry_sdk/integrations/redis/rb.py +3 -2
- sentry_sdk/integrations/redis/redis.py +4 -4
- sentry_sdk/integrations/redis/redis_cluster.py +21 -13
- sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +3 -2
- sentry_sdk/integrations/redis/utils.py +23 -24
- sentry_sdk/integrations/rq.py +13 -16
- sentry_sdk/integrations/rust_tracing.py +9 -6
- sentry_sdk/integrations/sanic.py +34 -46
- sentry_sdk/integrations/serverless.py +22 -27
- sentry_sdk/integrations/socket.py +27 -15
- sentry_sdk/integrations/spark/__init__.py +1 -0
- sentry_sdk/integrations/spark/spark_driver.py +45 -83
- sentry_sdk/integrations/spark/spark_worker.py +7 -11
- sentry_sdk/integrations/sqlalchemy.py +22 -19
- sentry_sdk/integrations/starlette.py +86 -90
- sentry_sdk/integrations/starlite.py +28 -34
- sentry_sdk/integrations/statsig.py +5 -4
- sentry_sdk/integrations/stdlib.py +28 -24
- sentry_sdk/integrations/strawberry.py +62 -49
- sentry_sdk/integrations/sys_exit.py +7 -11
- sentry_sdk/integrations/threading.py +12 -14
- sentry_sdk/integrations/tornado.py +28 -32
- sentry_sdk/integrations/trytond.py +4 -3
- sentry_sdk/integrations/typer.py +8 -6
- sentry_sdk/integrations/unleash.py +5 -4
- sentry_sdk/integrations/wsgi.py +47 -46
- sentry_sdk/logger.py +41 -10
- sentry_sdk/monitor.py +16 -28
- sentry_sdk/opentelemetry/consts.py +11 -4
- sentry_sdk/opentelemetry/contextvars_context.py +26 -16
- sentry_sdk/opentelemetry/propagator.py +38 -21
- sentry_sdk/opentelemetry/sampler.py +51 -34
- sentry_sdk/opentelemetry/scope.py +36 -37
- sentry_sdk/opentelemetry/span_processor.py +48 -58
- sentry_sdk/opentelemetry/tracing.py +58 -14
- sentry_sdk/opentelemetry/utils.py +186 -194
- sentry_sdk/profiler/continuous_profiler.py +108 -97
- sentry_sdk/profiler/transaction_profiler.py +70 -97
- sentry_sdk/profiler/utils.py +11 -15
- sentry_sdk/scope.py +251 -273
- sentry_sdk/scrubber.py +22 -26
- sentry_sdk/serializer.py +40 -54
- sentry_sdk/session.py +44 -61
- sentry_sdk/sessions.py +35 -49
- sentry_sdk/spotlight.py +15 -21
- sentry_sdk/tracing.py +121 -187
- sentry_sdk/tracing_utils.py +104 -122
- sentry_sdk/transport.py +131 -157
- sentry_sdk/utils.py +232 -309
- sentry_sdk/worker.py +16 -28
- {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a3.dist-info}/METADATA +3 -3
- sentry_sdk-3.0.0a3.dist-info/RECORD +168 -0
- {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a3.dist-info}/WHEEL +1 -1
- sentry_sdk-3.0.0a1.dist-info/RECORD +0 -154
- {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a3.dist-info}/entry_points.txt +0 -0
- {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a3.dist-info}/licenses/LICENSE +0 -0
- {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a3.dist-info}/top_level.txt +0 -0
|
@@ -1,35 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
from opentelemetry import trace
|
|
2
3
|
from opentelemetry.propagate import set_global_textmap
|
|
4
|
+
from opentelemetry.sdk.resources import Resource
|
|
3
5
|
from opentelemetry.sdk.trace import TracerProvider, Span, ReadableSpan
|
|
4
6
|
|
|
7
|
+
from sentry_sdk.consts import VERSION
|
|
5
8
|
from sentry_sdk.opentelemetry import (
|
|
6
9
|
SentryPropagator,
|
|
7
10
|
SentrySampler,
|
|
8
11
|
SentrySpanProcessor,
|
|
9
12
|
)
|
|
13
|
+
from sentry_sdk.opentelemetry.consts import (
|
|
14
|
+
RESOURCE_SERVICE_NAME,
|
|
15
|
+
RESOURCE_SERVICE_NAMESPACE,
|
|
16
|
+
RESOURCE_SERVICE_VERSION,
|
|
17
|
+
)
|
|
18
|
+
from sentry_sdk.utils import logger
|
|
19
|
+
|
|
10
20
|
|
|
21
|
+
READABLE_SPAN_PATCHED = False
|
|
11
22
|
|
|
12
|
-
|
|
13
|
-
|
|
23
|
+
|
|
24
|
+
def patch_readable_span() -> None:
|
|
14
25
|
"""
|
|
15
26
|
We need to pass through sentry specific metadata/objects from Span to ReadableSpan
|
|
16
27
|
to work with them consistently in the SpanProcessor.
|
|
17
28
|
"""
|
|
18
|
-
|
|
29
|
+
global READABLE_SPAN_PATCHED
|
|
30
|
+
if not READABLE_SPAN_PATCHED:
|
|
31
|
+
old_readable_span = Span._readable_span
|
|
32
|
+
|
|
33
|
+
def sentry_patched_readable_span(self: Span) -> ReadableSpan:
|
|
34
|
+
readable_span = old_readable_span(self)
|
|
35
|
+
readable_span._sentry_meta = getattr(self, "_sentry_meta", {}) # type: ignore[attr-defined]
|
|
36
|
+
return readable_span
|
|
37
|
+
|
|
38
|
+
Span._readable_span = sentry_patched_readable_span # type: ignore[method-assign]
|
|
39
|
+
READABLE_SPAN_PATCHED = True
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def setup_sentry_tracing() -> None:
|
|
43
|
+
# TracerProvider can only be set once. If we're the first ones setting it,
|
|
44
|
+
# there's no issue. If it already exists, we need to patch it.
|
|
45
|
+
from opentelemetry.trace import _TRACER_PROVIDER
|
|
19
46
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return readable_span
|
|
47
|
+
if _TRACER_PROVIDER is not None:
|
|
48
|
+
logger.debug("[Tracing] Detected an existing TracerProvider, patching")
|
|
49
|
+
tracer_provider = _TRACER_PROVIDER
|
|
50
|
+
tracer_provider.sampler = SentrySampler() # type: ignore[attr-defined]
|
|
25
51
|
|
|
26
|
-
|
|
52
|
+
else:
|
|
53
|
+
logger.debug("[Tracing] No TracerProvider set, creating a new one")
|
|
54
|
+
tracer_provider = TracerProvider(
|
|
55
|
+
sampler=SentrySampler(),
|
|
56
|
+
resource=Resource.create(
|
|
57
|
+
{
|
|
58
|
+
RESOURCE_SERVICE_NAME: "sentry-python",
|
|
59
|
+
RESOURCE_SERVICE_VERSION: VERSION,
|
|
60
|
+
RESOURCE_SERVICE_NAMESPACE: "sentry",
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
trace.set_tracer_provider(tracer_provider)
|
|
27
65
|
|
|
66
|
+
try:
|
|
67
|
+
existing_span_processors = (
|
|
68
|
+
tracer_provider._active_span_processor._span_processors # type: ignore[attr-defined]
|
|
69
|
+
)
|
|
70
|
+
except Exception:
|
|
71
|
+
existing_span_processors = []
|
|
28
72
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
73
|
+
for span_processor in existing_span_processors:
|
|
74
|
+
if isinstance(span_processor, SentrySpanProcessor):
|
|
75
|
+
break
|
|
76
|
+
else:
|
|
77
|
+
tracer_provider.add_span_processor(SentrySpanProcessor()) # type: ignore[attr-defined]
|
|
34
78
|
|
|
35
79
|
set_global_textmap(SentryPropagator())
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import re
|
|
2
|
-
from typing import cast
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
|
|
5
6
|
from urllib3.util import parse_url as urlparse
|
|
6
7
|
from urllib.parse import quote, unquote
|
|
@@ -30,8 +31,10 @@ from sentry_sdk.tracing_utils import Baggage, get_span_status_from_http_code
|
|
|
30
31
|
from sentry_sdk._types import TYPE_CHECKING
|
|
31
32
|
|
|
32
33
|
if TYPE_CHECKING:
|
|
33
|
-
from typing import Any, Optional,
|
|
34
|
-
from
|
|
34
|
+
from typing import Any, Optional, Union, Type, TypeVar
|
|
35
|
+
from opentelemetry.util.types import Attributes
|
|
36
|
+
|
|
37
|
+
T = TypeVar("T")
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
GRPC_ERROR_MAP = {
|
|
@@ -54,8 +57,7 @@ GRPC_ERROR_MAP = {
|
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
|
|
57
|
-
def is_sentry_span(span):
|
|
58
|
-
# type: (ReadableSpan) -> bool
|
|
60
|
+
def is_sentry_span(span: ReadableSpan) -> bool:
|
|
59
61
|
"""
|
|
60
62
|
Break infinite loop:
|
|
61
63
|
HTTP requests to Sentry are caught by OTel and send again to Sentry.
|
|
@@ -65,10 +67,8 @@ def is_sentry_span(span):
|
|
|
65
67
|
if not span.attributes:
|
|
66
68
|
return False
|
|
67
69
|
|
|
68
|
-
span_url = span.attributes
|
|
69
|
-
span_url
|
|
70
|
-
|
|
71
|
-
if not span_url:
|
|
70
|
+
span_url = get_typed_attribute(span.attributes, SpanAttributes.HTTP_URL, str)
|
|
71
|
+
if span_url is None:
|
|
72
72
|
return False
|
|
73
73
|
|
|
74
74
|
dsn_url = None
|
|
@@ -89,201 +89,184 @@ def is_sentry_span(span):
|
|
|
89
89
|
return False
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def convert_from_otel_timestamp(time):
|
|
93
|
-
# type: (int) -> datetime
|
|
92
|
+
def convert_from_otel_timestamp(time: int) -> datetime:
|
|
94
93
|
"""Convert an OTel nanosecond-level timestamp to a datetime."""
|
|
95
94
|
return datetime.fromtimestamp(time / 1e9, timezone.utc)
|
|
96
95
|
|
|
97
96
|
|
|
98
|
-
def convert_to_otel_timestamp(time):
|
|
99
|
-
# type: (Union[datetime, float]) -> int
|
|
97
|
+
def convert_to_otel_timestamp(time: Union[datetime, float]) -> int:
|
|
100
98
|
"""Convert a datetime to an OTel timestamp (with nanosecond precision)."""
|
|
101
99
|
if isinstance(time, datetime):
|
|
102
100
|
return int(time.timestamp() * 1e9)
|
|
103
101
|
return int(time * 1e9)
|
|
104
102
|
|
|
105
103
|
|
|
106
|
-
def extract_transaction_name_source(
|
|
107
|
-
|
|
104
|
+
def extract_transaction_name_source(
|
|
105
|
+
span: ReadableSpan,
|
|
106
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
108
107
|
if not span.attributes:
|
|
109
108
|
return (None, None)
|
|
110
109
|
return (
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
get_typed_attribute(span.attributes, SentrySpanAttribute.NAME, str),
|
|
111
|
+
get_typed_attribute(span.attributes, SentrySpanAttribute.SOURCE, str),
|
|
113
112
|
)
|
|
114
113
|
|
|
115
114
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
status
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return (op, description, status, http_status, origin)
|
|
115
|
+
@dataclass
|
|
116
|
+
class ExtractedSpanData:
|
|
117
|
+
description: str
|
|
118
|
+
op: Optional[str] = None
|
|
119
|
+
status: Optional[str] = None
|
|
120
|
+
http_status: Optional[int] = None
|
|
121
|
+
origin: Optional[str] = None
|
|
124
122
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
|
|
124
|
+
def extract_span_data(span: ReadableSpan) -> ExtractedSpanData:
|
|
125
|
+
"""
|
|
126
|
+
Try to populate sane values for op, description and statuses based on what we have.
|
|
127
|
+
The op and description mapping is fundamentally janky because otel only has a single `name`.
|
|
128
|
+
|
|
129
|
+
Priority is given first to attributes explicitly defined by us via the SDK.
|
|
130
|
+
Otherwise we try to infer sane values from other attributes.
|
|
131
|
+
"""
|
|
132
|
+
op = get_typed_attribute(span.attributes, SentrySpanAttribute.OP, str) or infer_op(
|
|
133
|
+
span
|
|
129
134
|
)
|
|
130
|
-
origin = cast("Optional[str]", span.attributes.get(SentrySpanAttribute.ORIGIN))
|
|
131
|
-
|
|
132
|
-
http_method = span.attributes.get(SpanAttributes.HTTP_METHOD)
|
|
133
|
-
http_method = cast("Optional[str]", http_method)
|
|
134
|
-
if http_method:
|
|
135
|
-
return span_data_for_http_method(span)
|
|
136
|
-
|
|
137
|
-
db_query = span.attributes.get(SpanAttributes.DB_SYSTEM)
|
|
138
|
-
if db_query:
|
|
139
|
-
return span_data_for_db_query(span)
|
|
140
|
-
|
|
141
|
-
rpc_service = span.attributes.get(SpanAttributes.RPC_SERVICE)
|
|
142
|
-
if rpc_service:
|
|
143
|
-
return (
|
|
144
|
-
attribute_op or "rpc",
|
|
145
|
-
description,
|
|
146
|
-
status,
|
|
147
|
-
http_status,
|
|
148
|
-
origin,
|
|
149
|
-
)
|
|
150
135
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
status,
|
|
157
|
-
http_status,
|
|
158
|
-
origin,
|
|
159
|
-
)
|
|
136
|
+
description = (
|
|
137
|
+
get_typed_attribute(span.attributes, SentrySpanAttribute.DESCRIPTION, str)
|
|
138
|
+
or get_typed_attribute(span.attributes, SentrySpanAttribute.NAME, str)
|
|
139
|
+
or infer_description(span)
|
|
140
|
+
)
|
|
160
141
|
|
|
161
|
-
|
|
162
|
-
if faas_trigger:
|
|
163
|
-
return (str(faas_trigger), description, status, http_status, origin)
|
|
142
|
+
origin = get_typed_attribute(span.attributes, SentrySpanAttribute.ORIGIN, str)
|
|
164
143
|
|
|
165
|
-
|
|
144
|
+
(status, http_status) = extract_span_status(span)
|
|
166
145
|
|
|
146
|
+
return ExtractedSpanData(
|
|
147
|
+
description=description or span.name,
|
|
148
|
+
op=op,
|
|
149
|
+
status=status,
|
|
150
|
+
http_status=http_status,
|
|
151
|
+
origin=origin,
|
|
152
|
+
)
|
|
167
153
|
|
|
168
|
-
def span_data_for_http_method(span):
|
|
169
|
-
# type: (ReadableSpan) -> OtelExtractedSpanData
|
|
170
|
-
span_attributes = span.attributes or {}
|
|
171
154
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
155
|
+
def infer_op(span: ReadableSpan) -> Optional[str]:
|
|
156
|
+
"""
|
|
157
|
+
Try to infer op for the various types of instrumentation.
|
|
158
|
+
"""
|
|
159
|
+
if span.attributes is None:
|
|
160
|
+
return None
|
|
175
161
|
|
|
162
|
+
if SpanAttributes.HTTP_METHOD in span.attributes:
|
|
176
163
|
if span.kind == SpanKind.SERVER:
|
|
177
|
-
|
|
164
|
+
return OP.HTTP_SERVER
|
|
178
165
|
elif span.kind == SpanKind.CLIENT:
|
|
179
|
-
|
|
166
|
+
return OP.HTTP_CLIENT
|
|
167
|
+
else:
|
|
168
|
+
return OP.HTTP
|
|
169
|
+
elif SpanAttributes.DB_SYSTEM in span.attributes:
|
|
170
|
+
return OP.DB
|
|
171
|
+
elif SpanAttributes.RPC_SERVICE in span.attributes:
|
|
172
|
+
return OP.RPC
|
|
173
|
+
elif SpanAttributes.MESSAGING_SYSTEM in span.attributes:
|
|
174
|
+
return OP.MESSAGE
|
|
175
|
+
elif SpanAttributes.FAAS_TRIGGER in span.attributes:
|
|
176
|
+
return get_typed_attribute(span.attributes, SpanAttributes.FAAS_TRIGGER, str)
|
|
177
|
+
else:
|
|
178
|
+
return None
|
|
180
179
|
|
|
181
|
-
http_method = span_attributes.get(SpanAttributes.HTTP_METHOD)
|
|
182
|
-
route = span_attributes.get(SpanAttributes.HTTP_ROUTE)
|
|
183
|
-
target = span_attributes.get(SpanAttributes.HTTP_TARGET)
|
|
184
|
-
peer_name = span_attributes.get(SpanAttributes.NET_PEER_NAME)
|
|
185
180
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
181
|
+
def infer_description(span: ReadableSpan) -> Optional[str]:
|
|
182
|
+
if span.attributes is None:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
if SpanAttributes.HTTP_METHOD in span.attributes:
|
|
186
|
+
http_method = get_typed_attribute(
|
|
187
|
+
span.attributes, SpanAttributes.HTTP_METHOD, str
|
|
188
|
+
)
|
|
189
|
+
route = get_typed_attribute(span.attributes, SpanAttributes.HTTP_ROUTE, str)
|
|
190
|
+
target = get_typed_attribute(span.attributes, SpanAttributes.HTTP_TARGET, str)
|
|
191
|
+
peer_name = get_typed_attribute(
|
|
192
|
+
span.attributes, SpanAttributes.NET_PEER_NAME, str
|
|
193
|
+
)
|
|
194
|
+
url = get_typed_attribute(span.attributes, SpanAttributes.HTTP_URL, str)
|
|
193
195
|
|
|
194
196
|
if route:
|
|
195
|
-
|
|
197
|
+
return f"{http_method} {route}"
|
|
196
198
|
elif target:
|
|
197
|
-
|
|
199
|
+
return f"{http_method} {target}"
|
|
198
200
|
elif peer_name:
|
|
199
|
-
|
|
201
|
+
return f"{http_method} {peer_name}"
|
|
202
|
+
elif url:
|
|
203
|
+
parsed_url = urlparse(url)
|
|
204
|
+
url = "{}://{}{}".format(
|
|
205
|
+
parsed_url.scheme, parsed_url.netloc, parsed_url.path
|
|
206
|
+
)
|
|
207
|
+
return f"{http_method} {url}"
|
|
200
208
|
else:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
url = "{}://{}{}".format(
|
|
207
|
-
parsed_url.scheme, parsed_url.netloc, parsed_url.path
|
|
208
|
-
)
|
|
209
|
-
description = f"{http_method} {url}"
|
|
210
|
-
|
|
211
|
-
status, http_status = extract_span_status(span)
|
|
212
|
-
|
|
213
|
-
origin = cast("Optional[str]", span_attributes.get(SentrySpanAttribute.ORIGIN))
|
|
214
|
-
|
|
215
|
-
return (op, description, status, http_status, origin)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def span_data_for_db_query(span):
|
|
219
|
-
# type: (ReadableSpan) -> OtelExtractedSpanData
|
|
220
|
-
span_attributes = span.attributes or {}
|
|
221
|
-
|
|
222
|
-
op = cast("str", span_attributes.get(SentrySpanAttribute.OP, OP.DB))
|
|
223
|
-
|
|
224
|
-
statement = span_attributes.get(SpanAttributes.DB_STATEMENT, None)
|
|
225
|
-
statement = cast("Optional[str]", statement)
|
|
226
|
-
|
|
227
|
-
description = statement or span.name
|
|
228
|
-
origin = cast("Optional[str]", span_attributes.get(SentrySpanAttribute.ORIGIN))
|
|
229
|
-
|
|
230
|
-
return (op, description, None, None, origin)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def extract_span_status(span):
|
|
234
|
-
# type: (ReadableSpan) -> tuple[Optional[str], Optional[int]]
|
|
235
|
-
span_attributes = span.attributes or {}
|
|
236
|
-
status = span.status or None
|
|
237
|
-
|
|
238
|
-
if status:
|
|
239
|
-
inferred_status, http_status = infer_status_from_attributes(span_attributes)
|
|
240
|
-
|
|
241
|
-
if status.status_code == StatusCode.OK:
|
|
242
|
-
return (SPANSTATUS.OK, http_status)
|
|
243
|
-
elif status.status_code == StatusCode.ERROR:
|
|
244
|
-
if status.description is None:
|
|
245
|
-
if inferred_status:
|
|
246
|
-
return (inferred_status, http_status)
|
|
247
|
-
|
|
248
|
-
if http_status is not None:
|
|
249
|
-
return (inferred_status, http_status)
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
status.description is not None
|
|
253
|
-
and status.description in GRPC_ERROR_MAP.values()
|
|
254
|
-
):
|
|
255
|
-
return (status.description, None)
|
|
256
|
-
else:
|
|
257
|
-
return (SPANSTATUS.UNKNOWN_ERROR, None)
|
|
209
|
+
return http_method
|
|
210
|
+
elif SpanAttributes.DB_SYSTEM in span.attributes:
|
|
211
|
+
return get_typed_attribute(span.attributes, SpanAttributes.DB_STATEMENT, str)
|
|
212
|
+
else:
|
|
213
|
+
return None
|
|
258
214
|
|
|
259
|
-
inferred_status, http_status = infer_status_from_attributes(span_attributes)
|
|
260
|
-
if inferred_status:
|
|
261
|
-
return (inferred_status, http_status)
|
|
262
215
|
|
|
263
|
-
|
|
264
|
-
|
|
216
|
+
def extract_span_status(span: ReadableSpan) -> tuple[Optional[str], Optional[int]]:
|
|
217
|
+
"""
|
|
218
|
+
Extract a reasonable Sentry SPANSTATUS and a HTTP status code from the otel span.
|
|
219
|
+
OKs are simply OKs.
|
|
220
|
+
ERRORs first try to map to HTTP/GRPC statuses via attributes otherwise fallback
|
|
221
|
+
on the description if it is a valid status for Sentry.
|
|
222
|
+
In the final UNSET case, we try to infer HTTP/GRPC.
|
|
223
|
+
"""
|
|
224
|
+
status = span.status
|
|
225
|
+
http_status = get_http_status_code(span.attributes)
|
|
226
|
+
final_status = None
|
|
227
|
+
|
|
228
|
+
if status.status_code == StatusCode.OK:
|
|
229
|
+
final_status = SPANSTATUS.OK
|
|
230
|
+
elif status.status_code == StatusCode.ERROR:
|
|
231
|
+
inferred_status = infer_status_from_attributes(span.attributes, http_status)
|
|
232
|
+
|
|
233
|
+
if inferred_status is not None:
|
|
234
|
+
final_status = inferred_status
|
|
235
|
+
elif (
|
|
236
|
+
status.description is not None
|
|
237
|
+
and status.description in GRPC_ERROR_MAP.values()
|
|
238
|
+
):
|
|
239
|
+
final_status = status.description
|
|
240
|
+
else:
|
|
241
|
+
final_status = SPANSTATUS.UNKNOWN_ERROR
|
|
265
242
|
else:
|
|
266
|
-
|
|
243
|
+
# UNSET case
|
|
244
|
+
final_status = infer_status_from_attributes(span.attributes, http_status)
|
|
267
245
|
|
|
246
|
+
return (final_status, http_status)
|
|
268
247
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
http_status
|
|
248
|
+
|
|
249
|
+
def infer_status_from_attributes(
|
|
250
|
+
span_attributes: Attributes, http_status: Optional[int]
|
|
251
|
+
) -> Optional[str]:
|
|
252
|
+
if span_attributes is None:
|
|
253
|
+
return None
|
|
272
254
|
|
|
273
255
|
if http_status:
|
|
274
|
-
return
|
|
256
|
+
return get_span_status_from_http_code(http_status)
|
|
275
257
|
|
|
276
258
|
grpc_status = span_attributes.get(SpanAttributes.RPC_GRPC_STATUS_CODE)
|
|
277
259
|
if grpc_status:
|
|
278
|
-
return
|
|
260
|
+
return GRPC_ERROR_MAP.get(str(grpc_status), SPANSTATUS.UNKNOWN_ERROR)
|
|
279
261
|
|
|
280
|
-
return
|
|
262
|
+
return None
|
|
281
263
|
|
|
282
264
|
|
|
283
|
-
def get_http_status_code(span_attributes):
|
|
284
|
-
# type: (Mapping[str, str | bool | int | float | Sequence[str] | Sequence[bool] | Sequence[int] | Sequence[float]]) -> Optional[int]
|
|
265
|
+
def get_http_status_code(span_attributes: Attributes) -> Optional[int]:
|
|
285
266
|
try:
|
|
286
|
-
http_status =
|
|
267
|
+
http_status = get_typed_attribute(
|
|
268
|
+
span_attributes, SpanAttributes.HTTP_RESPONSE_STATUS_CODE, int
|
|
269
|
+
)
|
|
287
270
|
except AttributeError:
|
|
288
271
|
# HTTP_RESPONSE_STATUS_CODE was added in 1.21, so if we're on an older
|
|
289
272
|
# OTel version SpanAttributes.HTTP_RESPONSE_STATUS_CODE will throw an
|
|
@@ -292,19 +275,18 @@ def get_http_status_code(span_attributes):
|
|
|
292
275
|
|
|
293
276
|
if http_status is None:
|
|
294
277
|
# Fall back to the deprecated attribute
|
|
295
|
-
http_status =
|
|
296
|
-
|
|
297
|
-
|
|
278
|
+
http_status = get_typed_attribute(
|
|
279
|
+
span_attributes, SpanAttributes.HTTP_STATUS_CODE, int
|
|
280
|
+
)
|
|
298
281
|
|
|
299
282
|
return http_status
|
|
300
283
|
|
|
301
284
|
|
|
302
|
-
def extract_span_attributes(span, namespace):
|
|
303
|
-
# type: (ReadableSpan, str) -> dict[str, Any]
|
|
285
|
+
def extract_span_attributes(span: ReadableSpan, namespace: str) -> dict[str, Any]:
|
|
304
286
|
"""
|
|
305
287
|
Extract Sentry-specific span attributes and make them look the way Sentry expects.
|
|
306
288
|
"""
|
|
307
|
-
extracted_attrs
|
|
289
|
+
extracted_attrs: dict[str, Any] = {}
|
|
308
290
|
|
|
309
291
|
for attr, value in (span.attributes or {}).items():
|
|
310
292
|
if attr.startswith(namespace):
|
|
@@ -314,8 +296,9 @@ def extract_span_attributes(span, namespace):
|
|
|
314
296
|
return extracted_attrs
|
|
315
297
|
|
|
316
298
|
|
|
317
|
-
def get_trace_context(
|
|
318
|
-
|
|
299
|
+
def get_trace_context(
|
|
300
|
+
span: ReadableSpan, span_data: Optional[ExtractedSpanData] = None
|
|
301
|
+
) -> dict[str, Any]:
|
|
319
302
|
if not span.context:
|
|
320
303
|
return {}
|
|
321
304
|
|
|
@@ -326,32 +309,27 @@ def get_trace_context(span, span_data=None):
|
|
|
326
309
|
if span_data is None:
|
|
327
310
|
span_data = extract_span_data(span)
|
|
328
311
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
trace_context = {
|
|
312
|
+
trace_context: dict[str, Any] = {
|
|
332
313
|
"trace_id": trace_id,
|
|
333
314
|
"span_id": span_id,
|
|
334
315
|
"parent_span_id": parent_span_id,
|
|
335
|
-
"
|
|
336
|
-
|
|
337
|
-
} # type: dict[str, Any]
|
|
338
|
-
|
|
339
|
-
if status:
|
|
340
|
-
trace_context["status"] = status
|
|
316
|
+
"origin": span_data.origin or DEFAULT_SPAN_ORIGIN,
|
|
317
|
+
}
|
|
341
318
|
|
|
319
|
+
if span_data.op:
|
|
320
|
+
trace_context["op"] = span_data.op
|
|
321
|
+
if span_data.status:
|
|
322
|
+
trace_context["status"] = span_data.status
|
|
342
323
|
if span.attributes:
|
|
343
324
|
trace_context["data"] = dict(span.attributes)
|
|
344
325
|
|
|
345
326
|
trace_state = get_trace_state(span)
|
|
346
327
|
trace_context["dynamic_sampling_context"] = dsc_from_trace_state(trace_state)
|
|
347
328
|
|
|
348
|
-
# TODO-neel-potel profiler thread_id, thread_name
|
|
349
|
-
|
|
350
329
|
return trace_context
|
|
351
330
|
|
|
352
331
|
|
|
353
|
-
def trace_state_from_baggage(baggage):
|
|
354
|
-
# type: (Baggage) -> TraceState
|
|
332
|
+
def trace_state_from_baggage(baggage: Baggage) -> TraceState:
|
|
355
333
|
items = []
|
|
356
334
|
for k, v in baggage.sentry_items.items():
|
|
357
335
|
key = Baggage.SENTRY_PREFIX + quote(k)
|
|
@@ -360,13 +338,11 @@ def trace_state_from_baggage(baggage):
|
|
|
360
338
|
return TraceState(items)
|
|
361
339
|
|
|
362
340
|
|
|
363
|
-
def baggage_from_trace_state(trace_state):
|
|
364
|
-
# type: (TraceState) -> Baggage
|
|
341
|
+
def baggage_from_trace_state(trace_state: TraceState) -> Baggage:
|
|
365
342
|
return Baggage(dsc_from_trace_state(trace_state))
|
|
366
343
|
|
|
367
344
|
|
|
368
|
-
def serialize_trace_state(trace_state):
|
|
369
|
-
# type: (TraceState) -> str
|
|
345
|
+
def serialize_trace_state(trace_state: TraceState) -> str:
|
|
370
346
|
sentry_items = []
|
|
371
347
|
for k, v in trace_state.items():
|
|
372
348
|
if Baggage.SENTRY_PREFIX_REGEX.match(k):
|
|
@@ -374,8 +350,7 @@ def serialize_trace_state(trace_state):
|
|
|
374
350
|
return ",".join(key + "=" + value for key, value in sentry_items)
|
|
375
351
|
|
|
376
352
|
|
|
377
|
-
def dsc_from_trace_state(trace_state):
|
|
378
|
-
# type: (TraceState) -> dict[str, str]
|
|
353
|
+
def dsc_from_trace_state(trace_state: TraceState) -> dict[str, str]:
|
|
379
354
|
dsc = {}
|
|
380
355
|
for k, v in trace_state.items():
|
|
381
356
|
if Baggage.SENTRY_PREFIX_REGEX.match(k):
|
|
@@ -384,16 +359,14 @@ def dsc_from_trace_state(trace_state):
|
|
|
384
359
|
return dsc
|
|
385
360
|
|
|
386
361
|
|
|
387
|
-
def has_incoming_trace(trace_state):
|
|
388
|
-
# type: (TraceState) -> bool
|
|
362
|
+
def has_incoming_trace(trace_state: TraceState) -> bool:
|
|
389
363
|
"""
|
|
390
364
|
The existence of a sentry-trace_id in the baggage implies we continued an upstream trace.
|
|
391
365
|
"""
|
|
392
366
|
return (Baggage.SENTRY_PREFIX + "trace_id") in trace_state
|
|
393
367
|
|
|
394
368
|
|
|
395
|
-
def get_trace_state(span):
|
|
396
|
-
# type: (Union[AbstractSpan, ReadableSpan]) -> TraceState
|
|
369
|
+
def get_trace_state(span: Union[AbstractSpan, ReadableSpan]) -> TraceState:
|
|
397
370
|
"""
|
|
398
371
|
Get the existing trace_state with sentry items
|
|
399
372
|
or populate it if we are the head SDK.
|
|
@@ -451,26 +424,45 @@ def get_trace_state(span):
|
|
|
451
424
|
return trace_state
|
|
452
425
|
|
|
453
426
|
|
|
454
|
-
def get_sentry_meta(span, key):
|
|
455
|
-
# type: (Union[AbstractSpan, ReadableSpan], str) -> Any
|
|
427
|
+
def get_sentry_meta(span: Union[AbstractSpan, ReadableSpan], key: str) -> Any:
|
|
456
428
|
sentry_meta = getattr(span, "_sentry_meta", None)
|
|
457
429
|
return sentry_meta.get(key) if sentry_meta else None
|
|
458
430
|
|
|
459
431
|
|
|
460
|
-
def set_sentry_meta(
|
|
461
|
-
|
|
432
|
+
def set_sentry_meta(
|
|
433
|
+
span: Union[AbstractSpan, ReadableSpan], key: str, value: Any
|
|
434
|
+
) -> None:
|
|
462
435
|
sentry_meta = getattr(span, "_sentry_meta", {})
|
|
463
436
|
sentry_meta[key] = value
|
|
464
437
|
span._sentry_meta = sentry_meta # type: ignore[union-attr]
|
|
465
438
|
|
|
466
439
|
|
|
467
|
-
def
|
|
468
|
-
|
|
440
|
+
def delete_sentry_meta(span: Union[AbstractSpan, ReadableSpan]) -> None:
|
|
441
|
+
try:
|
|
442
|
+
del span._sentry_meta # type: ignore[union-attr]
|
|
443
|
+
except AttributeError:
|
|
444
|
+
pass
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def get_profile_context(span: ReadableSpan) -> Optional[dict[str, str]]:
|
|
469
448
|
if not span.attributes:
|
|
470
449
|
return None
|
|
471
450
|
|
|
472
|
-
profiler_id =
|
|
451
|
+
profiler_id = get_typed_attribute(span.attributes, SPANDATA.PROFILER_ID, str)
|
|
473
452
|
if profiler_id is None:
|
|
474
453
|
return None
|
|
475
454
|
|
|
476
455
|
return {"profiler_id": profiler_id}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def get_typed_attribute(attributes: Attributes, key: str, type: Type[T]) -> Optional[T]:
|
|
459
|
+
"""
|
|
460
|
+
helper method to coerce types of attribute values
|
|
461
|
+
"""
|
|
462
|
+
if attributes is None:
|
|
463
|
+
return None
|
|
464
|
+
value = attributes.get(key)
|
|
465
|
+
if value is not None and isinstance(value, type):
|
|
466
|
+
return value
|
|
467
|
+
else:
|
|
468
|
+
return None
|