Skip to content

Commit 5d04d3d

Browse files
committed
Merge branch 'neel/potel/initial-scope-management' into neel/potel/span-processor
2 parents e7a20f2 + 436626b commit 5d04d3d

File tree

5 files changed

+115
-46
lines changed

5 files changed

+115
-46
lines changed

sentry_sdk/integrations/opentelemetry/consts.py

Lines changed: 2 additions & 0 deletions
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_exporter.py

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
from opentelemetry.trace import INVALID_SPAN, get_current_span # type: ignore
2-
from opentelemetry.context import Context # type: ignore
3-
from opentelemetry.sdk.trace import Span, ReadableSpan, SpanProcessor # type: ignore
1+
from collections import deque
2+
from datetime import datetime
43

5-
from sentry_sdk.integrations.opentelemetry.utils import is_sentry_span
6-
from sentry_sdk.integrations.opentelemetry.potel_span_exporter import (
7-
PotelSentrySpanExporter,
8-
)
4+
from opentelemetry.trace import INVALID_SPAN, get_current_span, format_trace_id, format_span_id
5+
from opentelemetry.context import Context
6+
from opentelemetry.sdk.trace import Span, ReadableSpan, SpanProcessor
97

8+
from sentry_sdk import capture_event
9+
from sentry_sdk.integrations.opentelemetry.utils import is_sentry_span, convert_otel_timestamp
10+
from sentry_sdk.integrations.opentelemetry.consts import OTEL_SENTRY_CONTEXT, SPAN_ORIGIN
1011
from sentry_sdk._types import TYPE_CHECKING
1112

1213
if TYPE_CHECKING:
13-
from typing import Optional
14+
from typing import Optional, List, Any
15+
from sentry_sdk._types import Event
1416

1517

1618
class PotelSentrySpanProcessor(SpanProcessor): # type: ignore
@@ -27,25 +29,23 @@ def __new__(cls):
2729

2830
def __init__(self):
2931
# type: () -> None
30-
self._exporter = PotelSentrySpanExporter()
32+
self._children_spans = {} # type: dict[int, List[ReadableSpan]]
3133

3234
def on_start(self, span, parent_context=None):
3335
# type: (Span, Optional[Context]) -> None
3436
pass
35-
# if is_sentry_span(span):
36-
# return
37-
38-
# parent_span = get_current_span(parent_context)
39-
40-
# # TODO-neel-potel check remote logic with propagation and incoming trace later
41-
# if parent_span != INVALID_SPAN:
42-
# # TODO-neel once we add our apis, we might need to store references on the span
43-
# # directly, see if we need to do this like JS
44-
# pass
4537

4638
def on_end(self, span):
4739
# type: (ReadableSpan) -> None
48-
self._exporter.export(span)
40+
if is_sentry_span(span):
41+
return
42+
43+
# TODO-neel-potel-remote only take parent if not remote
44+
if span.parent:
45+
self._children_spans.setdefault(span.parent.span_id, []).append(span)
46+
else:
47+
# if have a root span ending, we build a transaction and send it
48+
self._flush_root_span(span)
4949

