Skip to content

Commit e50d955

Browse files
authored
POTEL 11 - Move sampling logic into OTel Sampler (#3462)
* replace hub with scopes * Add Scopes * Introduce `IScopes` interface. * Replace `IHub` with `IScopes` in core * Replace `IHub` with `IScopes` in android core * Replace `IHub` with `IScopes` in android integrations * Replace `IHub` with `IScopes` in apollo integrations * Replace `IHub` with `IScopes` in okhttp integration * Replace `IHub` with `IScopes` in graphql integration * Replace `IHub` with `IScopes` in logging integrations * Replace `IHub` with `IScopes` in more integrations * Replace `IHub` with `IScopes` in OTel integration * Replace `IHub` with `IScopes` in Spring 5 / Spring Boot 2 integrations * Replace `IHub` with `IScopes` in Spring 6 / Spring Boot 3 integrations * Replace `IHub` with `IScopes` in samples * gitscopes -> github * Replace ThreadLocal with ScopesStorage * Move client and throwable to span map to scope * Add global scope * use global scope in Scopes * Implement pushScope popScope and withScope for Scopes * Add pushIsolationScope; add fork methods to ISCope * Use separate scopes for current, isolation and global scope; rename mainScopes to rootScopes * Allow controlling which scope configureScope uses * Combine scopes * Use new API for CRONS integrations * Add lifecycle helper * Change spring integrations to use new API * Use new API in servlet integrations * Use new API for kotlin coroutines and wrapers for Supplier/Callable * Discussion TODOs * Fix breadcrumb ordering * Mark TODOS with [HSM] * Add getGlobalScope and forkedRootScopes to IScopes * Fix EventProcessor ordering on scopes * Reuse code in Scopes * No longer replace global scope * Replace hub occurrences in comments, var names etc. * Implement ScopesTest * Implement CombinedScopeViewTest * Fix combined contexts * Use combined scopes for cross platform * Changes according to reviews of previous PRs * more * even more * isEnabled checks client instead of having a property on Scopes * Use SentryOptions.empty * Remove Hub * Use OpenTelemetry for Performance and Scopes propagation * Promote certain span attributes * Use OTel in Sentry API * Deduplicate SpanInfo extraction * Forward Sentry API to Sentry through OTel * Use OTel status for Sentry span API * POTel Tracing * fix root span detection (remote flag), and scope closing * Inherit OTel span IDs when sending to sentry * Fix tracing; parse incoming baggage; add baggage to outgoing * Cleanup * Move sampling logic to OTel Sampler
1 parent 67490cd commit e50d955

File tree

6 files changed

+242
-76
lines changed

6 files changed

+242
-76
lines changed

sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ private SdkTracerProviderBuilder configureSdkTracerProvider(
151151
// TODO [POTEL] configurable or separate packages for old vs new way
152152
// return tracerProvider.addSpanProcessor(new SentrySpanProcessor());
153153
return tracerProvider
154+
.setSampler(new SentrySampler())
154155
.addSpanProcessor(new PotelSentrySpanProcessor())
155156
.addSpanProcessor(BatchSpanProcessor.builder(new SentrySpanExporter()).build());
156157
}

sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ public final class io/sentry/opentelemetry/OpenTelemetryLinkErrorEventProcessor
44
public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
55
}
66

