Skip to content

Commit 56ef079

Browse files
committed
Add root_span implementation
* add and track underlying root span in a hidden attr `_sentry_root_otel_span` in all subsequent children spans in the span processor on start * wrap this underlying root otel span in a `POTelSpan` to act as a proxy * make `POTelSpan` constructor work with explicitly passed in `otel_span` * implement `__eq__` on `POTelSpan` to make sure proxies to the same underlying `_otel_span` are considered the same
1 parent b8e687e commit 56ef079

File tree

3 files changed

+72
-26
lines changed

3 files changed

+72
-26
lines changed

sentry_sdk/integrations/opentelemetry/potel_span_processor.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
from collections import deque, defaultdict
22
from typing import cast
33

4-
from opentelemetry.trace import format_trace_id, format_span_id
4+
from opentelemetry.trace import (
5+
format_trace_id,
6+
format_span_id,
7+
get_current_span,
8+
INVALID_SPAN,
9+
Span as TraceApiSpan,
10+
)
511
from opentelemetry.context import Context
612
from opentelemetry.sdk.trace import Span, ReadableSpan, SpanProcessor
713

@@ -44,7 +50,8 @@ def __init__(self):
4450

4551
def on_start(self, span, parent_context=None):
4652
# type: (Span, Optional[Context]) -> None
47-
pass
53+
if not is_sentry_span(span):
54+
self._add_root_span(span, get_current_span(parent_context))
4855

4956
def on_end(self, span):
5057
# type: (ReadableSpan) -> None
@@ -68,6 +75,21 @@ def force_flush(self, timeout_millis=30000):
6875
# type: (int) -> bool
6976
return True
7077

78+
def _add_root_span(self, span, parent_span):
79+
# type: (Span, TraceApiSpan) -> None
80+
"""
81+
This is required to make POTelSpan.root_span work
82+
since we can't traverse back to the root purely with otel efficiently.
83+
"""
84+
if parent_span != INVALID_SPAN and not parent_span.get_span_context().is_remote:
85+
# child span points to parent's root or parent
86+
span._sentry_root_otel_span = getattr(
87+
parent_span, "_sentry_root_otel_span", parent_span
88+
)
89+
else:
90+
# root span points to itself
91+
span._sentry_root_otel_span = span
92+
7193
def _flush_root_span(self, span):
7294
# type: (ReadableSpan) -> None
7395
transaction_event = self._root_span_to_transaction_event(span)

sentry_sdk/tracing.py

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import datetime, timedelta, timezone
44

55
from opentelemetry import trace as otel_trace, context
6-
from opentelemetry.trace import format_trace_id, format_span_id
6+
from opentelemetry.trace import format_trace_id, format_span_id, Span as OtelSpan
77
from opentelemetry.trace.status import StatusCode
88
from opentelemetry.sdk.trace import ReadableSpan
99

@@ -17,7 +17,7 @@
1717
nanosecond_time,
1818
)
1919

20-
from typing import TYPE_CHECKING
20+
from typing import TYPE_CHECKING, cast
2121

2222
if TYPE_CHECKING:
2323
from collections.abc import Callable, Mapping, MutableMapping
@@ -1201,32 +1201,42 @@ def __init__(
12011201
start_timestamp=None, # type: Optional[Union[datetime, float]]
12021202
origin=None, # type: Optional[str]
12031203
name=None, # type: Optional[str]
1204+
otel_span=None, # type: Optional[OtelSpan]
12041205
**_, # type: dict[str, object]
12051206
):
12061207
# type: (...) -> None
12071208
"""
12081209
For backwards compatibility with old the old Span interface, this class
12091210
accepts arbitrary keyword arguments, in addition to the ones explicitly
12101211
listed in the signature. These additional arguments are ignored.
1212+
1213+
If otel_span is passed explicitly, just acts as a proxy.
12111214
"""
1212-
from sentry_sdk.integrations.opentelemetry.utils import (
1213-
convert_to_otel_timestamp,
1214-
)
1215+
if otel_span is not None:
1216+
self._otel_span = otel_span
1217+
else:
1218+
from sentry_sdk.integrations.opentelemetry.utils import (
1219+
convert_to_otel_timestamp,
1220+
)
12151221

1216-
if start_timestamp is not None:
1217-
# OTel timestamps have nanosecond precision
1218-
start_timestamp = convert_to_otel_timestamp(start_timestamp)
1222+
if start_timestamp is not None:
1223+
# OTel timestamps have nanosecond precision
1224+
start_timestamp = convert_to_otel_timestamp(start_timestamp)
12191225

1220-
self._otel_span = tracer.start_span(
1221-
description or op or "", start_time=start_timestamp
1222-
)
1226+
self._otel_span = tracer.start_span(
1227+
description or op or "", start_time=start_timestamp
1228+
)
12231229

1224-
self.origin = origin or DEFAULT_SPAN_ORIGIN
1225-
self.op = op
1226-
self.description = description
1227-
self.name = name
1228-
if status is not None:
1229-
self.set_status(status)
1230+
self.origin = origin or DEFAULT_SPAN_ORIGIN
1231+
self.op = op
1232+
self.description = description
1233+
self.name = name
1234+
if status is not None:
1235+
self.set_status(status)
1236+
1237+
def __eq__(self, other):
1238+
# type: (POTelSpan) -> bool
1239+
return self._otel_span == other._otel_span
12301240

12311241
def __repr__(self):
12321242
# type: () -> str
@@ -1318,17 +1328,16 @@ def containing_transaction(self):
13181328
@property
13191329
def root_span(self):
13201330
# type: () -> Optional[POTelSpan]
1321-
# XXX implement this
1322-
# there's a span.parent property, but it returns the parent spancontext
1323-
# not sure if there's a way to retrieve the parent with pure otel.
1324-
return None
1331+
root_otel_span = cast(
1332+
"Optional[OtelSpan]",
1333+
getattr(self._otel_span, "_sentry_root_otel_span", None),
1334+
)
1335+
return POTelSpan(otel_span=root_otel_span) if root_otel_span else None
13251336

13261337
@property
13271338
def is_root_span(self):
13281339
# type: () -> bool
1329-
return (
1330-
isinstance(self._otel_span, ReadableSpan) and self._otel_span.parent is None
1331-
)
1340+
return self.root_span == self
13321341

13331342
@property
13341343
def parent_span_id(self):

tests/integrations/opentelemetry/test_potel.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,3 +314,18 @@ def test_multiple_transaction_tags_isolation_scope_started_with_sentry(
314314

315315
assert payload_a["tags"] == {"tag.global": 99, "tag.inner.a": "a"}
316316
assert payload_b["tags"] == {"tag.global": 99, "tag.inner.b": "b"}
317+
318+
319+
def test_potel_span_root_span_references():
320+
with sentry_sdk.start_span(description="request") as request_span:
321+
assert request_span.is_root_span
322+
assert request_span.root_span == request_span
323+
with sentry_sdk.start_span(description="db") as db_span:
324+
assert not db_span.is_root_span
325+
assert db_span.root_span == request_span
326+
with sentry_sdk.start_span(description="redis") as redis_span:
327+
assert not redis_span.is_root_span
328+
assert redis_span.root_span == request_span
329+
with sentry_sdk.start_span(description="http") as http_span:
330+
assert not http_span.is_root_span
331+
assert http_span.root_span == request_span

0 commit comments

Comments
 (0)