Skip to content

Commit 4428ee9

Browse files
sl0thentr0pyszokeasaurusrex
authored andcommitted
Implement new POTel span processor (#3223)
* only acts on `on_end` instead of both `on_start/on_end` as before * store children spans in a dict mapping `span_id -> children` * new dict only stores otel span objects and no sentry transaction/span objects so we save a bit of useless memory allocation * I'm not using our current `Transaction/Span` classes at all to build the event because when we add our APIs later, we'll need to rip these out and we also avoid having to deal with the `instrumenter` problem * if we get a root span (without parent), we recursively walk the dict and find the children and package up the transaction event and send it * I didn't do it like JS because I think this way is better * they [group an array of `finished_spans`](https://github.com/getsentry/sentry-javascript/blob/7e298036a21a5658f3eb9ba184165178c48d7ef8/packages/opentelemetry/src/spanExporter.ts#L132) every time a root span ends and I think this uses more cpu than what I did * and the dict like I used it doesn't take more space than the array either * if we get a span with a parent we just update the dict to find the span later * moved the common `is_sentry_span` logic to utils
1 parent b6a6231 commit 4428ee9

File tree

5 files changed

+270
-114
lines changed

5 files changed

+270
-114
lines changed

sentry_sdk/integrations/opentelemetry/consts.py

+2
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33

44
SENTRY_TRACE_KEY = create_key("sentry-trace")
55
SENTRY_BAGGAGE_KEY = create_key("sentry-baggage")
6+
OTEL_SENTRY_CONTEXT = "otel"
7+
SPAN_ORIGIN = "auto.otel"

sentry_sdk/integrations/opentelemetry/potel_span_processor.py

+143-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
1-
from opentelemetry.sdk.trace import SpanProcessor
1+
from collections import deque, defaultdict
2+
3+
from opentelemetry.trace import format_trace_id, format_span_id
24
from opentelemetry.context import Context
5+
from opentelemetry.sdk.trace import Span, ReadableSpan, SpanProcessor
36

7+
from sentry_sdk import capture_event
8+
from sentry_sdk.integrations.opentelemetry.utils import (
9+
is_sentry_span,
10+
convert_otel_timestamp,
11+
extract_span_data,
12+
)
13+
from sentry_sdk.integrations.opentelemetry.consts import (
14+
OTEL_SENTRY_CONTEXT,
15+
SPAN_ORIGIN,
16+
)
417
from sentry_sdk._types import TYPE_CHECKING
518

619
if TYPE_CHECKING:
7-
from typing import Optional
8-
from opentelemetry.sdk.trace import ReadableSpan
20+
from typing import Optional, List, Any, Deque, DefaultDict
21+
from sentry_sdk._types import Event
922

1023

1124
class PotelSentrySpanProcessor(SpanProcessor):
@@ -22,15 +35,25 @@ def __new__(cls):
2235

2336
def __init__(self):
2437
# type: () -> None
25-
pass
38+
self._children_spans = defaultdict(
39+
list
40+
) # type: DefaultDict[int, List[ReadableSpan]]
2641

2742
def on_start(self, span, parent_context=None):
28-
# type: (ReadableSpan, Optional[Context]) -> None
43+
# type: (Span, Optional[Context]) -> None
2944
pass
3045

3146
def on_end(self, span):
3247
# type: (ReadableSpan) -> None
33-
pass
48+
if is_sentry_span(span):
49+
return
50+
51+
# TODO-neel-potel-remote only take parent if not remote
52+
if span.parent:
53+
self._children_spans[span.parent.span_id].append(span)
54+
else:
55+
# if have a root span ending, we build a transaction and send it
56+
self._flush_root_span(span)
3457

3558
# TODO-neel-potel not sure we need a clear like JS
3659
def shutdown(self):
@@ -42,3 +65,117 @@ def shutdown(self):
4265
def force_flush(self, timeout_millis=30000):
4366
# type: (int) -> bool
4467
return True
68+
69+
def _flush_root_span(self, span):
70+
# type: (ReadableSpan) -> None
71+
transaction_event = self._root_span_to_transaction_event(span)
72+
if not transaction_event:
73+
return
74+
75+
spans = []
76+
for child in self._collect_children(span):
77+
span_json = self._span_to_json(child)
78+
if span_json:
79+
spans.append(span_json)
80+
transaction_event["spans"] = spans
81+
# TODO-neel-potel sort and cutoff max spans
82+
83+
capture_event(transaction_event)
84+
85+
def _collect_children(self, span):
86+
# type: (ReadableSpan) -> List[ReadableSpan]
87+
if not span.context:
88+
return []
89+
90+
children = []
91+
bfs_queue = deque() # type: Deque[int]
92+
bfs_queue.append(span.context.span_id)
93+
94+
while bfs_queue:
95+
parent_span_id = bfs_queue.popleft()
96+
node_children = self._children_spans.pop(parent_span_id, [])
97+
children.extend(node_children)
98+
bfs_queue.extend(
99+
[child.context.span_id for child in node_children if child.context]
100+
)
101+
102+
return children
103+
104+
# we construct the event from scratch here
105+
# and not use the current Transaction class for easier refactoring
106+
def _root_span_to_transaction_event(self, span):
107+
# type: (ReadableSpan) -> Optional[Event]
108+
if not span.context:
109+
return None
110+
if not span.start_time:
111+
return None
112+
if not span.end_time:
113+
return None
114+
115+
trace_id = format_trace_id(span.context.trace_id)
116+
span_id = format_span_id(span.context.span_id)
117+
parent_span_id = format_span_id(span.parent.span_id) if span.parent else None
118+
119+
(op, description, _) = extract_span_data(span)
120+
121+
trace_context = {
122+
"trace_id": trace_id,
123+
"span_id": span_id,
124+
"origin": SPAN_ORIGIN,
125+
"op": op,
126+
"status": "ok", # TODO-neel-potel span status mapping
127+
} # type: dict[str, Any]
128+
129+
if parent_span_id:
130+
trace_context["parent_span_id"] = parent_span_id
131+
if span.attributes:
132+
trace_context["data"] = dict(span.attributes)
133+
134+
contexts = {"trace": trace_context}
135+
if span.resource.attributes:
136+
contexts[OTEL_SENTRY_CONTEXT] = {"resource": dict(span.resource.attributes)}
137+
138+
event = {
139+
"type": "transaction",
140+
"transaction": description,
141+
# TODO-neel-potel tx source based on integration
142+
"transaction_info": {"source": "custom"},
143+
"contexts": contexts,
144+
"start_timestamp": convert_otel_timestamp(span.start_time),
145+
"timestamp": convert_otel_timestamp(span.end_time),
146+
} # type: Event
147+
148+
return event
149+
150+
def _span_to_json(self, span):
151+
# type: (ReadableSpan) -> Optional[dict[str, Any]]
152+
if not span.context:
153+
return None
154+
if not span.start_time:
155+
return None
156+
if not span.end_time:
157+
return None
158+
159+
trace_id = format_trace_id(span.context.trace_id)
160+
span_id = format_span_id(span.context.span_id)
161+
parent_span_id = format_span_id(span.parent.span_id) if span.parent else None
162+
163+
(op, description, _) = extract_span_data(span)
164+
165+
span_json = {
166+
"trace_id": trace_id,
167+
"span_id": span_id,
168+
"origin": SPAN_ORIGIN,
169+
"op": op,
170+
"description": description,
171+
"status": "ok", # TODO-neel-potel span status mapping
172+
"start_timestamp": convert_otel_timestamp(span.start_time),
173+
"timestamp": convert_otel_timestamp(span.end_time),
174+
} # type: dict[str, Any]
175+
176+
if parent_span_id:
177+
span_json["parent_span_id"] = parent_span_id
178+
if span.attributes:
179+
span_json["data"] = dict(span.attributes)
180+
181+
return span_json

sentry_sdk/integrations/opentelemetry/span_processor.py

+15-103
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@
44

55
from opentelemetry.context import get_value
66
from opentelemetry.sdk.trace import SpanProcessor, ReadableSpan as OTelSpan
7-
from opentelemetry.semconv.trace import SpanAttributes
87
from opentelemetry.trace import (
98
format_span_id,
109
format_trace_id,
1110
get_current_span,
12-
SpanKind,
1311
)
1412
from opentelemetry.trace.span import (
1513
INVALID_SPAN_ID,
@@ -20,22 +18,24 @@
2018
from sentry_sdk.integrations.opentelemetry.consts import (
2119
SENTRY_BAGGAGE_KEY,
2220
SENTRY_TRACE_KEY,
21+
OTEL_SENTRY_CONTEXT,
22+
SPAN_ORIGIN,
23+
)
24+
from sentry_sdk.integrations.opentelemetry.utils import (
25+
is_sentry_span,
26+
extract_span_data,
2327
)
2428
from sentry_sdk.scope import add_global_event_processor
2529
from sentry_sdk.tracing import Transaction, Span as SentrySpan
26-
from sentry_sdk.utils import Dsn
2730
from sentry_sdk._types import TYPE_CHECKING
2831

29-
from urllib3.util import parse_url as urlparse
3032

3133
if TYPE_CHECKING:
3234
from typing import Any, Optional, Union
3335
from opentelemetry import context as context_api
3436
from sentry_sdk._types import Event, Hint
3537

36-
OPEN_TELEMETRY_CONTEXT = "otel"
3738
SPAN_MAX_TIME_OPEN_MINUTES = 10
38-
SPAN_ORIGIN = "auto.otel"
3939

4040

4141
def link_trace_context_to_error_event(event, otel_span_map):
@@ -117,18 +117,13 @@ def on_start(self, otel_span, parent_context=None):
117117
if not client.dsn:
118118
return
119119

120-
try:
121-
_ = Dsn(client.dsn)
122-
except Exception:
123-
return
124-
125120
if client.options["instrumenter"] != INSTRUMENTER.OTEL:
126121
return
127122

128123
if not otel_span.get_span_context().is_valid:
129124
return
130125

131-
if self._is_sentry_span(otel_span):
126+
if is_sentry_span(otel_span):
132127
return
133128

134129
trace_data = self._get_trace_data(otel_span, parent_context)
@@ -200,7 +195,7 @@ def on_end(self, otel_span):
200195
if isinstance(sentry_span, Transaction):
201196
sentry_span.name = otel_span.name
202197
sentry_span.set_context(
203-
OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span)
198+
OTEL_SENTRY_CONTEXT, self._get_otel_context(otel_span)
204199
)
205200
self._update_transaction_with_otel_data(sentry_span, otel_span)
206201

@@ -223,27 +218,6 @@ def on_end(self, otel_span):
223218

224219
self._prune_old_spans()
225220

226-
def _is_sentry_span(self, otel_span):
227-
# type: (OTelSpan) -> bool
228-
"""
229-
Break infinite loop:
230-
HTTP requests to Sentry are caught by OTel and send again to Sentry.
231-
"""
232-
otel_span_url = None
233-
if otel_span.attributes is not None:
234-
otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL)
235-
otel_span_url = cast("Optional[str]", otel_span_url)
236-
237-
dsn_url = None
238-
client = get_client()
239-
if client.dsn:
240-
dsn_url = Dsn(client.dsn).netloc
241-
242-
if otel_span_url and dsn_url and dsn_url in otel_span_url:
243-
return True
244-
245-
return False
246-
247221
def _get_otel_context(self, otel_span):
248222
# type: (OTelSpan) -> dict[str, Any]
249223
"""
@@ -312,81 +286,19 @@ def _update_span_with_otel_data(self, sentry_span, otel_span):
312286
"""
313287
sentry_span.set_data("otel.kind", otel_span.kind)
314288

