Skip to content

Commit b230b84

Browse files
committed
Cleanup op, description and status mapping
1 parent 03659a4 commit b230b84

File tree

13 files changed

+192
-208
lines changed

13 files changed

+192
-208
lines changed

sentry_sdk/_types.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,5 +262,3 @@ class SDKInfo(TypedDict):
262262
)
263263

264264
HttpStatusCodeRange = Union[int, Container[int]]
265-
266-
OtelExtractedSpanData = tuple[str, str, Optional[str], Optional[int], Optional[str]]

sentry_sdk/consts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,7 @@ class OP:
674674
HTTP_CLIENT = "http.client"
675675
HTTP_CLIENT_STREAM = "http.client.stream"
676676
HTTP_SERVER = "http.server"
677+
MESSAGE = "message"
677678
MIDDLEWARE_DJANGO = "middleware.django"
678679
MIDDLEWARE_LITESTAR = "middleware.litestar"
679680
MIDDLEWARE_LITESTAR_RECEIVE = "middleware.litestar.receive"
@@ -705,6 +706,7 @@ class OP:
705706
QUEUE_TASK_HUEY = "queue.task.huey"
706707
QUEUE_SUBMIT_RAY = "queue.submit.ray"
707708
QUEUE_TASK_RAY = "queue.task.ray"
709+
RPC = "rpc"
708710
SUBPROCESS = "subprocess"
709711
SUBPROCESS_WAIT = "subprocess.wait"
710712
SUBPROCESS_COMMUNICATE = "subprocess.communicate"

sentry_sdk/opentelemetry/span_processor.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -219,18 +219,16 @@ def _root_span_to_transaction_event(self, span: ReadableSpan) -> Optional[Event]
219219
if profile_context:
220220
contexts["profile"] = profile_context
221221

222-
(_, description, _, http_status, _) = span_data
223-
224-
if http_status:
225-
contexts["response"] = {"status_code": http_status}
222+
if span_data.http_status:
223+
contexts["response"] = {"status_code": span_data.http_status}
226224

227225
if span.resource.attributes:
228226
contexts[OTEL_SENTRY_CONTEXT] = {"resource": dict(span.resource.attributes)}
229227