5050
# TODO-neel-potel not sure we need a clear like JS
5151
def shutdown(self):
@@ -56,4 +56,77 @@ def shutdown(self):
5656
# TODO-neel-potel call this in client.flush
5757
def force_flush(self, timeout_millis=30000):
5858
# type: (int) -> bool
59-
return self._exporter.flush(timeout_millis)
59+
return True
60+
61+
def _flush_root_span(self, span):
62+
# type: (ReadableSpan) -> None
63+
transaction_event = self._root_span_to_transaction_event(span)
64+
if not transaction_event:
65+
return
66+
67+
children = self._collect_children(span)
68+
# TODO add converted spans
69+
capture_event(transaction_event)
70+
71+
72+
def _collect_children(self, span):
73+
# type: (ReadableSpan) -> List[ReadableSpan]
74+
if not span.context:
75+
return []
76+
77+
children = []
78+
bfs_queue = deque()
79+
bfs_queue.append(span.context.span_id)
80+
81+
while bfs_queue:
82+
parent_span_id = bfs_queue.popleft()
83+
node_children = self._children_spans.pop(parent_span_id, [])
84+
children.extend(node_children)
85+
bfs_queue.extend([child.context.span_id for child in node_children if child.context])
86+
87+
return children
88+
89+
# we construct the event from scratch here
90+
# and not use the current Transaction class for easier refactoring
91+
# TODO-neel-potel op, description, status logic
92+
def _root_span_to_transaction_event(self, span):
93+
# type: (ReadableSpan) -> Optional[Event]
94+
if not span.context:
95+
return None
96+
if not span.start_time:
97+
return None
98+
if not span.end_time:
99+
return None
100+
101+
trace_id = format_trace_id(span.context.trace_id)
102+
span_id = format_span_id(span.context.span_id)
103+
parent_span_id = format_span_id(span.parent.span_id) if span.parent else None
104+
105+
trace_context = {
106+
"trace_id": trace_id,
107+
"span_id": span_id,
108+
"origin": SPAN_ORIGIN,
109+
"op": span.name, # TODO
110+
"status": "ok", # TODO
111+
} # type: dict[str, Any]
112+
113+
if parent_span_id:
114+
trace_context["parent_span_id"] = parent_span_id
115+
if span.attributes:
116+
trace_context["data"] = dict(span.attributes)
117+
118+
contexts = {"trace": trace_context}
119+
if span.resource.attributes:
120+
contexts[OTEL_SENTRY_CONTEXT] = {"resource": dict(span.resource.attributes)}
121+
122+
event = {
123+
"type": "transaction",
124+
"transaction": span.name, # TODO
125+
"transaction_info": {"source": "custom"}, # TODO
126+
"contexts": contexts,
127+
"start_timestamp": convert_otel_timestamp(span.start_time),
128+
"timestamp": convert_otel_timestamp(span.end_time),
129+
"spans": [],
130+
} # type: Event
131+
132+
return event

sentry_sdk/integrations/opentelemetry/span_processor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from sentry_sdk.integrations.opentelemetry.consts import (
2121
SENTRY_BAGGAGE_KEY,
2222
SENTRY_TRACE_KEY,
23+
OTEL_SENTRY_CONTEXT,
24+
SPAN_ORIGIN,
2325
)
2426
from sentry_sdk.integrations.opentelemetry.utils import is_sentry_span
2527
from sentry_sdk.scope import add_global_event_processor
@@ -33,9 +35,7 @@
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):
@@ -195,7 +195,7 @@ def on_end(self, otel_span):
195195
if isinstance(sentry_span, Transaction):
196196
sentry_span.name = otel_span.name
197197
sentry_span.set_context(
198-
OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span)
198+
OTEL_SENTRY_CONTEXT, self._get_otel_context(otel_span)
199199
)
200200
self._update_transaction_with_otel_data(sentry_span, otel_span)
201201

sentry_sdk/integrations/opentelemetry/utils.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
1-
from opentelemetry.semconv.trace import SpanAttributes # type: ignore
2-
from opentelemetry.trace import Span # type: ignore
1+
from typing import cast
2+
from datetime import datetime, timezone
3+
4+
from opentelemetry.semconv.trace import SpanAttributes
5+
from opentelemetry.sdk.trace import ReadableSpan
36

47
from sentry_sdk import get_client, start_transaction
58
from sentry_sdk.utils import Dsn
69

10+
from sentry_sdk._types import TYPE_CHECKING
11+
12+
if TYPE_CHECKING:
13+
from typing import Optional
14+
715
def is_sentry_span(span):
8-
# type: (Span) -> bool
16+
# type: (ReadableSpan) -> bool
917
"""
1018
Break infinite loop:
1119
HTTP requests to Sentry are caught by OTel and send again to Sentry.
1220
"""
1321
span_url = span.attributes.get(SpanAttributes.HTTP_URL, None)
22+
span_url = cast("Optional[str]", span_url)
1423

1524
if not span_url:
1625
return False
@@ -31,3 +40,7 @@ def is_sentry_span(span):
3140
return True
3241

3342
return False
43+
44+
def convert_otel_timestamp(time):
45+
# type: (int) -> datetime
46+
return datetime.fromtimestamp(time / 1e9, timezone.utc)

0 commit comments

Comments
 (0)