7+
public final class io/sentry/opentelemetry/OtelSamplingUtil {
8+
public fun <init> ()V
9+
public static fun extractSamplingDecision (Lio/opentelemetry/api/common/Attributes;)Lio/sentry/TracesSamplingDecision;
10+
public static fun extractSamplingDecisionOrDefault (Lio/opentelemetry/api/common/Attributes;)Lio/sentry/TracesSamplingDecision;
11+
}
12+
713
public final class io/sentry/opentelemetry/OtelSpanInfo {
814
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V
915
public fun <init> (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
3642
public fun inject (Lio/opentelemetry/context/Context;Ljava/lang/Object;Lio/opentelemetry/context/propagation/TextMapSetter;)V
3743
}
3844

45+
public final class io/sentry/opentelemetry/SentrySampler : io/opentelemetry/sdk/trace/samplers/Sampler {
46+
public fun <init> ()V
47+
public fun <init> (Lio/sentry/IScopes;)V
48+
public fun getDescription ()Ljava/lang/String;
49+
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;
50+
}
51+
52+
public final class io/sentry/opentelemetry/SentrySamplingResult : io/opentelemetry/sdk/trace/samplers/SamplingResult {
53+
public fun <init> (Lio/sentry/TracesSamplingDecision;)V
54+
public fun getAttributes ()Lio/opentelemetry/api/common/Attributes;
55+
public fun getDecision ()Lio/opentelemetry/sdk/trace/samplers/SamplingDecision;
56+
public fun getSentryDecision ()Lio/sentry/TracesSamplingDecision;
57+
}
58+
3959
public final class io/sentry/opentelemetry/SentrySpanExporter : io/opentelemetry/sdk/trace/export/SpanExporter {
4060
public fun <init> ()V
4161
public fun <init> (Lio/sentry/IScopes;)V
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.sentry.opentelemetry;
2+
3+
import io.opentelemetry.api.common.Attributes;
4+
import io.sentry.TracesSamplingDecision;
5+
import org.jetbrains.annotations.ApiStatus;
6+
import org.jetbrains.annotations.NotNull;
7+
import org.jetbrains.annotations.Nullable;
8+
9+
@ApiStatus.Internal
10+
public final class OtelSamplingUtil {
11+
12+
public static @Nullable TracesSamplingDecision extractSamplingDecisionOrDefault(
13+
final @NotNull Attributes attributes) {
14+
final @Nullable TracesSamplingDecision decision = extractSamplingDecision(attributes);
15+
if (decision != null) {
16+
return decision;
17+
} else {
18+
return new TracesSamplingDecision(false);
19+
}
20+
}
21+
22+
public static @Nullable TracesSamplingDecision extractSamplingDecision(
23+
final @NotNull Attributes attributes) {
24+
final @Nullable Boolean sampled = attributes.get(InternalSemanticAttributes.SAMPLED);
25+
if (sampled != null) {
26+
final @Nullable Double sampleRate = attributes.get(InternalSemanticAttributes.SAMPLE_RATE);
27+
final @Nullable Boolean profileSampled =
28+
attributes.get(InternalSemanticAttributes.PROFILE_SAMPLED);
29+
final @Nullable Double profileSampleRate =
30+
attributes.get(InternalSemanticAttributes.PROFILE_SAMPLE_RATE);
31+
32+
return new TracesSamplingDecision(
33+
sampled, sampleRate, profileSampled == null ? false : profileSampled, profileSampleRate);
34+
} else {
35+
return null;
36+
}
37+
}
38+
}

sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/PotelSentrySpanProcessor.java

Lines changed: 40 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,14 @@
1111
import io.sentry.Baggage;
1212
import io.sentry.IScopes;
1313
import io.sentry.PropagationContext;
14-
import io.sentry.SamplingContext;
1514
import io.sentry.ScopesAdapter;
1615
import io.sentry.Sentry;
1716
import io.sentry.SentryDate;
1817
import io.sentry.SentryLevel;
1918
import io.sentry.SentryLongDate;
2019
import io.sentry.SentryTraceHeader;
2120
import io.sentry.SpanId;
22-
import io.sentry.TracesSampler;
2321
import io.sentry.TracesSamplingDecision;
24-
import io.sentry.TransactionContext;
2522
import io.sentry.protocol.SentryId;
2623
import org.jetbrains.annotations.NotNull;
2724
import org.jetbrains.annotations.Nullable;
@@ -30,15 +27,12 @@ public final class PotelSentrySpanProcessor implements SpanProcessor {
3027
private final @NotNull SentryWeakSpanStorage spanStorage = SentryWeakSpanStorage.getInstance();
3128
private final @NotNull IScopes scopes;
3229

33-
private final @NotNull TracesSampler tracesSampler;
34-
3530
public PotelSentrySpanProcessor() {
3631
this(ScopesAdapter.getInstance());
3732
}
3833

3934
PotelSentrySpanProcessor(final @NotNull IScopes scopes) {
4035
this.scopes = scopes;
41-
this.tracesSampler = new TracesSampler(scopes.getOptions());
4236
}
4337

4438
@Override
@@ -55,10 +49,26 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri
5549

5650
final @Nullable OtelSpanWrapper sentryParentSpan =
5751
spanStorage.getSentrySpan(otelSpan.getParentSpanContext());
58-
@Nullable TracesSamplingDecision samplingDecision = null;
52+
@NotNull
53+
TracesSamplingDecision samplingDecision =
54+
OtelSamplingUtil.extractSamplingDecisionOrDefault(otelSpan.toSpanData().getAttributes());
5955
@Nullable Baggage baggage = null;
6056
otelSpan.setAttribute(IS_REMOTE_PARENT, otelSpan.getParentSpanContext().isRemote());
6157
if (sentryParentSpan == null) {
58+
final @NotNull String traceId = otelSpan.getSpanContext().getTraceId();
59+
final @NotNull String spanId = otelSpan.getSpanContext().getSpanId();
60+
final @NotNull SpanId sentrySpanId = new SpanId(spanId);
61+
final @NotNull String parentSpanId = otelSpan.getParentSpanContext().getSpanId();
62+
final @Nullable SpanId sentryParentSpanId =
63+
io.opentelemetry.api.trace.SpanId.isValid(parentSpanId) ? new SpanId(parentSpanId) : null;
64+
65+
@Nullable
66+
SentryTraceHeader sentryTraceHeader = parentContext.get(SentryOtelKeys.SENTRY_TRACE_KEY);
67+
@Nullable Baggage baggageFromContext = parentContext.get(SentryOtelKeys.SENTRY_BAGGAGE_KEY);
68+
if (sentryTraceHeader != null) {
69+
baggage = baggageFromContext;
70+
}
71+
6272
final @Nullable Boolean baggageMutable =
6373
otelSpan.getAttribute(InternalSemanticAttributes.BAGGAGE_MUTABLE);
6474
final @Nullable String baggageString =
@@ -69,77 +79,20 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri
6979
baggage.freeze();
7080
}
7181
}
72-
final @Nullable Boolean sampled = otelSpan.getAttribute(InternalSemanticAttributes.SAMPLED);
73-
final @Nullable Double sampleRate =
74-
otelSpan.getAttribute(InternalSemanticAttributes.SAMPLE_RATE);
75-
final @Nullable Boolean profileSampled =
76-
otelSpan.getAttribute(InternalSemanticAttributes.PROFILE_SAMPLED);
77-
final @Nullable Double profileSampleRate =
78-
otelSpan.getAttribute(InternalSemanticAttributes.PROFILE_SAMPLE_RATE);
79-
if (sampled != null) {
80-
// span created by Sentry API
81-
82-
final @NotNull String traceId = otelSpan.getSpanContext().getTraceId();
83-
final @NotNull String spanId = otelSpan.getSpanContext().getSpanId();
84-
// TODO [POTEL] parent span id could be invalid
85-
final @NotNull String parentSpanId = otelSpan.getParentSpanContext().getSpanId();
86-
87-
final @NotNull PropagationContext propagationContext =
88-
new PropagationContext(
89-
new SentryId(traceId),
90-
new SpanId(spanId),
91-
new SpanId(parentSpanId),
92-
baggage,
93-
sampled);
94-
95-
scopes.configureScope(
96-
scope -> {
97-
scope.withPropagationContext(
98-
oldPropagationContext -> {
99-
scope.setPropagationContext(propagationContext);
100-
});
101-
});
102-
103-
// TODO [POTEL] can we use OTel Sampler to let OTel know our sampling decision
104-
// Sentry not sampled vs OTel not sampled may mean different things for trace propagation
105-
samplingDecision =
106-
new TracesSamplingDecision(
107-
sampled,
108-
sampleRate,
109-
profileSampled == null ? false : profileSampled,
110-
profileSampleRate);
111-
} else {
112-
// span not created by Sentry API
113-
114-
final @NotNull String traceId = otelSpan.getSpanContext().getTraceId();
115-
final @NotNull String spanId = otelSpan.getSpanContext().getSpanId();
116-
final @NotNull SpanId sentrySpanId = new SpanId(spanId);
117-
118-
@Nullable
119-
SentryTraceHeader sentryTraceHeader = parentContext.get(SentryOtelKeys.SENTRY_TRACE_KEY);
120-
@Nullable Baggage baggageFromContext = parentContext.get(SentryOtelKeys.SENTRY_BAGGAGE_KEY);
121-
if (sentryTraceHeader != null) {
122-
baggage = baggageFromContext;
123-
}
12482

125-
final @NotNull PropagationContext propagationContext =
126-
sentryTraceHeader == null
127-
? new PropagationContext(new SentryId(traceId), sentrySpanId, null, baggage, null)
128-
: PropagationContext.fromHeaders(sentryTraceHeader, baggage, sentrySpanId);
129-
130-
scopes.configureScope(
131-
scope -> {
132-
scope.withPropagationContext(
133-
oldPropagationContext -> {
134-
scope.setPropagationContext(propagationContext);
135-
});
136-
});
137-
138-
final @NotNull TransactionContext transactionContext =
139-
TransactionContext.fromPropagationContext(propagationContext);
140-
samplingDecision = tracesSampler.sample(new SamplingContext(transactionContext, null));
141-
}
83+
// TODO [POTEL] what do we use as fallback here? could happen if misconfigured (i.e. sampler
84+
// not in place)
85+
final boolean sampled = samplingDecision != null ? samplingDecision.getSampled() : true;
86+
87+
final @NotNull PropagationContext propagationContext =
88+
sentryTraceHeader == null
89+
? new PropagationContext(
90+
new SentryId(traceId), sentrySpanId, sentryParentSpanId, baggage, sampled)
91+
: PropagationContext.fromHeaders(sentryTraceHeader, baggage, sentrySpanId);
92+
93+
updatePropagationContext(scopes, propagationContext);
14294
}
95+
14396
final @NotNull SpanContext spanContext = otelSpan.getSpanContext();
14497
final @NotNull SentryDate startTimestamp =
14598
new SentryLongDate(otelSpan.toSpanData().getStartEpochNanos());
@@ -149,6 +102,17 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri
149102
otelSpan, scopes, startTimestamp, samplingDecision, sentryParentSpan, baggage));
150103
}
151104

