Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### Breaking Changes

- 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
- `Sentry.captureFeedback(...)`: use `Sentry.feedback().capture(...)` instead
Expand Down
10 changes: 10 additions & 0 deletions sentry-opentelemetry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,13 @@ Sentry.init(
}
)
```

## Trace propagation

The Sentry propagator only injects tracing headers into outgoing requests when the OpenTelemetry
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.
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
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.opentelemetry.semconv.ServerAttributes;
import io.sentry.Baggage;
import io.sentry.BaggageHeader;
import io.sentry.IScopes;
Expand Down Expand Up @@ -74,11 +77,18 @@ public <C> void inject(final Context context, final C carrier, final TextMapSett
return;
}

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 String propagationTarget =
getPropagationTarget(otelSpan, sentrySpan, scopes.getOptions());
final @Nullable TracingUtils.TracingHeaders tracingHeaders;
if (propagationTarget != null) {
tracingHeaders =
TracingUtils.traceIfAllowed(
scopes, propagationTarget, Collections.emptyList(), sentrySpan);
} else if (isSpanKind(otelSpan, SpanKind.PRODUCER)) {
tracingHeaders = TracingUtils.trace(scopes, Collections.emptyList(), sentrySpan);
} else {
tracingHeaders = null;
}

if (tracingHeaders != null) {
final @NotNull SentryTraceHeader sentryTraceHeader = tracingHeaders.getSentryTraceHeader();
Expand All @@ -90,13 +100,46 @@ public <C> void inject(final Context context, final C carrier, final TextMapSett
}
}

private @Nullable String getUrl(
final @NotNull IOtelSpanWrapper sentrySpan, final @NotNull SentryOptions options) {
private boolean isSpanKind(final @NotNull Span otelSpan, final @NotNull SpanKind spanKind) {
return otelSpan instanceof ReadableSpan && spanKind.equals(((ReadableSpan) otelSpan).getKind());
}

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ 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.ServerAttributes
import io.opentelemetry.semconv.UrlAttributes
import io.sentry.BaggageHeader
import io.sentry.Sentry
Expand Down Expand Up @@ -119,7 +122,7 @@ class OtelSentryPropagatorTest {
}

@Test
fun `injects headers if no URL`() {
fun `does not inject headers if no URL`() {
val propagator = OtelSentryPropagator()
val carrier = mutableMapOf<String, String>()

Expand All @@ -144,13 +147,124 @@ 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<String, String>()

val sentrySpan = mock<IOtelSpanWrapper>()
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"],
)
}

@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<String, String>()
val otelAttributes =
Attributes.builder()
.put(ServerAttributes.SERVER_ADDRESS, "orders.internal")
.put(ServerAttributes.SERVER_PORT, 4317L)
.build()
val sentrySpan = mock<IOtelSpanWrapper>()
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<String, String>()
val otelAttributes =
Attributes.builder()
.put(ServerAttributes.SERVER_ADDRESS, "orders.internal")
.put(ServerAttributes.SERVER_PORT, 4317L)
.build()
val sentrySpan = mock<IOtelSpanWrapper>()
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()
Expand Down
10 changes: 10 additions & 0 deletions sentry-opentelemetry/sentry-opentelemetry-otlp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,13 @@ 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 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.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -88,20 +89,30 @@ public <C> 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);

return url == null
|| PropagationTargetsUtils.contain(options.getTracePropagationTargets(), url);
}
if (url != null) {
return PropagationTargetsUtils.contain(options.getTracePropagationTargets(), url);
}

private @Nullable String extractUrl(
final @NotNull Span otelSpan, final @NotNull SentryOptions options) {
if (!(otelSpan instanceof ReadableSpan)) {
return null;
if (SpanKind.CLIENT.equals(readableSpan.getKind())) {
final @Nullable String serverAuthority = getServerAuthority(attributes);
return serverAuthority != null
&& PropagationTargetsUtils.contain(options.getTracePropagationTargets(), serverAuthority);
}

final @NotNull Attributes attributes = ((ReadableSpan) otelSpan).getAttributes();
return SpanKind.PRODUCER.equals(readableSpan.getKind());
}

private @Nullable String extractUrl(
final @NotNull Attributes attributes, final @NotNull SentryOptions options) {
final @Nullable String urlFull = attributes.get(UrlAttributes.URL_FULL);
if (urlFull != null) {
return urlFull;
Expand Down Expand Up @@ -131,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 <C> Context extract(
final Context context, final C carrier, final TextMapGetter<C> getter) {
Expand Down
Loading
Loading