11from __future__ import annotations
22import re
33from datetime import datetime , timezone
4+ from dataclasses import dataclass
45
56from urllib3 .util import parse_url as urlparse
67from urllib .parse import quote , unquote
3031from sentry_sdk ._types import TYPE_CHECKING
3132
3233if 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
225221def 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
260256def 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
331305def 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
0 commit comments