105+
private static void updatePropagationContext(
106+
IScopes scopes, PropagationContext propagationContext) {
107+
scopes.configureScope(
108+
scope -> {
109+
scope.withPropagationContext(
110+
oldPropagationContext -> {
111+
scope.setPropagationContext(propagationContext);
112+
});
113+
});
114+
}
115+
152116
@Override
153117
public boolean isStartRequired() {
154118
return true;
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package io.sentry.opentelemetry;
2+
3+
import io.opentelemetry.api.common.Attributes;
4+
import io.opentelemetry.api.trace.Span;
5+
import io.opentelemetry.api.trace.SpanKind;
6+
import io.opentelemetry.context.Context;
7+
import io.opentelemetry.sdk.trace.data.LinkData;
8+
import io.opentelemetry.sdk.trace.samplers.Sampler;
9+
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
10+
import io.sentry.Baggage;
11+
import io.sentry.IScopes;
12+
import io.sentry.PropagationContext;
13+
import io.sentry.SamplingContext;
14+
import io.sentry.ScopesAdapter;
15+
import io.sentry.SentryTraceHeader;
16+
import io.sentry.SpanId;
17+
import io.sentry.TracesSampler;
18+
import io.sentry.TracesSamplingDecision;
19+
import io.sentry.TransactionContext;
20+
import io.sentry.protocol.SentryId;
21+
import java.util.List;
22+
import org.jetbrains.annotations.NotNull;
23+
import org.jetbrains.annotations.Nullable;
24+
25+
public final class SentrySampler implements Sampler {
26+
27+
private final @NotNull SentryWeakSpanStorage spanStorage = SentryWeakSpanStorage.getInstance();
28+
private final @NotNull TracesSampler tracesSampler;
29+
30+
public SentrySampler(final @NotNull IScopes scopes) {
31+
this.tracesSampler = new TracesSampler(scopes.getOptions());
32+
}
33+
34+
public SentrySampler() {
35+
this(ScopesAdapter.getInstance());
36+
}
37+
38+
@Override
39+
public SamplingResult shouldSample(
40+
final @NotNull Context parentContext,
41+
final @NotNull String traceId,
42+
final @NotNull String name,
43+
final @NotNull SpanKind spanKind,
44+
final @NotNull Attributes attributes,
45+
final @NotNull List<LinkData> parentLinks) {
46+
// note: parentLinks seems to usually be empty
47+
final @Nullable Span parentOtelSpan = Span.fromContextOrNull(parentContext);
48+
final @Nullable OtelSpanWrapper parentSentrySpan =
49+
parentOtelSpan != null ? spanStorage.getSentrySpan(parentOtelSpan.getSpanContext()) : null;
50+
51+
if (parentSentrySpan != null) {
52+
return copyParentSentryDecision(parentSentrySpan);
53+
} else {
54+
final @Nullable TracesSamplingDecision samplingDecision =
55+
OtelSamplingUtil.extractSamplingDecision(attributes);
56+
if (samplingDecision != null) {
57+
return new SentrySamplingResult(samplingDecision);
58+
} else {
59+
return handleRootOtelSpan(traceId, parentContext);
60+
}
61+
}
62+
}
63+
64+
private @NotNull SentrySamplingResult handleRootOtelSpan(
65+
final @NotNull String traceId, final @NotNull Context parentContext) {
66+
@Nullable Baggage baggage = null;
67+
@Nullable
68+
SentryTraceHeader sentryTraceHeader = parentContext.get(SentryOtelKeys.SENTRY_TRACE_KEY);
69+
@Nullable Baggage baggageFromContext = parentContext.get(SentryOtelKeys.SENTRY_BAGGAGE_KEY);
70+
if (sentryTraceHeader != null) {
71+
baggage = baggageFromContext;
72+
}
73+
74+
// there's no way to get the span id here, so we just use a random id for sampling
75+
SpanId randomSpanId = new SpanId();
76+
final @NotNull PropagationContext propagationContext =
77+
sentryTraceHeader == null
78+
? new PropagationContext(new SentryId(traceId), randomSpanId, null, baggage, null)
79+
: PropagationContext.fromHeaders(sentryTraceHeader, baggage, randomSpanId);
80+
81+
final @NotNull TransactionContext transactionContext =
82+
TransactionContext.fromPropagationContext(propagationContext);
83+
final @NotNull TracesSamplingDecision sentryDecision =
84+
tracesSampler.sample(new SamplingContext(transactionContext, null));
85+
return new SentrySamplingResult(sentryDecision);
86+
}
87+
88+
private @NotNull SentrySamplingResult copyParentSentryDecision(
89+
final @NotNull OtelSpanWrapper parentSentrySpan) {
90+
final @Nullable TracesSamplingDecision parentSamplingDecision =
91+
parentSentrySpan.getSamplingDecision();
92+
if (parentSamplingDecision != null) {
93+
return new SentrySamplingResult(parentSamplingDecision);
94+
} else {
95+
// this should never happen and only serve to calm the compiler
96+
// TODO [POTEL] log
97+
return new SentrySamplingResult(new TracesSamplingDecision(true));
98+
}
99+
}
100+
101+
@Override
102+
public String getDescription() {
103+
return "SentrySampler";
104+
}
105+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.sentry.opentelemetry;
2+
3+
import io.opentelemetry.api.common.Attributes;
4+
import io.opentelemetry.sdk.trace.samplers.SamplingDecision;
5+
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
6+
import io.sentry.TracesSamplingDecision;
7+
import org.jetbrains.annotations.NotNull;
8+
9+
public final class SentrySamplingResult implements SamplingResult {
10+
private final TracesSamplingDecision sentryDecision;
11+
12+
public SentrySamplingResult(final @NotNull TracesSamplingDecision sentryDecision) {
13+
this.sentryDecision = sentryDecision;
14+
}
15+
16+
@Override
17+
public SamplingDecision getDecision() {
18+
if (sentryDecision.getSampled()) {
19+
return SamplingDecision.RECORD_AND_SAMPLE;
20+
} else {
21+
return SamplingDecision.RECORD_ONLY;
22+
}
23+
}
24+
25+
@Override
26+
public Attributes getAttributes() {
27+
return Attributes.builder()
28+
.put(InternalSemanticAttributes.SAMPLED, sentryDecision.getSampled())
29+
.put(InternalSemanticAttributes.SAMPLE_RATE, sentryDecision.getSampleRate())
30+
.put(InternalSemanticAttributes.PROFILE_SAMPLED, sentryDecision.getProfileSampled())
31+
.put(InternalSemanticAttributes.PROFILE_SAMPLE_RATE, sentryDecision.getProfileSampleRate())
32+
.build();
33+
}
34+
35+
public TracesSamplingDecision getSentryDecision() {
36+
return sentryDecision;
37+
}
38+
}

0 commit comments

Comments
 (0)