Skip to content

Commit 19d9e70

Browse files
committed
refactor(otel): Accept builder, remove custom sampling, fix scope leaks
1 parent 0835dcc commit 19d9e70

7 files changed

Lines changed: 51 additions & 225 deletions

File tree

examples/src/main/java/software/amazon/lambda/durable/examples/general/OtelExample.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import software.amazon.lambda.durable.DurableContext;
1010
import software.amazon.lambda.durable.DurableHandler;
1111
import software.amazon.lambda.durable.examples.types.GreetingRequest;
12-
import software.amazon.lambda.durable.otel.DeterministicIdGenerator;
1312
import software.amazon.lambda.durable.otel.OpenTelemetryDurablePlugin;
1413

1514
/**
@@ -40,12 +39,8 @@ public class OtelExample extends DurableHandler<GreetingRequest, String> {
4039

4140
@Override
4241
protected DurableConfig createConfiguration() {
43-
var idGenerator = new DeterministicIdGenerator();
44-
var tracerProvider = SdkTracerProvider.builder()
45-
.setIdGenerator(idGenerator)
46-
.addSpanProcessor(SimpleSpanProcessor.create(LoggingSpanExporter.create()))
47-
.build();
48-
var otelPlugin = new OpenTelemetryDurablePlugin(tracerProvider, idGenerator);
42+
var otelPlugin = new OpenTelemetryDurablePlugin(
43+
SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(LoggingSpanExporter.create())));
4944

5045
return DurableConfig.builder().withPlugins(otelPlugin).build();
5146
}

otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OpenTelemetryDurablePlugin.java

Lines changed: 31 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
import io.opentelemetry.api.trace.TraceFlags;
1212
import io.opentelemetry.api.trace.TraceState;
1313
import io.opentelemetry.api.trace.Tracer;
14-
import io.opentelemetry.api.trace.TracerProvider;
1514
import io.opentelemetry.context.Context;
1615
import io.opentelemetry.context.Scope;
1716
import io.opentelemetry.sdk.trace.SdkTracerProvider;
17+
import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder;
1818
import java.time.Instant;
1919
import java.util.concurrent.ConcurrentHashMap;
2020
import org.slf4j.Logger;
@@ -52,17 +52,15 @@ public class OpenTelemetryDurablePlugin implements DurableExecutionPlugin {
5252
private static final Logger logger = LoggerFactory.getLogger(OpenTelemetryDurablePlugin.class);
5353
private static final String INSTRUMENTATION_NAME = "aws-durable-execution-sdk-java";
5454

55-
private final TracerProvider tracerProvider;
55+
private final SdkTracerProvider tracerProvider;
5656
private final Tracer tracer;
5757
private final DeterministicIdGenerator idGenerator;
5858
private final ContextExtractor contextExtractor;
59-
private final double samplingRate;
6059
private final boolean enableMdc;
6160

6261
// Per-invocation state
6362
private volatile Span invocationSpan;
6463
private volatile String executionArn;
65-
private volatile boolean sampled = true;
6664

6765
// Thread-safe storage for operation spans (keyed by operationId) — open spans that need ending
6866
private final ConcurrentHashMap<String, Span> operationSpans = new ConcurrentHashMap<>();
@@ -75,75 +73,59 @@ public class OpenTelemetryDurablePlugin implements DurableExecutionPlugin {
7573
private final ConcurrentHashMap<String, SpanContext> operationContexts = new ConcurrentHashMap<>();
7674

7775
/**
78-
* Creates an OTel plugin with default settings: X-Ray context extraction, 100% sampling, MDC enabled.
76+
* Creates an OTel plugin with default settings: X-Ray context extraction, MDC enabled.
7977
*
80-
* @param tracerProvider the OTel tracer provider (should use {@link DeterministicIdGenerator})
81-
* @param idGenerator the deterministic ID generator (same instance configured in the tracer provider)
78+
* <p>The plugin internally creates a {@link DeterministicIdGenerator} and overrides the tracer provider's ID
79+
* generator to ensure deterministic trace/span IDs across invocations.
80+
*
81+
* <p>Sampling is handled by the tracer provider's configured {@code Sampler}. Since trace IDs are deterministic
82+
* (same execution ARN → same trace ID), using {@code TraceIdRatioBased} sampler gives consistent per-execution
83+
* sampling across invocations.
84+
*
85+
* @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden)
8286
*/
83-
public OpenTelemetryDurablePlugin(TracerProvider tracerProvider, DeterministicIdGenerator idGenerator) {
84-
this(tracerProvider, idGenerator, new XRayContextExtractor(), 1.0, true);
87+
public OpenTelemetryDurablePlugin(SdkTracerProviderBuilder tracerProviderBuilder) {
88+
this(tracerProviderBuilder, new XRayContextExtractor(), true);
8589
}
8690

8791
/**
88-
* Creates an OTel plugin with a custom context extractor and sampling rate, MDC enabled.
92+
* Creates an OTel plugin with a custom context extractor.
8993
*
90-
* @param tracerProvider the OTel tracer provider
91-
* @param idGenerator the deterministic ID generator
94+
* @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden)
9295
* @param contextExtractor extracts parent trace context from the Lambda environment
93-
* @param samplingRate value between 0.0 and 1.0 — fraction of executions to trace
9496
*/
9597
public OpenTelemetryDurablePlugin(
96-
TracerProvider tracerProvider,
97-
DeterministicIdGenerator idGenerator,
98-
ContextExtractor contextExtractor,
99-
double samplingRate) {
100-
this(tracerProvider, idGenerator, contextExtractor, samplingRate, true);
98+
SdkTracerProviderBuilder tracerProviderBuilder, ContextExtractor contextExtractor) {
99+
this(tracerProviderBuilder, contextExtractor, true);
101100
}
102101

103102
/**
104103
* Creates an OTel plugin with full configuration.
105104
*
106-
* @param tracerProvider the OTel tracer provider
107-
* @param idGenerator the deterministic ID generator
105+
* <p>The plugin internally creates a {@link DeterministicIdGenerator} and sets it on the provided builder before
106+
* building the tracer provider. Customers configure exporters, span processors, and samplers on the builder — the
107+
* plugin handles ID generation.
108+
*
109+
* @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden)
108110
* @param contextExtractor extracts parent trace context from the Lambda environment
109-
* @param samplingRate value between 0.0 and 1.0 — fraction of executions to trace
110111
* @param enableMdc if true, injects trace_id/span_id into SLF4J MDC for log correlation
111112
*/
112113
public OpenTelemetryDurablePlugin(
113-
TracerProvider tracerProvider,
114-
DeterministicIdGenerator idGenerator,
115-
ContextExtractor contextExtractor,
116-
double samplingRate,
117-
boolean enableMdc) {
118-
this.tracerProvider = tracerProvider;
119-
this.idGenerator = idGenerator;
114+
SdkTracerProviderBuilder tracerProviderBuilder, ContextExtractor contextExtractor, boolean enableMdc) {
115+
this.idGenerator = new DeterministicIdGenerator();
116+
this.tracerProvider = tracerProviderBuilder.setIdGenerator(idGenerator).build();
120117
this.tracer = tracerProvider.get(INSTRUMENTATION_NAME);
121118
this.contextExtractor = contextExtractor;
122-
this.samplingRate = samplingRate;
123119
this.enableMdc = enableMdc;
124120
}
125121

126-
/**
127-
* Creates an OTel plugin using a pre-configured {@link SdkTracerProvider}.
128-
*
129-
* @param sdkTracerProvider the SDK tracer provider
130-
* @param idGenerator the deterministic ID generator
131-
*/
132-
public OpenTelemetryDurablePlugin(SdkTracerProvider sdkTracerProvider, DeterministicIdGenerator idGenerator) {
133-
this((TracerProvider) sdkTracerProvider, idGenerator);
134-
}
135-
136122
// ─── Invocation hooks ────────────────────────────────────────────────
137123

138124
@Override
139125
public void onInvocationStart(InvocationInfo info) {
140126
this.executionArn = info.executionArn();
141127
idGenerator.setExecutionArn(info.executionArn());
142128

143-
// Determine sampling (consistent across all invocations of same execution)
144-
this.sampled = SamplingUtil.shouldSampleExecution(info.executionArn(), samplingRate);
145-
if (!sampled) return;
146-
147129
// Extract parent context from Lambda environment (X-Ray, W3C, etc.)
148130
var extractedParentContext = contextExtractor.extract();
149131

@@ -162,7 +144,7 @@ public void onInvocationStart(InvocationInfo info) {
162144

163145
@Override
164146
public void onInvocationEnd(InvocationEndInfo info) {
165-
if (!sampled || invocationSpan == null) return;
147+
if (invocationSpan == null) return;
166148

167149
// End any operation spans that are still open (operations that didn't complete in this invocation)
168150
for (var entry : operationSpans.entrySet()) {
@@ -196,19 +178,17 @@ public void onInvocationEnd(InvocationEndInfo info) {
196178
invocationSpan = null;
197179

198180
// Flush spans before Lambda freezes
199-
if (tracerProvider instanceof SdkTracerProvider sdkProvider) {
200-
var flushResult = sdkProvider.forceFlush().join(5, java.util.concurrent.TimeUnit.SECONDS);
201-
if (!flushResult.isSuccess()) {
202-
logger.warn("OTel span flush failed or timed out — some spans may be lost");
203-
}
181+
var flushResult = tracerProvider.forceFlush().join(5, java.util.concurrent.TimeUnit.SECONDS);
182+
if (!flushResult.isSuccess()) {
183+
logger.warn("OTel span flush failed or timed out — some spans may be lost");
204184
}
205185
}
206186

207187
// ─── Operation hooks ─────────────────────────────────────────────────
208188

209189
@Override
210190
public void onOperationStart(OperationInfo info) {
211-
if (!sampled || info.id() == null) return;
191+
if (info.id() == null) return;
212192

213193
idGenerator.setNextSpanOperationId(info.id());
214194

@@ -239,7 +219,7 @@ public void onOperationStart(OperationInfo info) {
239219

240220
@Override
241221
public void onOperationEnd(OperationEndInfo info) {
242-
if (!sampled || info.id() == null) return;
222+
if (info.id() == null) return;
243223

244224
// End the operation span that was started in onOperationStart
245225
var span = operationSpans.remove(info.id());
@@ -257,7 +237,6 @@ public void onOperationEnd(OperationEndInfo info) {
257237

258238
@Override
259239
public void onUserFunctionStart(UserFunctionStartInfo info) {
260-
if (!sampled) return;
261240
var key = attemptKey(info.id(), info.attempt());
262241

263242
// Use the operation span as parent for the attempt span
@@ -295,7 +274,6 @@ public void onUserFunctionStart(UserFunctionStartInfo info) {
295274

296275
@Override
297276
public void onUserFunctionEnd(UserFunctionEndInfo info) {
298-
if (!sampled) return;
299277
var key = attemptKey(info.id(), info.attempt());
300278

301279
// Close scope first (must happen on same thread as makeCurrent)

otel-plugin/src/main/java/software/amazon/lambda/durable/otel/SamplingUtil.java

Lines changed: 0 additions & 55 deletions
This file was deleted.

otel-plugin/src/test/java/software/amazon/lambda/durable/otel/MdcSpanEnricherTest.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,11 @@ void inject_withNullArn_doesNotSetArn() {
5454
void plugin_withMdcEnabled_setsArnInMdc() {
5555
// Test MDC through the full plugin lifecycle (where makeCurrent is called on same thread)
5656
var spanExporter = InMemorySpanExporter.create();
57-
var idGenerator = new DeterministicIdGenerator();
58-
var tracerProvider = SdkTracerProvider.builder()
59-
.setIdGenerator(idGenerator)
60-
.addSpanProcessor(SimpleSpanProcessor.create(spanExporter))
61-
.build();
6257

6358
var plugin = new OpenTelemetryDurablePlugin(
64-
tracerProvider, idGenerator, () -> io.opentelemetry.context.Context.root(), 1.0, true);
59+
SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(spanExporter)),
60+
() -> io.opentelemetry.context.Context.root(),
61+
true);
6562

6663
plugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec-mdc-test", true));
6764

otel-plugin/src/test/java/software/amazon/lambda/durable/otel/OpenTelemetryDurablePluginTest.java

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,15 @@ class OpenTelemetryDurablePluginTest {
1717

1818
private InMemorySpanExporter spanExporter;
1919
private OpenTelemetryDurablePlugin plugin;
20-
private DeterministicIdGenerator idGenerator;
2120

2221
@BeforeEach
2322
void setUp() {
2423
spanExporter = InMemorySpanExporter.create();
25-
idGenerator = new DeterministicIdGenerator();
26-
27-
var tracerProvider = SdkTracerProvider.builder()
28-
.setIdGenerator(idGenerator)
29-
.addSpanProcessor(SimpleSpanProcessor.create(spanExporter))
30-
.build();
3124

3225
plugin = new OpenTelemetryDurablePlugin(
33-
tracerProvider, idGenerator, () -> io.opentelemetry.context.Context.root(), 1.0, false);
26+
SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(spanExporter)),
27+
() -> io.opentelemetry.context.Context.root(),
28+
false);
3429
}
3530

3631
@Test
@@ -215,16 +210,13 @@ void operationNotCompleted_spanEndedAtInvocationEnd() {
215210
}
216211

217212
@Test
218-
void sampling_disabledExecution_producesNoSpans() {
213+
void sampling_disabled_producesNoSpans() {
219214
spanExporter = InMemorySpanExporter.create();
220215
var sampledPlugin = new OpenTelemetryDurablePlugin(
221216
SdkTracerProvider.builder()
222-
.setIdGenerator(idGenerator)
223-
.addSpanProcessor(SimpleSpanProcessor.create(spanExporter))
224-
.build(),
225-
idGenerator,
217+
.setSampler(io.opentelemetry.sdk.trace.samplers.Sampler.alwaysOff())
218+
.addSpanProcessor(SimpleSpanProcessor.create(spanExporter)),
226219
() -> io.opentelemetry.context.Context.root(),
227-
0.0, // 0% sampling — nothing should be traced
228220
false);
229221

230222
sampledPlugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec1", true));

0 commit comments

Comments
 (0)