Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Link errors to OTel spans #1787

Merged
merged 10 commits into from Dec 19, 2022
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