Skip to content

Commit

Permalink
Link errors to OTel spans (#1787)
Browse files Browse the repository at this point in the history
Link Sentry captured issue events to performance events from Otel. (This makes Sentry issues visible in Otel performance data)
  • Loading branch information
antonpirker committed Dec 19, 2022
1 parent 561cd4b commit 6959941
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 2 deletions.
47 changes: 47 additions & 0 deletions sentry_sdk/integrations/opentelemetry/span_processor.py
Expand Up @@ -6,16 +6,22 @@
from opentelemetry.trace import ( # type: ignore
format_span_id,
format_trace_id,
get_current_span,
SpanContext,
Span as OTelSpan,
SpanKind,
)
from opentelemetry.trace.span import ( # type: ignore
INVALID_SPAN_ID,
INVALID_TRACE_ID,
)
from sentry_sdk.consts import INSTRUMENTER
from sentry_sdk.hub import Hub
from sentry_sdk.integrations.opentelemetry.consts import (
SENTRY_BAGGAGE_KEY,
SENTRY_TRACE_KEY,
)
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk.tracing import Transaction, Span as SentrySpan
from sentry_sdk.utils import Dsn
from sentry_sdk._types import MYPY
Expand All @@ -26,10 +32,44 @@
from typing import Any
from typing import Dict
from typing import Union
from sentry_sdk._types import Event, Hint

OPEN_TELEMETRY_CONTEXT = "otel"


def link_trace_context_to_error_event(event, otel_span_map):
# type: (Event, Dict[str, Union[Transaction, OTelSpan]]) -> Event
hub = Hub.current
if not hub:
return event

if hub.client and hub.client.options["instrumenter"] != INSTRUMENTER.OTEL:
return event

if hasattr(event, "type") and event["type"] == "transaction":
return event

otel_span = get_current_span()
if not otel_span:
return event

ctx = otel_span.get_span_context()
trace_id = format_trace_id(ctx.trace_id)
span_id = format_span_id(ctx.span_id)

if trace_id == INVALID_TRACE_ID or span_id == INVALID_SPAN_ID:
return event

sentry_span = otel_span_map.get(span_id, None)
if not sentry_span:
return event

contexts = event.setdefault("contexts", {})
contexts.setdefault("trace", {}).update(sentry_span.get_trace_context())

return event


class SentrySpanProcessor(SpanProcessor): # type: ignore
"""
Converts OTel spans into Sentry spans so they can be sent to the Sentry backend.
Expand All @@ -45,6 +85,13 @@ def __new__(cls):

return cls.instance

def __init__(self):
# type: () -> None
@add_global_event_processor
def global_event_processor(event, hint):
# type: (Event, Hint) -> Event
return link_trace_context_to_error_event(event, self.otel_span_map)

def on_start(self, otel_span, parent_context=None):
# type: (OTelSpan, SpanContext) -> None
hub = Hub.current
Expand Down
60 changes: 58 additions & 2 deletions tests/integrations/opentelemetry/test_span_processor.py
Expand Up @@ -2,10 +2,13 @@
from mock import MagicMock
import mock
import time
from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor
from sentry_sdk.integrations.opentelemetry.span_processor import (
SentrySpanProcessor,
link_trace_context_to_error_event,
)
from sentry_sdk.tracing import Span, Transaction

from opentelemetry.trace import SpanKind
from opentelemetry.trace import SpanKind, SpanContext


def test_is_sentry_span():
Expand Down Expand Up @@ -403,3 +406,56 @@ def test_on_end_sentry_span():
fake_sentry_span, otel_span
)
fake_sentry_span.finish.assert_called_once()


def test_link_trace_context_to_error_event():
"""
Test that the trace context is added to the error event.
"""
fake_client = MagicMock()
fake_client.options = {"instrumenter": "otel"}
fake_client

current_hub = MagicMock()
current_hub.client = fake_client

fake_hub = MagicMock()
fake_hub.current = current_hub

span_id = "1234567890abcdef"
trace_id = "1234567890abcdef1234567890abcdef"

fake_trace_context = {
"bla": "blub",
"foo": "bar",
"baz": 123,
}

sentry_span = MagicMock()
sentry_span.get_trace_context = MagicMock(return_value=fake_trace_context)

otel_span_map = {
span_id: sentry_span,
}

span_context = SpanContext(
trace_id=int(trace_id, 16),
span_id=int(span_id, 16),
is_remote=True,
)
otel_span = MagicMock()
otel_span.get_span_context = MagicMock(return_value=span_context)

fake_event = {"event_id": "1234567890abcdef1234567890abcdef"}

with mock.patch(
"sentry_sdk.integrations.opentelemetry.span_processor.get_current_span",
return_value=otel_span,
):
event = link_trace_context_to_error_event(fake_event, otel_span_map)

assert event
assert event == fake_event # the event is changed in place inside the function
assert "contexts" in event
assert "trace" in event["contexts"]
assert event["contexts"]["trace"] == fake_trace_context

0 comments on commit 6959941

Please sign in to comment.