Skip to content

Commit 6959941

Browse files
authored
Link errors to OTel spans (#1787)
Link Sentry captured issue events to performance events from Otel. (This makes Sentry issues visible in Otel performance data)
1 parent 561cd4b commit 6959941

File tree

2 files changed

+105
-2
lines changed

2 files changed

+105
-2
lines changed

sentry_sdk/integrations/opentelemetry/span_processor.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@
66
from opentelemetry.trace import ( # type: ignore
77
format_span_id,
88
format_trace_id,
9+
get_current_span,
910
SpanContext,
1011
Span as OTelSpan,
1112
SpanKind,
1213
)
14+
from opentelemetry.trace.span import ( # type: ignore
15+
INVALID_SPAN_ID,
16+
INVALID_TRACE_ID,
17+
)
1318
from sentry_sdk.consts import INSTRUMENTER
1419
from sentry_sdk.hub import Hub
1520
from sentry_sdk.integrations.opentelemetry.consts import (
1621
SENTRY_BAGGAGE_KEY,
1722
SENTRY_TRACE_KEY,
1823
)
24+
from sentry_sdk.scope import add_global_event_processor
1925
from sentry_sdk.tracing import Transaction, Span as SentrySpan
2026
from sentry_sdk.utils import Dsn
2127
from sentry_sdk._types import MYPY
@@ -26,10 +32,44 @@
2632
from typing import Any
2733
from typing import Dict
2834
from typing import Union
35+
from sentry_sdk._types import Event, Hint
2936

3037
OPEN_TELEMETRY_CONTEXT = "otel"
3138

3239

40+
def link_trace_context_to_error_event(event, otel_span_map):
41+
# type: (Event, Dict[str, Union[Transaction, OTelSpan]]) -> Event
42+
hub = Hub.current
43+
if not hub:
44+
return event
45+
46+
if hub.client and hub.client.options["instrumenter"] != INSTRUMENTER.OTEL:
47+
return event
48+
49+
if hasattr(event, "type") and event["type"] == "transaction":
50+
return event
51+
52+
otel_span = get_current_span()
53+
if not otel_span:
54+
return event
55+
56+
ctx = otel_span.get_span_context()
57+
trace_id = format_trace_id(ctx.trace_id)
58+
span_id = format_span_id(ctx.span_id)
59+
60+
if trace_id == INVALID_TRACE_ID or span_id == INVALID_SPAN_ID:
61+
return event
62+
63+
sentry_span = otel_span_map.get(span_id, None)
64+
if not sentry_span:
65+
return event
66+
67+
contexts = event.setdefault("contexts", {})
68+
contexts.setdefault("trace", {}).update(sentry_span.get_trace_context())
69+
70+
return event
71+
72+
3373
class SentrySpanProcessor(SpanProcessor): # type: ignore
3474
"""
3575
Converts OTel spans into Sentry spans so they can be sent to the Sentry backend.
@@ -45,6 +85,13 @@ def __new__(cls):
4585

4686
return cls.instance
4787

88+
def __init__(self):
89+
# type: () -> None
90+
@add_global_event_processor
91+
def global_event_processor(event, hint):
92+
# type: (Event, Hint) -> Event
93+
return link_trace_context_to_error_event(event, self.otel_span_map)
94+
4895
def on_start(self, otel_span, parent_context=None):
4996
# type: (OTelSpan, SpanContext) -> None
5097
hub = Hub.current

tests/integrations/opentelemetry/test_span_processor.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
from mock import MagicMock
33
import mock
44
import time
5-
from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor
5+
from sentry_sdk.integrations.opentelemetry.span_processor import (
6+
SentrySpanProcessor,
7+
link_trace_context_to_error_event,
8+
)
69
from sentry_sdk.tracing import Span, Transaction
710

8-
from opentelemetry.trace import SpanKind
11+
from opentelemetry.trace import SpanKind, SpanContext
912

1013

1114
def test_is_sentry_span():
@@ -403,3 +406,56 @@ def test_on_end_sentry_span():
403406
fake_sentry_span, otel_span
404407
)
405408
fake_sentry_span.finish.assert_called_once()
409+
410+
411+
def test_link_trace_context_to_error_event():
412+
"""
413+
Test that the trace context is added to the error event.
414+
"""
415+
fake_client = MagicMock()
416+
fake_client.options = {"instrumenter": "otel"}
417+
fake_client
418+
419+
current_hub = MagicMock()
420+
current_hub.client = fake_client
421+
422+
fake_hub = MagicMock()
423+
fake_hub.current = current_hub
424+
425+
span_id = "1234567890abcdef"
426+
trace_id = "1234567890abcdef1234567890abcdef"
427+
428+
fake_trace_context = {
429+
"bla": "blub",
430+
"foo": "bar",
431+
"baz": 123,
432+
}
433+
434+
sentry_span = MagicMock()
435+
sentry_span.get_trace_context = MagicMock(return_value=fake_trace_context)
436+
437+
otel_span_map = {
438+
span_id: sentry_span,
439+
}
440+
441+
span_context = SpanContext(
442+
trace_id=int(trace_id, 16),
443+
span_id=int(span_id, 16),
444+
is_remote=True,
445+
)
446+
otel_span = MagicMock()
447+
otel_span.get_span_context = MagicMock(return_value=span_context)
448+
449+
fake_event = {"event_id": "1234567890abcdef1234567890abcdef"}
450+
451+
with mock.patch(
452+
"sentry_sdk.integrations.opentelemetry.span_processor.get_current_span",
453+
return_value=otel_span,
454+
):
455+
event = link_trace_context_to_error_event(fake_event, otel_span_map)
456+
457+
assert event
458+
assert event == fake_event # the event is changed in place inside the function
459+
assert "contexts" in event
460+
assert "trace" in event["contexts"]
461+
assert event["contexts"]["trace"] == fake_trace_context

0 commit comments

Comments
 (0)