230228
event.update(
231229
{
232230
"type": "transaction",
233-
"transaction": transaction_name or description,
231+
"transaction": transaction_name or span_data.description,
234232
"transaction_info": {"source": transaction_source or "custom"},
235233
"contexts": contexts,
236234
}
@@ -257,19 +255,21 @@ def _span_to_json(self, span: ReadableSpan) -> Optional[dict[str, Any]]:
257255
span_id = format_span_id(span.context.span_id)
258256
parent_span_id = format_span_id(span.parent.span_id) if span.parent else None
259257

260-
(op, description, status, _, origin) = extract_span_data(span)
258+
span_data = extract_span_data(span)
261259

262260
span_json.update(
263261
{
264262
"trace_id": trace_id,
265263
"span_id": span_id,
266-
"op": op,
267-
"description": description,
268-
"status": status,
269-
"origin": origin or DEFAULT_SPAN_ORIGIN,
264+
"description": span_data.description,
265+
"origin": span_data.origin or DEFAULT_SPAN_ORIGIN,
270266
}
271267
)
272268

269+
if span_data.op:
270+
span_json["op"] = span_data.op
271+
if span_data.status:
272+
span_json["status"] = span_data.status
273273
if parent_span_id:
274274
span_json["parent_span_id"] = parent_span_id
275275

sentry_sdk/opentelemetry/utils.py

Lines changed: 94 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22
import re
33
from datetime import datetime, timezone
4+
from dataclasses import dataclass
45

56
from urllib3.util import parse_url as urlparse
67
from urllib.parse import quote, unquote
@@ -30,8 +31,7 @@
3031
from sentry_sdk._types import TYPE_CHECKING
3132

3233
if TYPE_CHECKING:
33-
from typing import Any, Optional, Mapping, Sequence, Union, Type, TypeVar
34-
from sentry_sdk._types import OtelExtractedSpanData
34+
from typing import Any, Optional, Mapping, Union, Type, TypeVar
3535

3636
T = TypeVar("T")
3737

@@ -111,115 +111,111 @@ def extract_transaction_name_source(
111111
)
112112

113113

114-
def extract_span_data(span: ReadableSpan) -> OtelExtractedSpanData:
115-
op = span.name
116-
description = span.name
117-
status, http_status = extract_span_status(span)
114+
@dataclass
115+
class ExtractedSpanData:
116+
description: str
117+
op: Optional[str] = None
118+
status: Optional[str] = None
119+
http_status: Optional[int] = None
120+
origin: Optional[str] = None
121+
122+
123+
def extract_span_data(span: ReadableSpan) -> ExtractedSpanData:
124+
"""
125+
Try to populate sane values for op, description and statuses based on what we have.
126+
The op and description mapping is fundamentally janky because otel only has a single `name`.
127+
128+
Priority is given first to attributes explicitly defined by us via the SDK.
129+
Otherwise we try to infer sane values from other attributes.
130+
"""
131+
op = None
132+
description = None
118133
origin = None
119-
if span.attributes is None:
120-
return (op, description, status, http_status, origin)
121134

122-
attribute_op = get_typed_attribute(span.attributes, SentrySpanAttribute.OP, str)
123-
op = attribute_op or op
124-
description = (
125-
get_typed_attribute(span.attributes, SentrySpanAttribute.DESCRIPTION, str)
126-
or description
127-
)
128-
origin = get_typed_attribute(span.attributes, SentrySpanAttribute.ORIGIN, str)
129-
130-
http_method = get_typed_attribute(span.attributes, SpanAttributes.HTTP_METHOD, str)
131-
if http_method:
132-
return span_data_for_http_method(span)
133-
134-
db_query = span.attributes.get(SpanAttributes.DB_SYSTEM)
135-
if db_query:
136-
return span_data_for_db_query(span)
137-
138-
rpc_service = span.attributes.get(SpanAttributes.RPC_SERVICE)
139-
if rpc_service:
140-
return (
141-
attribute_op or "rpc",
142-
description,
143-
status,
144-
http_status,
145-
origin,
146-
)
135+
if span.attributes is not None:
136+
op = get_typed_attribute(
137+
span.attributes, SentrySpanAttribute.OP, str
138+
) or infer_op(span)
147139

148-
messaging_system = span.attributes.get(SpanAttributes.MESSAGING_SYSTEM)
149-
if messaging_system:
150-
return (
151-
attribute_op or "message",
152-
description,
153-
status,
154-
http_status,
155-
origin,
140+
description = (
141+
get_typed_attribute(span.attributes, SentrySpanAttribute.DESCRIPTION, str)
142+
or get_typed_attribute(span.attributes, SentrySpanAttribute.NAME, str)
143+
or infer_description(span)
156144
)
157145

158-
faas_trigger = span.attributes.get(SpanAttributes.FAAS_TRIGGER)
159-
if faas_trigger:
160-
return (str(faas_trigger), description, status, http_status, origin)
146+
origin = get_typed_attribute(span.attributes, SentrySpanAttribute.ORIGIN, str)
161147

162-
return (op, description, status, http_status, origin)
148+
# TODO status cleanup
149+
(status, http_status) = extract_span_status(span)
163150

151+
return ExtractedSpanData(
152+
description=description or span.name,
153+
op=op,
154+
status=status,
155+
http_status=http_status,
156+
origin=origin,
157+
)
164158

165-
def span_data_for_http_method(span: ReadableSpan) -> OtelExtractedSpanData:
166-
span_attributes = span.attributes or {}
167159

168-
op = get_typed_attribute(span_attributes, SentrySpanAttribute.OP, str)
169-
if op is None:
170-
op = "http"
160+
def infer_op(span: ReadableSpan) -> Optional[str]:
161+
"""
162+
Try to infer op for the various types of instrumentation.
163+
"""
164+
if span.attributes is None:
165+
return None
171166

167+
if SpanAttributes.HTTP_METHOD in span.attributes:
168+
op = "http"
172169
if span.kind == SpanKind.SERVER:
173170
op += ".server"
174171
elif span.kind == SpanKind.CLIENT:
175172
op += ".client"
173+
return op
174+
elif SpanAttributes.DB_SYSTEM in span.attributes:
175+
return OP.DB
176+
elif SpanAttributes.RPC_SERVICE in span.attributes:
177+
return OP.RPC
178+
elif SpanAttributes.MESSAGING_SYSTEM in span.attributes:
179+
return OP.MESSAGE
180+
elif SpanAttributes.FAAS_TRIGGER in span.attributes:
181+
return get_typed_attribute(span.attributes, SpanAttributes.FAAS_TRIGGER, str)
182+
else:
183+
return None
176184

177-
http_method = span_attributes.get(SpanAttributes.HTTP_METHOD)
178-
route = span_attributes.get(SpanAttributes.HTTP_ROUTE)
179-
target = span_attributes.get(SpanAttributes.HTTP_TARGET)
180-
peer_name = span_attributes.get(SpanAttributes.NET_PEER_NAME)
181185

182-
# TODO-neel-potel remove description completely
183-
description = get_typed_attribute(
184-
span_attributes, SentrySpanAttribute.DESCRIPTION, str
185-
) or get_typed_attribute(span_attributes, SentrySpanAttribute.NAME, str)
186-
if description is None:
187-
description = f"{http_method}"
186+
def infer_description(span: ReadableSpan) -> Optional[str]:
187+
if span.attributes is None:
188+
return None
189+
190+
if SpanAttributes.HTTP_METHOD in span.attributes:
191+
http_method = get_typed_attribute(
192+
span.attributes, SpanAttributes.HTTP_METHOD, str
193+
)
194+
route = get_typed_attribute(span.attributes, SpanAttributes.HTTP_ROUTE, str)
195+
target = get_typed_attribute(span.attributes, SpanAttributes.HTTP_TARGET, str)
196+
peer_name = get_typed_attribute(
197+
span.attributes, SpanAttributes.NET_PEER_NAME, str
198+
)
199+
url = get_typed_attribute(span.attributes, SpanAttributes.HTTP_URL, str)
188200

189201
if route:
190-
description = f"{http_method} {route}"
202+
return f"{http_method} {route}"
191203
elif target:
192-
description = f"{http_method} {target}"
204+
return f"{http_method} {target}"
193205
elif peer_name:
194-
description = f"{http_method} {peer_name}"
206+
return f"{http_method} {peer_name}"
207+
elif url:
208+
parsed_url = urlparse(url)
209+
url = "{}://{}{}".format(
210+
parsed_url.scheme, parsed_url.netloc, parsed_url.path
211+
)
212+
return f"{http_method} {url}"
195213
else:
196-
url = span_attributes.get(SpanAttributes.HTTP_URL)
197-
url = get_typed_attribute(span_attributes, SpanAttributes.HTTP_URL, str)
198-
199-
if url:
200-
parsed_url = urlparse(url)
201-
url = "{}://{}{}".format(
202-
parsed_url.scheme, parsed_url.netloc, parsed_url.path
203-
)
204-
description = f"{http_method} {url}"
205-
206-
status, http_status = extract_span_status(span)
207-
208-
origin = get_typed_attribute(span_attributes, SentrySpanAttribute.ORIGIN, str)
209-
210-
return (op, description, status, http_status, origin)
211-
212-
213-
def span_data_for_db_query(span: ReadableSpan) -> OtelExtractedSpanData:
214-
span_attributes = span.attributes or {}
215-
216-
op = get_typed_attribute(span_attributes, SentrySpanAttribute.OP, str) or OP.DB
217-
statement = get_typed_attribute(span_attributes, SpanAttributes.DB_STATEMENT, str)
218-
219-
description = statement or span.name
220-
origin = get_typed_attribute(span_attributes, SentrySpanAttribute.ORIGIN, str)
221-
222-
return (op, description, None, None, origin)
214+
return http_method
215+
elif SpanAttributes.DB_SYSTEM in span.attributes:
216+
return get_typed_attribute(span.attributes, SpanAttributes.DB_STATEMENT, str)
217+
else:
218+
return None
223219

224220

225221
def extract_span_status(span: ReadableSpan) -> tuple[Optional[str], Optional[int]]:
@@ -258,17 +254,7 @@ def extract_span_status(span: ReadableSpan) -> tuple[Optional[str], Optional[int
258254

259255

260256
def infer_status_from_attributes(
261-
span_attributes: Mapping[
262-
str,
263-
str
264-
| bool
265-
| int
266-
| float
267-
| Sequence[str]
268-
| Sequence[bool]
269-
| Sequence[int]
270-
| Sequence[float],
271-
],
257+
span_attributes: Mapping[str, Any],
272258
) -> tuple[Optional[str], Optional[int]]:
273259
http_status = get_http_status_code(span_attributes)
274260

@@ -282,19 +268,7 @@ def infer_status_from_attributes(
282268
return (None, None)
283269

284270

285-
def get_http_status_code(
286-
span_attributes: Mapping[
287-
str,
288-
str
289-
| bool
290-
| int
291-
| float
292-
| Sequence[str]
293-
| Sequence[bool]
294-
| Sequence[int]
295-
| Sequence[float],
296-
],
297-
) -> Optional[int]:
271+
def get_http_status_code(span_attributes: Mapping[str, Any]) -> Optional[int]:
298272
try:
299273
http_status = get_typed_attribute(
300274
span_attributes, SpanAttributes.HTTP_RESPONSE_STATUS_CODE, int
@@ -329,7 +303,7 @@ def extract_span_attributes(span: ReadableSpan, namespace: str) -> dict[str, Any
329303

330304

331305
def get_trace_context(
332-
span: ReadableSpan, span_data: Optional[OtelExtractedSpanData] = None
306+
span: ReadableSpan, span_data: Optional[ExtractedSpanData] = None
333307
) -> dict[str, Any]:
334308
if not span.context:
335309
return {}
@@ -341,27 +315,23 @@ def get_trace_context(
341315
if span_data is None:
342316
span_data = extract_span_data(span)
343317

344-
(op, _, status, _, origin) = span_data
345-
346318
trace_context: dict[str, Any] = {
347319
"trace_id": trace_id,
348320
"span_id": span_id,
349321
"parent_span_id": parent_span_id,
350-
"op": op,
351-
"origin": origin or DEFAULT_SPAN_ORIGIN,
322+
"origin": span_data.origin or DEFAULT_SPAN_ORIGIN,
352323
}
353324

354-
if status:
355-
trace_context["status"] = status
356-
325+
if span_data.op:
326+
trace_context["op"] = span_data.op
327+
if span_data.status:
328+
trace_context["status"] = span_data.status
357329
if span.attributes:
358330
trace_context["data"] = dict(span.attributes)
359331

360332
trace_state = get_trace_state(span)
361333
trace_context["dynamic_sampling_context"] = dsc_from_trace_state(trace_state)
362334

363-
# TODO-neel-potel profiler thread_id, thread_name
364-
365335
return trace_context
366336

367337

sentry_sdk/tracing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def __init__(
187187
# OTel timestamps have nanosecond precision
188188
start_timestamp = convert_to_otel_timestamp(start_timestamp)
189189

190-
span_name = name or description or op or DEFAULT_SPAN_NAME
190+
span_name = name or description or DEFAULT_SPAN_NAME
191191

192192
# Prepopulate some attrs so that they're accessible in traces_sampler
193193
attributes = attributes or {}

tests/integrations/clickhouse_driver/test_clickhouse_driver.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Tests need a local clickhouse instance running, this can best be done using
33
```sh
4-
docker run -d -p 18123:8123 -p9000:9000 --name clickhouse-test --ulimit nofile=262144:262144 --rm clickhouse
4+
docker run -d -e CLICKHOUSE_SKIP_USER_SETUP=1 -p 8123:8123 -p 9000:9000 --name clickhouse-test --ulimit nofile=262144:262144 --rm clickhouse
55
```
66
"""
77

@@ -822,7 +822,7 @@ def test_clickhouse_dbapi_spans(sentry_init, capture_events, capture_envelopes)
822822
span.pop("span_id", None)
823823
span.pop("start_timestamp", None)
824824
span.pop("timestamp", None)
825-
span.pop("status")
825+
span.pop("status", None)
826826

827827
assert event["spans"] == expected_spans
828828

0 commit comments

Comments
 (0)