315-
op = otel_span.name
316-
description = otel_span.name
317-
318289
if otel_span.attributes is not None:
319290
for key, val in otel_span.attributes.items():
320291
sentry_span.set_data(key, val)
321292

322-
http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD)
323-
http_method = cast("Optional[str]", http_method)
324-
325-
db_query = otel_span.attributes.get(SpanAttributes.DB_SYSTEM)
326-
327-
if http_method:
328-
op = "http"
329-
330-
if otel_span.kind == SpanKind.SERVER:
331-
op += ".server"
332-
elif otel_span.kind == SpanKind.CLIENT:
333-
op += ".client"
334-
335-
description = http_method
336-
337-
peer_name = otel_span.attributes.get(SpanAttributes.NET_PEER_NAME, None)
338-
if peer_name:
339-
description += " {}".format(peer_name)
340-
341-
target = otel_span.attributes.get(SpanAttributes.HTTP_TARGET, None)
342-
if target:
343-
description += " {}".format(target)
344-
345-
if not peer_name and not target:
346-
url = otel_span.attributes.get(SpanAttributes.HTTP_URL, None)
347-
url = cast("Optional[str]", url)
348-
if url:
349-
parsed_url = urlparse(url)
350-
url = "{}://{}{}".format(
351-
parsed_url.scheme, parsed_url.netloc, parsed_url.path
352-
)
353-
description += " {}".format(url)
354-
355-
status_code = otel_span.attributes.get(
356-
SpanAttributes.HTTP_STATUS_CODE, None
357-
)
358-
status_code = cast("Optional[int]", status_code)
359-
if status_code:
360-
sentry_span.set_http_status(status_code)
361-
362-
elif db_query:
363-
op = "db"
364-
statement = otel_span.attributes.get(SpanAttributes.DB_STATEMENT, None)
365-
statement = cast("Optional[str]", statement)
366-
if statement:
367-
description = statement
368-
293+
(op, description, status_code) = extract_span_data(otel_span)
369294
sentry_span.op = op
370295
sentry_span.description = description
296+
if status_code:
297+
sentry_span.set_http_status(status_code)
371298

372299
def _update_transaction_with_otel_data(self, sentry_span, otel_span):
373300
# type: (SentrySpan, OTelSpan) -> None
374-
if otel_span.attributes is None:
375-
return
376-
377-
http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD)
378-
379-
if http_method:
380-
status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE)
381-
status_code = cast("Optional[int]", status_code)
382-
if status_code:
383-
sentry_span.set_http_status(status_code)
384-
385-
op = "http"
386-
387-
if otel_span.kind == SpanKind.SERVER:
388-
op += ".server"
389-
elif otel_span.kind == SpanKind.CLIENT:
390-
op += ".client"
391-
392-
sentry_span.op = op
301+
(op, _, status_code) = extract_span_data(otel_span)
302+
sentry_span.op = op
303+
if status_code:
304+
sentry_span.set_http_status(status_code)

0 commit comments

Comments
 (0)