diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java index c914ee9f0b0..6e776f94ed0 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java @@ -151,6 +151,7 @@ private SdkTracerProviderBuilder configureSdkTracerProvider( // TODO [POTEL] configurable or separate packages for old vs new way // return tracerProvider.addSpanProcessor(new SentrySpanProcessor()); return tracerProvider + .setSampler(new SentrySampler()) .addSpanProcessor(new PotelSentrySpanProcessor()) .addSpanProcessor(BatchSpanProcessor.builder(new SentrySpanExporter()).build()); } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api b/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api index 88a7c235306..6112e9e3cdd 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api +++ b/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api @@ -4,6 +4,12 @@ public final class io/sentry/opentelemetry/OpenTelemetryLinkErrorEventProcessor public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; } +public final class io/sentry/opentelemetry/OtelSamplingUtil { + public fun ()V + public static fun extractSamplingDecision (Lio/opentelemetry/api/common/Attributes;)Lio/sentry/TracesSamplingDecision; + public static fun extractSamplingDecisionOrDefault (Lio/opentelemetry/api/common/Attributes;)Lio/sentry/TracesSamplingDecision; +} + public final class io/sentry/opentelemetry/OtelSpanInfo { public fun (Ljava/lang/String;Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V public fun (Ljava/lang/String;Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;Ljava/util/Map;)V @@ -36,6 +42,20 @@ public final class io/sentry/opentelemetry/SentryPropagator : io/opentelemetry/c public fun inject (Lio/opentelemetry/context/Context;Ljava/lang/Object;Lio/opentelemetry/context/propagation/TextMapSetter;)V } +public final class io/sentry/opentelemetry/SentrySampler : io/opentelemetry/sdk/trace/samplers/Sampler { + public fun ()V + public fun (Lio/sentry/IScopes;)V + public fun getDescription ()Ljava/lang/String; + public fun shouldSample (Lio/opentelemetry/context/Context;Ljava/lang/String;Ljava/lang/String;Lio/opentelemetry/api/trace/SpanKind;Lio/opentelemetry/api/common/Attributes;Ljava/util/List;)Lio/opentelemetry/sdk/trace/samplers/SamplingResult; +} + +public final class io/sentry/opentelemetry/SentrySamplingResult : io/opentelemetry/sdk/trace/samplers/SamplingResult { + public fun (Lio/sentry/TracesSamplingDecision;)V + public fun getAttributes ()Lio/opentelemetry/api/common/Attributes; + public fun getDecision ()Lio/opentelemetry/sdk/trace/samplers/SamplingDecision; + public fun getSentryDecision ()Lio/sentry/TracesSamplingDecision; +} + public final class io/sentry/opentelemetry/SentrySpanExporter : io/opentelemetry/sdk/trace/export/SpanExporter { public fun ()V public fun (Lio/sentry/IScopes;)V diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSamplingUtil.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSamplingUtil.java new file mode 100644 index 00000000000..4a6124df11b --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSamplingUtil.java @@ -0,0 +1,38 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.common.Attributes; +import io.sentry.TracesSamplingDecision; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class OtelSamplingUtil { + + public static @Nullable TracesSamplingDecision extractSamplingDecisionOrDefault( + final @NotNull Attributes attributes) { + final @Nullable TracesSamplingDecision decision = extractSamplingDecision(attributes); + if (decision != null) { + return decision; + } else { + return new TracesSamplingDecision(false); + } + } + + public static @Nullable TracesSamplingDecision extractSamplingDecision( + final @NotNull Attributes attributes) { + final @Nullable Boolean sampled = attributes.get(InternalSemanticAttributes.SAMPLED); + if (sampled != null) { + final @Nullable Double sampleRate = attributes.get(InternalSemanticAttributes.SAMPLE_RATE); + final @Nullable Boolean profileSampled = + attributes.get(InternalSemanticAttributes.PROFILE_SAMPLED); + final @Nullable Double profileSampleRate = + attributes.get(InternalSemanticAttributes.PROFILE_SAMPLE_RATE); + + return new TracesSamplingDecision( + sampled, sampleRate, profileSampled == null ? false : profileSampled, profileSampleRate); + } else { + return null; + } + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/PotelSentrySpanProcessor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/PotelSentrySpanProcessor.java index 1a672ca5623..4d54f03be12 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/PotelSentrySpanProcessor.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/PotelSentrySpanProcessor.java @@ -11,7 +11,6 @@ import io.sentry.Baggage; import io.sentry.IScopes; import io.sentry.PropagationContext; -import io.sentry.SamplingContext; import io.sentry.ScopesAdapter; import io.sentry.Sentry; import io.sentry.SentryDate; @@ -19,9 +18,7 @@ import io.sentry.SentryLongDate; import io.sentry.SentryTraceHeader; import io.sentry.SpanId; -import io.sentry.TracesSampler; import io.sentry.TracesSamplingDecision; -import io.sentry.TransactionContext; import io.sentry.protocol.SentryId; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -30,15 +27,12 @@ public final class PotelSentrySpanProcessor implements SpanProcessor { private final @NotNull SentryWeakSpanStorage spanStorage = SentryWeakSpanStorage.getInstance(); private final @NotNull IScopes scopes; - private final @NotNull TracesSampler tracesSampler; - public PotelSentrySpanProcessor() { this(ScopesAdapter.getInstance()); } PotelSentrySpanProcessor(final @NotNull IScopes scopes) { this.scopes = scopes; - this.tracesSampler = new TracesSampler(scopes.getOptions()); } @Override @@ -55,10 +49,26 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri final @Nullable OtelSpanWrapper sentryParentSpan = spanStorage.getSentrySpan(otelSpan.getParentSpanContext()); - @Nullable TracesSamplingDecision samplingDecision = null; + @NotNull + TracesSamplingDecision samplingDecision = + OtelSamplingUtil.extractSamplingDecisionOrDefault(otelSpan.toSpanData().getAttributes()); @Nullable Baggage baggage = null; otelSpan.setAttribute(IS_REMOTE_PARENT, otelSpan.getParentSpanContext().isRemote()); if (sentryParentSpan == null) { + final @NotNull String traceId = otelSpan.getSpanContext().getTraceId(); + final @NotNull String spanId = otelSpan.getSpanContext().getSpanId(); + final @NotNull SpanId sentrySpanId = new SpanId(spanId); + final @NotNull String parentSpanId = otelSpan.getParentSpanContext().getSpanId(); + final @Nullable SpanId sentryParentSpanId = + io.opentelemetry.api.trace.SpanId.isValid(parentSpanId) ? new SpanId(parentSpanId) : null; + + @Nullable + SentryTraceHeader sentryTraceHeader = parentContext.get(SentryOtelKeys.SENTRY_TRACE_KEY); + @Nullable Baggage baggageFromContext = parentContext.get(SentryOtelKeys.SENTRY_BAGGAGE_KEY); + if (sentryTraceHeader != null) { + baggage = baggageFromContext; + } + final @Nullable Boolean baggageMutable = otelSpan.getAttribute(InternalSemanticAttributes.BAGGAGE_MUTABLE); final @Nullable String baggageString = @@ -69,77 +79,20 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri baggage.freeze(); } } - final @Nullable Boolean sampled = otelSpan.getAttribute(InternalSemanticAttributes.SAMPLED); - final @Nullable Double sampleRate = - otelSpan.getAttribute(InternalSemanticAttributes.SAMPLE_RATE); - final @Nullable Boolean profileSampled = - otelSpan.getAttribute(InternalSemanticAttributes.PROFILE_SAMPLED); - final @Nullable Double profileSampleRate = - otelSpan.getAttribute(InternalSemanticAttributes.PROFILE_SAMPLE_RATE); - if (sampled != null) { - // span created by Sentry API - - final @NotNull String traceId = otelSpan.getSpanContext().getTraceId(); - final @NotNull String spanId = otelSpan.getSpanContext().getSpanId(); - // TODO [POTEL] parent span id could be invalid - final @NotNull String parentSpanId = otelSpan.getParentSpanContext().getSpanId(); - - final @NotNull PropagationContext propagationContext = - new PropagationContext( - new SentryId(traceId), - new SpanId(spanId), - new SpanId(parentSpanId), - baggage, - sampled); - - scopes.configureScope( - scope -> { - scope.withPropagationContext( - oldPropagationContext -> { - scope.setPropagationContext(propagationContext); - }); - }); - - // TODO [POTEL] can we use OTel Sampler to let OTel know our sampling decision - // Sentry not sampled vs OTel not sampled may mean different things for trace propagation - samplingDecision = - new TracesSamplingDecision( - sampled, - sampleRate, - profileSampled == null ? false : profileSampled, - profileSampleRate); - } else { - // span not created by Sentry API - - final @NotNull String traceId = otelSpan.getSpanContext().getTraceId(); - final @NotNull String spanId = otelSpan.getSpanContext().getSpanId(); - final @NotNull SpanId sentrySpanId = new SpanId(spanId); - - @Nullable - SentryTraceHeader sentryTraceHeader = parentContext.get(SentryOtelKeys.SENTRY_TRACE_KEY); - @Nullable Baggage baggageFromContext = parentContext.get(SentryOtelKeys.SENTRY_BAGGAGE_KEY); - if (sentryTraceHeader != null) { - baggage = baggageFromContext; - } - final @NotNull PropagationContext propagationContext = - sentryTraceHeader == null - ? new PropagationContext(new SentryId(traceId), sentrySpanId, null, baggage, null) - : PropagationContext.fromHeaders(sentryTraceHeader, baggage, sentrySpanId); - - scopes.configureScope( - scope -> { - scope.withPropagationContext( - oldPropagationContext -> { - scope.setPropagationContext(propagationContext); - }); - }); - - final @NotNull TransactionContext transactionContext = - TransactionContext.fromPropagationContext(propagationContext); - samplingDecision = tracesSampler.sample(new SamplingContext(transactionContext, null)); - } + // TODO [POTEL] what do we use as fallback here? could happen if misconfigured (i.e. sampler + // not in place) + final boolean sampled = samplingDecision != null ? samplingDecision.getSampled() : true; + + final @NotNull PropagationContext propagationContext = + sentryTraceHeader == null + ? new PropagationContext( + new SentryId(traceId), sentrySpanId, sentryParentSpanId, baggage, sampled) + : PropagationContext.fromHeaders(sentryTraceHeader, baggage, sentrySpanId); + + updatePropagationContext(scopes, propagationContext); } + final @NotNull SpanContext spanContext = otelSpan.getSpanContext(); final @NotNull SentryDate startTimestamp = new SentryLongDate(otelSpan.toSpanData().getStartEpochNanos()); @@ -149,6 +102,17 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri otelSpan, scopes, startTimestamp, samplingDecision, sentryParentSpan, baggage)); } + private static void updatePropagationContext( + IScopes scopes, PropagationContext propagationContext) { + scopes.configureScope( + scope -> { + scope.withPropagationContext( + oldPropagationContext -> { + scope.setPropagationContext(propagationContext); + }); + }); + } + @Override public boolean isStartRequired() { return true; diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java new file mode 100644 index 00000000000..0d54cd9947e --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java @@ -0,0 +1,105 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import io.sentry.Baggage; +import io.sentry.IScopes; +import io.sentry.PropagationContext; +import io.sentry.SamplingContext; +import io.sentry.ScopesAdapter; +import io.sentry.SentryTraceHeader; +import io.sentry.SpanId; +import io.sentry.TracesSampler; +import io.sentry.TracesSamplingDecision; +import io.sentry.TransactionContext; +import io.sentry.protocol.SentryId; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class SentrySampler implements Sampler { + + private final @NotNull SentryWeakSpanStorage spanStorage = SentryWeakSpanStorage.getInstance(); + private final @NotNull TracesSampler tracesSampler; + + public SentrySampler(final @NotNull IScopes scopes) { + this.tracesSampler = new TracesSampler(scopes.getOptions()); + } + + public SentrySampler() { + this(ScopesAdapter.getInstance()); + } + + @Override + public SamplingResult shouldSample( + final @NotNull Context parentContext, + final @NotNull String traceId, + final @NotNull String name, + final @NotNull SpanKind spanKind, + final @NotNull Attributes attributes, + final @NotNull List parentLinks) { + // note: parentLinks seems to usually be empty + final @Nullable Span parentOtelSpan = Span.fromContextOrNull(parentContext); + final @Nullable OtelSpanWrapper parentSentrySpan = + parentOtelSpan != null ? spanStorage.getSentrySpan(parentOtelSpan.getSpanContext()) : null; + + if (parentSentrySpan != null) { + return copyParentSentryDecision(parentSentrySpan); + } else { + final @Nullable TracesSamplingDecision samplingDecision = + OtelSamplingUtil.extractSamplingDecision(attributes); + if (samplingDecision != null) { + return new SentrySamplingResult(samplingDecision); + } else { + return handleRootOtelSpan(traceId, parentContext); + } + } + } + + private @NotNull SentrySamplingResult handleRootOtelSpan( + final @NotNull String traceId, final @NotNull Context parentContext) { + @Nullable Baggage baggage = null; + @Nullable + SentryTraceHeader sentryTraceHeader = parentContext.get(SentryOtelKeys.SENTRY_TRACE_KEY); + @Nullable Baggage baggageFromContext = parentContext.get(SentryOtelKeys.SENTRY_BAGGAGE_KEY); + if (sentryTraceHeader != null) { + baggage = baggageFromContext; + } + + // there's no way to get the span id here, so we just use a random id for sampling + SpanId randomSpanId = new SpanId(); + final @NotNull PropagationContext propagationContext = + sentryTraceHeader == null + ? new PropagationContext(new SentryId(traceId), randomSpanId, null, baggage, null) + : PropagationContext.fromHeaders(sentryTraceHeader, baggage, randomSpanId); + + final @NotNull TransactionContext transactionContext = + TransactionContext.fromPropagationContext(propagationContext); + final @NotNull TracesSamplingDecision sentryDecision = + tracesSampler.sample(new SamplingContext(transactionContext, null)); + return new SentrySamplingResult(sentryDecision); + } + + private @NotNull SentrySamplingResult copyParentSentryDecision( + final @NotNull OtelSpanWrapper parentSentrySpan) { + final @Nullable TracesSamplingDecision parentSamplingDecision = + parentSentrySpan.getSamplingDecision(); + if (parentSamplingDecision != null) { + return new SentrySamplingResult(parentSamplingDecision); + } else { + // this should never happen and only serve to calm the compiler + // TODO [POTEL] log + return new SentrySamplingResult(new TracesSamplingDecision(true)); + } + } + + @Override + public String getDescription() { + return "SentrySampler"; + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySamplingResult.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySamplingResult.java new file mode 100644 index 00000000000..c8049f3067b --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySamplingResult.java @@ -0,0 +1,38 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import io.sentry.TracesSamplingDecision; +import org.jetbrains.annotations.NotNull; + +public final class SentrySamplingResult implements SamplingResult { + private final TracesSamplingDecision sentryDecision; + + public SentrySamplingResult(final @NotNull TracesSamplingDecision sentryDecision) { + this.sentryDecision = sentryDecision; + } + + @Override + public SamplingDecision getDecision() { + if (sentryDecision.getSampled()) { + return SamplingDecision.RECORD_AND_SAMPLE; + } else { + return SamplingDecision.RECORD_ONLY; + } + } + + @Override + public Attributes getAttributes() { + return Attributes.builder() + .put(InternalSemanticAttributes.SAMPLED, sentryDecision.getSampled()) + .put(InternalSemanticAttributes.SAMPLE_RATE, sentryDecision.getSampleRate()) + .put(InternalSemanticAttributes.PROFILE_SAMPLED, sentryDecision.getProfileSampled()) + .put(InternalSemanticAttributes.PROFILE_SAMPLE_RATE, sentryDecision.getProfileSampleRate()) + .build(); + } + + public TracesSamplingDecision getSentryDecision() { + return sentryDecision; + } +}