From d6755a422484fdc3d91af39f7b12d35e13eee34e Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 29 Oct 2026 17:46:44 +0100 Subject: [PATCH 1/3] fix(opentelemetry)!: Skip propagation without URL Stop injecting sentry-trace and baggage for URL-less outgoing spans so tracePropagationTargets can be enforced consistently. Preserve propagation for producer spans because messaging carriers do not have a URL. BREAKING CHANGE: URL-less non-producer spans no longer receive Sentry tracing headers. Fixes #5725 Co-Authored-By: Claude --- sentry-opentelemetry/README.md | 9 +++ .../opentelemetry/OtelSentryPropagator.java | 20 ++++- .../test/kotlin/OtelSentryPropagatorTest.kt | 39 +++++++++- .../sentry-opentelemetry-otlp/README.md | 9 +++ .../otlp/OpenTelemetryOtlpPropagator.java | 9 ++- .../test/kotlin/OtelSentryPropagatorTest.kt | 77 ++++++++++++++----- 6 files changed, 136 insertions(+), 27 deletions(-) diff --git a/sentry-opentelemetry/README.md b/sentry-opentelemetry/README.md index b31c43e2211..b4581f8da9e 100644 --- a/sentry-opentelemetry/README.md +++ b/sentry-opentelemetry/README.md @@ -83,3 +83,12 @@ Sentry.init( } ) ``` + +## Trace propagation + +The Sentry propagator only injects tracing headers into outgoing requests when the OpenTelemetry +span contains a URL that matches `tracePropagationTargets`. If no URL can be determined, tracing +headers are not injected. + +Messaging producer spans (`SpanKind.PRODUCER`) are an exception because queue and messaging +instrumentations propagate tracing information through message metadata rather than a URL. diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java index e87af070748..ec79dabb4c7 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java @@ -3,12 +3,14 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.TraceFlags; import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.context.Context; import io.opentelemetry.context.propagation.TextMapGetter; import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.sdk.trace.ReadableSpan; import io.sentry.Baggage; import io.sentry.BaggageHeader; import io.sentry.IScopes; @@ -75,10 +77,15 @@ public void inject(final Context context, final C carrier, final TextMapSett } final @Nullable String url = getUrl(sentrySpan, scopes.getOptions()); - final @Nullable TracingUtils.TracingHeaders tracingHeaders = - url == null - ? TracingUtils.trace(scopes, Collections.emptyList(), sentrySpan) - : TracingUtils.traceIfAllowed(scopes, url, Collections.emptyList(), sentrySpan); + final @Nullable TracingUtils.TracingHeaders tracingHeaders; + if (url != null) { + tracingHeaders = + TracingUtils.traceIfAllowed(scopes, url, Collections.emptyList(), sentrySpan); + } else if (isProducerSpan(otelSpan)) { + tracingHeaders = TracingUtils.trace(scopes, Collections.emptyList(), sentrySpan); + } else { + tracingHeaders = null; + } if (tracingHeaders != null) { final @NotNull SentryTraceHeader sentryTraceHeader = tracingHeaders.getSentryTraceHeader(); @@ -90,6 +97,11 @@ public void inject(final Context context, final C carrier, final TextMapSett } } + private boolean isProducerSpan(final @NotNull Span otelSpan) { + return otelSpan instanceof ReadableSpan + && SpanKind.PRODUCER.equals(((ReadableSpan) otelSpan).getKind()); + } + private @Nullable String getUrl( final @NotNull IOtelSpanWrapper sentrySpan, final @NotNull SentryOptions options) { final @Nullable Attributes attributes = sentrySpan.getOpenTelemetrySpanAttributes(); diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelSentryPropagatorTest.kt b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelSentryPropagatorTest.kt index 6b9e733157d..27dcabfe272 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelSentryPropagatorTest.kt +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelSentryPropagatorTest.kt @@ -3,11 +3,13 @@ package io.sentry.opentelemetry import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.Span import io.opentelemetry.api.trace.SpanContext +import io.opentelemetry.api.trace.SpanKind import io.opentelemetry.api.trace.TraceFlags import io.opentelemetry.api.trace.TraceState import io.opentelemetry.context.Context import io.opentelemetry.context.propagation.TextMapGetter import io.opentelemetry.context.propagation.TextMapSetter +import io.opentelemetry.sdk.trace.SdkTracerProvider import io.opentelemetry.semconv.UrlAttributes import io.sentry.BaggageHeader import io.sentry.Sentry @@ -119,7 +121,7 @@ class OtelSentryPropagatorTest { } @Test - fun `injects headers if no URL`() { + fun `does not inject headers if no URL`() { val propagator = OtelSentryPropagator() val carrier = mutableMapOf() @@ -144,7 +146,40 @@ class OtelSentryPropagatorTest { propagator.inject(Context.root().with(otelSpan), carrier, MapSetter()) - assertEquals("f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1", carrier["sentry-trace"]) + assertNull(carrier["sentry-trace"]) + assertNull(carrier["baggage"]) + } + + @Test + fun `injects headers for producer span without URL`() { + val propagator = OtelSentryPropagator() + val carrier = mutableMapOf() + + val sentrySpan = mock() + whenever(sentrySpan.toSentryTrace()) + .thenReturn(SentryTraceHeader("f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1")) + whenever(sentrySpan.toBaggageHeader(anyOrNull())) + .thenReturn( + BaggageHeader( + "sentry-environment=production,sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=df71f5972f754b4c85af13ff5c07017d" + ) + ) + val tracerProvider = SdkTracerProvider.builder().build() + val otelSpan = + tracerProvider.get("test").spanBuilder("test").setSpanKind(SpanKind.PRODUCER).startSpan() + spanStorage.storeSentrySpan(otelSpan.spanContext, sentrySpan) + + try { + propagator.inject(Context.root().with(otelSpan), carrier, MapSetter()) + } finally { + otelSpan.end() + tracerProvider.shutdown() + } + + assertEquals( + "f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1", + carrier["sentry-trace"], + ) assertEquals( "sentry-environment=production,sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=df71f5972f754b4c85af13ff5c07017d", carrier["baggage"], diff --git a/sentry-opentelemetry/sentry-opentelemetry-otlp/README.md b/sentry-opentelemetry/sentry-opentelemetry-otlp/README.md index c729ce27629..74bb559cbf5 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-otlp/README.md +++ b/sentry-opentelemetry/sentry-opentelemetry-otlp/README.md @@ -5,3 +5,12 @@ This module provides a lightweight integration for using OpenTelemetry alongside Unlike the other `sentry-opentelemetry-*` modules, this module does not rely on OpenTelemetry for scope storage or span creation. It is intended for setups where OpenTelemetry handles performance/tracing and Sentry handles errors, logs, metrics, and other products. Please consult the documentation on how to install and use this integration in the [Sentry Docs for Java](https://docs.sentry.io/platforms/java/). + +## Trace propagation + +The Sentry propagator only injects tracing headers into outgoing requests when the OpenTelemetry +span contains a URL that matches `tracePropagationTargets`. If no URL can be determined, tracing +headers are not injected. + +Messaging producer spans (`SpanKind.PRODUCER`) are an exception because queue and messaging +instrumentations propagate tracing information through message metadata rather than a URL. diff --git a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java index 4cdf1b0ed09..0cdd56f9939 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java +++ b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java @@ -5,6 +5,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.TraceFlags; import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.context.Context; @@ -91,8 +92,12 @@ private boolean shouldInjectTracingHeaders(final @NotNull Span otelSpan) { final @NotNull SentryOptions options = scopes.getOptions(); final @Nullable String url = extractUrl(otelSpan, options); - return url == null - || PropagationTargetsUtils.contain(options.getTracePropagationTargets(), url); + if (url != null) { + return PropagationTargetsUtils.contain(options.getTracePropagationTargets(), url); + } + + return otelSpan instanceof ReadableSpan + && SpanKind.PRODUCER.equals(((ReadableSpan) otelSpan).getKind()); } private @Nullable String extractUrl( diff --git a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt index afa728c7ff0..9ade26cc08d 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt +++ b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt @@ -2,6 +2,7 @@ package io.sentry.opentelemetry.otlp import io.opentelemetry.api.trace.Span import io.opentelemetry.api.trace.SpanContext +import io.opentelemetry.api.trace.SpanKind import io.opentelemetry.api.trace.TraceFlags import io.opentelemetry.api.trace.TraceState import io.opentelemetry.context.Context @@ -144,7 +145,7 @@ class OpenTelemetryOtlpPropagatorTest { } @Test - fun `injects headers`() { + fun `does not inject headers if URL is unavailable`() { val propagator = OpenTelemetryOtlpPropagator() val carrier = mutableMapOf() @@ -165,11 +166,8 @@ class OpenTelemetryOtlpPropagatorTest { propagator.inject(context, carrier, MapSetter()) - assertEquals("f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1", carrier["sentry-trace"]) - assertEquals( - "sentry-environment=production,sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=df71f5972f754b4c85af13ff5c07017d", - carrier["baggage"], - ) + assertNull(carrier["sentry-trace"]) + assertNull(carrier["baggage"]) } @Test @@ -240,7 +238,7 @@ class OpenTelemetryOtlpPropagatorTest { } @Test - fun `injects headers if tracePropagationTargets is restricted and URL is unavailable`() { + fun `does not inject headers if tracePropagationTargets is restricted and URL is unavailable`() { Sentry.init { options -> options.dsn = "https://key@sentry.io/proj" options.setTracePropagationTargets(listOf("sentry.io")) @@ -259,10 +257,45 @@ class OpenTelemetryOtlpPropagatorTest { propagator.inject(Context.root().with(otelSpan), carrier, MapSetter()) - assertEquals("f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1", carrier["sentry-trace"]) + assertNull(carrier["sentry-trace"]) assertNull(carrier["baggage"]) } + @Test + fun `injects headers for producer span without URL`() { + Sentry.init { options -> + options.dsn = "https://key@sentry.io/proj" + options.setTracePropagationTargets(listOf("sentry.io")) + } + val propagator = OpenTelemetryOtlpPropagator() + val carrier = mutableMapOf() + val tracerProvider = SdkTracerProvider.builder().build() + val otelSpan = + tracerProvider.get("test").spanBuilder("test").setSpanKind(SpanKind.PRODUCER).startSpan() + val baggage = + Baggage.fromHeader( + "sentry-environment=production,sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=df71f5972f754b4c85af13ff5c07017d" + ) + + try { + val context = + Context.root().with(otelSpan).with(OpenTelemetryOtlpPropagator.SENTRY_BAGGAGE_KEY, baggage) + propagator.inject(context, carrier, MapSetter()) + } finally { + otelSpan.end() + tracerProvider.shutdown() + } + + assertEquals( + "${otelSpan.spanContext.traceId}-${otelSpan.spanContext.spanId}-1", + carrier["sentry-trace"], + ) + assertEquals( + "sentry-environment=production,sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=df71f5972f754b4c85af13ff5c07017d", + carrier["baggage"], + ) + } + @Test fun `does not inject headers when no span in context`() { val propagator = OpenTelemetryOtlpPropagator() @@ -289,19 +322,25 @@ class OpenTelemetryOtlpPropagatorTest { fun `does not inject baggage header when baggage is missing from context`() { val propagator = OpenTelemetryOtlpPropagator() val carrier = mutableMapOf() + val tracerProvider = SdkTracerProvider.builder().build() + val otelSpan = + tracerProvider + .get("test") + .spanBuilder("test") + .setAttribute("url.full", "https://sentry.io/api/0/") + .startSpan() - val otelSpanContext = - SpanContext.create( - "f9118105af4a2d42b4124532cd1065ff", - "424cffc8f94feeee", - TraceFlags.getSampled(), - TraceState.getDefault(), - ) - val otelSpan = Span.wrap(otelSpanContext) - - propagator.inject(Context.root().with(otelSpan), carrier, MapSetter()) + try { + propagator.inject(Context.root().with(otelSpan), carrier, MapSetter()) + } finally { + otelSpan.end() + tracerProvider.shutdown() + } - assertEquals("f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1", carrier["sentry-trace"]) + assertEquals( + "${otelSpan.spanContext.traceId}-${otelSpan.spanContext.spanId}-1", + carrier["sentry-trace"], + ) assertNull(carrier["baggage"]) } } From eefefae2c1a4aa62ade8784d2fbc8a9b2b1f5207 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 29 Oct 2026 17:49:00 +0100 Subject: [PATCH 2/3] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eb80a6a295..f90001c6c46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Breaking Changes +- Stop OpenTelemetry propagators from injecting tracing headers when an outgoing span has no URL ([#6419](https://github.com/getsentry/sentry-java/pull/6419)) + - Messaging producer spans continue to propagate trace context through message metadata - Remove the deprecated user feedback APIs ([#6337](https://github.com/getsentry/sentry-java/pull/6337)) - `UserFeedback` and `Sentry.captureUserFeedback(...)`: use `Sentry.feedback().capture(...)` with `io.sentry.protocol.Feedback` instead - `Sentry.captureFeedback(...)`: use `Sentry.feedback().capture(...)` instead From 433f6d4ffcaf79f6935c34bfd5babee6597d63fa Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 30 Oct 2026 07:02:52 +0100 Subject: [PATCH 3/3] fix(opentelemetry): Match client server authorities Use server.address and optional server.port as propagation targets for URL-less client spans. Preserve gRPC and other destination-aware client propagation while keeping unknown carriers fail-closed. Co-Authored-By: Claude --- CHANGELOG.md | 3 +- sentry-opentelemetry/README.md | 5 +- .../opentelemetry/OtelSentryPropagator.java | 51 +++++++++--- .../test/kotlin/OtelSentryPropagatorTest.kt | 79 +++++++++++++++++++ .../sentry-opentelemetry-otlp/README.md | 5 +- .../otlp/OpenTelemetryOtlpPropagator.java | 41 +++++++--- .../test/kotlin/OtelSentryPropagatorTest.kt | 61 ++++++++++++++ 7 files changed, 221 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f90001c6c46..fb121225242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ ### Breaking Changes -- Stop OpenTelemetry propagators from injecting tracing headers when an outgoing span has no URL ([#6419](https://github.com/getsentry/sentry-java/pull/6419)) +- Stop OpenTelemetry propagators from injecting tracing headers when an outgoing span has no matchable destination ([#6419](https://github.com/getsentry/sentry-java/pull/6419)) + - Client spans without a URL use `server.address` and optional `server.port` for matching `tracePropagationTargets` - Messaging producer spans continue to propagate trace context through message metadata - Remove the deprecated user feedback APIs ([#6337](https://github.com/getsentry/sentry-java/pull/6337)) - `UserFeedback` and `Sentry.captureUserFeedback(...)`: use `Sentry.feedback().capture(...)` with `io.sentry.protocol.Feedback` instead diff --git a/sentry-opentelemetry/README.md b/sentry-opentelemetry/README.md index b4581f8da9e..837f7e5a014 100644 --- a/sentry-opentelemetry/README.md +++ b/sentry-opentelemetry/README.md @@ -87,8 +87,9 @@ Sentry.init( ## Trace propagation The Sentry propagator only injects tracing headers into outgoing requests when the OpenTelemetry -span contains a URL that matches `tracePropagationTargets`. If no URL can be determined, tracing -headers are not injected. +span contains a destination that matches `tracePropagationTargets`. URLs are used when available. +For client spans without a URL, the destination is built from `server.address` and the optional +`server.port`. Spans without a URL or server address do not receive tracing headers. Messaging producer spans (`SpanKind.PRODUCER`) are an exception because queue and messaging instrumentations propagate tracing information through message metadata rather than a URL. diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java index ec79dabb4c7..01795b34214 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java @@ -11,6 +11,7 @@ import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.context.propagation.TextMapSetter; import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.semconv.ServerAttributes; import io.sentry.Baggage; import io.sentry.BaggageHeader; import io.sentry.IScopes; @@ -76,12 +77,14 @@ public void inject(final Context context, final C carrier, final TextMapSett return; } - final @Nullable String url = getUrl(sentrySpan, scopes.getOptions()); + final @Nullable String propagationTarget = + getPropagationTarget(otelSpan, sentrySpan, scopes.getOptions()); final @Nullable TracingUtils.TracingHeaders tracingHeaders; - if (url != null) { + if (propagationTarget != null) { tracingHeaders = - TracingUtils.traceIfAllowed(scopes, url, Collections.emptyList(), sentrySpan); - } else if (isProducerSpan(otelSpan)) { + TracingUtils.traceIfAllowed( + scopes, propagationTarget, Collections.emptyList(), sentrySpan); + } else if (isSpanKind(otelSpan, SpanKind.PRODUCER)) { tracingHeaders = TracingUtils.trace(scopes, Collections.emptyList(), sentrySpan); } else { tracingHeaders = null; @@ -97,18 +100,46 @@ public void inject(final Context context, final C carrier, final TextMapSett } } - private boolean isProducerSpan(final @NotNull Span otelSpan) { - return otelSpan instanceof ReadableSpan - && SpanKind.PRODUCER.equals(((ReadableSpan) otelSpan).getKind()); + private boolean isSpanKind(final @NotNull Span otelSpan, final @NotNull SpanKind spanKind) { + return otelSpan instanceof ReadableSpan && spanKind.equals(((ReadableSpan) otelSpan).getKind()); } - private @Nullable String getUrl( - final @NotNull IOtelSpanWrapper sentrySpan, final @NotNull SentryOptions options) { + private @Nullable String getPropagationTarget( + final @NotNull Span otelSpan, + final @NotNull IOtelSpanWrapper sentrySpan, + final @NotNull SentryOptions options) { final @Nullable Attributes attributes = sentrySpan.getOpenTelemetrySpanAttributes(); if (attributes == null) { return null; } - return attributesExtractor.extractUrl(attributes, options); + + final @Nullable String url = attributesExtractor.extractUrl(attributes, options); + if (url != null) { + return url; + } + + if (!isSpanKind(otelSpan, SpanKind.CLIENT)) { + return null; + } + + return getServerAuthority(attributes); + } + + private @Nullable String getServerAuthority(final @NotNull Attributes attributes) { + final @Nullable String serverAddress = attributes.get(ServerAttributes.SERVER_ADDRESS); + if (serverAddress == null || serverAddress.isEmpty()) { + return null; + } + + final @Nullable Long serverPort = attributes.get(ServerAttributes.SERVER_PORT); + if (serverPort == null) { + return serverAddress; + } + + final boolean isIpv6Address = serverAddress.indexOf(':') >= 0 && !serverAddress.startsWith("["); + final @NotNull String formattedAddress = + isIpv6Address ? "[" + serverAddress + "]" : serverAddress; + return formattedAddress + ":" + serverPort; } @Override diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelSentryPropagatorTest.kt b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelSentryPropagatorTest.kt index 27dcabfe272..235e7e9f5bf 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelSentryPropagatorTest.kt +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelSentryPropagatorTest.kt @@ -10,6 +10,7 @@ import io.opentelemetry.context.Context import io.opentelemetry.context.propagation.TextMapGetter import io.opentelemetry.context.propagation.TextMapSetter import io.opentelemetry.sdk.trace.SdkTracerProvider +import io.opentelemetry.semconv.ServerAttributes import io.opentelemetry.semconv.UrlAttributes import io.sentry.BaggageHeader import io.sentry.Sentry @@ -186,6 +187,84 @@ class OtelSentryPropagatorTest { ) } + @Test + fun `injects headers for client span if server authority matches tracePropagationTargets`() { + Sentry.init { options -> + options.dsn = "https://key@sentry.io/proj" + options.setTracePropagationTargets(listOf("orders.internal:4317")) + } + val propagator = OtelSentryPropagator() + val carrier = mutableMapOf() + val otelAttributes = + Attributes.builder() + .put(ServerAttributes.SERVER_ADDRESS, "orders.internal") + .put(ServerAttributes.SERVER_PORT, 4317L) + .build() + val sentrySpan = mock() + whenever(sentrySpan.toSentryTrace()) + .thenReturn(SentryTraceHeader("f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1")) + whenever(sentrySpan.openTelemetrySpanAttributes).thenReturn(otelAttributes) + val tracerProvider = SdkTracerProvider.builder().build() + val otelSpan = + tracerProvider + .get("test") + .spanBuilder("test") + .setSpanKind(SpanKind.CLIENT) + .setAllAttributes(otelAttributes) + .startSpan() + spanStorage.storeSentrySpan(otelSpan.spanContext, sentrySpan) + + try { + propagator.inject(Context.root().with(otelSpan), carrier, MapSetter()) + } finally { + otelSpan.end() + tracerProvider.shutdown() + } + + assertEquals( + "f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1", + carrier["sentry-trace"], + ) + } + + @Test + fun `does not inject headers for client span if server authority does not match tracePropagationTargets`() { + Sentry.init { options -> + options.dsn = "https://key@sentry.io/proj" + options.setTracePropagationTargets(listOf("orders.internal:443")) + } + val propagator = OtelSentryPropagator() + val carrier = mutableMapOf() + val otelAttributes = + Attributes.builder() + .put(ServerAttributes.SERVER_ADDRESS, "orders.internal") + .put(ServerAttributes.SERVER_PORT, 4317L) + .build() + val sentrySpan = mock() + whenever(sentrySpan.toSentryTrace()) + .thenReturn(SentryTraceHeader("f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1")) + whenever(sentrySpan.openTelemetrySpanAttributes).thenReturn(otelAttributes) + val tracerProvider = SdkTracerProvider.builder().build() + val otelSpan = + tracerProvider + .get("test") + .spanBuilder("test") + .setSpanKind(SpanKind.CLIENT) + .setAllAttributes(otelAttributes) + .startSpan() + spanStorage.storeSentrySpan(otelSpan.spanContext, sentrySpan) + + try { + propagator.inject(Context.root().with(otelSpan), carrier, MapSetter()) + } finally { + otelSpan.end() + tracerProvider.shutdown() + } + + assertNull(carrier["sentry-trace"]) + assertNull(carrier["baggage"]) + } + @Test fun `injects headers if URL in span attributes with default options`() { val propagator = OtelSentryPropagator() diff --git a/sentry-opentelemetry/sentry-opentelemetry-otlp/README.md b/sentry-opentelemetry/sentry-opentelemetry-otlp/README.md index 74bb559cbf5..221cf5bc228 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-otlp/README.md +++ b/sentry-opentelemetry/sentry-opentelemetry-otlp/README.md @@ -9,8 +9,9 @@ Please consult the documentation on how to install and use this integration in t ## Trace propagation The Sentry propagator only injects tracing headers into outgoing requests when the OpenTelemetry -span contains a URL that matches `tracePropagationTargets`. If no URL can be determined, tracing -headers are not injected. +span contains a destination that matches `tracePropagationTargets`. URLs are used when available. +For client spans without a URL, the destination is built from `server.address` and the optional +`server.port`. Spans without a URL or server address do not receive tracing headers. Messaging producer spans (`SpanKind.PRODUCER`) are an exception because queue and messaging instrumentations propagate tracing information through message metadata rather than a URL. diff --git a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java index 0cdd56f9939..6542643cfc3 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java +++ b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java @@ -89,24 +89,30 @@ public void inject(final Context context, final C carrier, final TextMapSett } private boolean shouldInjectTracingHeaders(final @NotNull Span otelSpan) { + if (!(otelSpan instanceof ReadableSpan)) { + return false; + } + + final @NotNull ReadableSpan readableSpan = (ReadableSpan) otelSpan; final @NotNull SentryOptions options = scopes.getOptions(); - final @Nullable String url = extractUrl(otelSpan, options); + final @NotNull Attributes attributes = readableSpan.getAttributes(); + final @Nullable String url = extractUrl(attributes, options); if (url != null) { return PropagationTargetsUtils.contain(options.getTracePropagationTargets(), url); } - return otelSpan instanceof ReadableSpan - && SpanKind.PRODUCER.equals(((ReadableSpan) otelSpan).getKind()); + if (SpanKind.CLIENT.equals(readableSpan.getKind())) { + final @Nullable String serverAuthority = getServerAuthority(attributes); + return serverAuthority != null + && PropagationTargetsUtils.contain(options.getTracePropagationTargets(), serverAuthority); + } + + return SpanKind.PRODUCER.equals(readableSpan.getKind()); } private @Nullable String extractUrl( - final @NotNull Span otelSpan, final @NotNull SentryOptions options) { - if (!(otelSpan instanceof ReadableSpan)) { - return null; - } - - final @NotNull Attributes attributes = ((ReadableSpan) otelSpan).getAttributes(); + final @NotNull Attributes attributes, final @NotNull SentryOptions options) { final @Nullable String urlFull = attributes.get(UrlAttributes.URL_FULL); if (urlFull != null) { return urlFull; @@ -136,6 +142,23 @@ private boolean shouldInjectTracingHeaders(final @NotNull Span otelSpan) { } } + private @Nullable String getServerAuthority(final @NotNull Attributes attributes) { + final @Nullable String serverAddress = attributes.get(ServerAttributes.SERVER_ADDRESS); + if (serverAddress == null || serverAddress.isEmpty()) { + return null; + } + + final @Nullable Long serverPort = attributes.get(ServerAttributes.SERVER_PORT); + if (serverPort == null) { + return serverAddress; + } + + final boolean isIpv6Address = serverAddress.indexOf(':') >= 0 && !serverAddress.startsWith("["); + final @NotNull String formattedAddress = + isIpv6Address ? "[" + serverAddress + "]" : serverAddress; + return formattedAddress + ":" + serverPort; + } + @Override public Context extract( final Context context, final C carrier, final TextMapGetter getter) { diff --git a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt index 9ade26cc08d..118ff2cf372 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt +++ b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt @@ -9,6 +9,7 @@ import io.opentelemetry.context.Context import io.opentelemetry.context.propagation.TextMapGetter import io.opentelemetry.context.propagation.TextMapSetter import io.opentelemetry.sdk.trace.SdkTracerProvider +import io.opentelemetry.semconv.ServerAttributes import io.sentry.Baggage import io.sentry.Sentry import kotlin.test.AfterTest @@ -261,6 +262,66 @@ class OpenTelemetryOtlpPropagatorTest { assertNull(carrier["baggage"]) } + @Test + fun `injects headers for client span if server authority matches tracePropagationTargets`() { + Sentry.init { options -> + options.dsn = "https://key@sentry.io/proj" + options.setTracePropagationTargets(listOf("orders.internal:4317")) + } + val propagator = OpenTelemetryOtlpPropagator() + val carrier = mutableMapOf() + val tracerProvider = SdkTracerProvider.builder().build() + val otelSpan = + tracerProvider + .get("test") + .spanBuilder("test") + .setSpanKind(SpanKind.CLIENT) + .setAttribute(ServerAttributes.SERVER_ADDRESS, "orders.internal") + .setAttribute(ServerAttributes.SERVER_PORT, 4317) + .startSpan() + + try { + propagator.inject(Context.root().with(otelSpan), carrier, MapSetter()) + } finally { + otelSpan.end() + tracerProvider.shutdown() + } + + assertEquals( + "${otelSpan.spanContext.traceId}-${otelSpan.spanContext.spanId}-1", + carrier["sentry-trace"], + ) + } + + @Test + fun `does not inject headers for client span if server authority does not match tracePropagationTargets`() { + Sentry.init { options -> + options.dsn = "https://key@sentry.io/proj" + options.setTracePropagationTargets(listOf("orders.internal:443")) + } + val propagator = OpenTelemetryOtlpPropagator() + val carrier = mutableMapOf() + val tracerProvider = SdkTracerProvider.builder().build() + val otelSpan = + tracerProvider + .get("test") + .spanBuilder("test") + .setSpanKind(SpanKind.CLIENT) + .setAttribute(ServerAttributes.SERVER_ADDRESS, "orders.internal") + .setAttribute(ServerAttributes.SERVER_PORT, 4317) + .startSpan() + + try { + propagator.inject(Context.root().with(otelSpan), carrier, MapSetter()) + } finally { + otelSpan.end() + tracerProvider.shutdown() + } + + assertNull(carrier["sentry-trace"]) + assertNull(carrier["baggage"]) + } + @Test fun `injects headers for producer span without URL`() { Sentry.init { options ->