From 3a1ce873b06fad21e50f8dd4fd3ea9ac0646a20a Mon Sep 17 00:00:00 2001 From: silanhe Date: Wed, 5 Aug 2026 17:18:57 +0000 Subject: [PATCH 1/7] feat(otel): make instrumentation scope name configurable Both ExecutionOtelPlugin and InvocationOtelPlugin hardcoded the tracer's instrumentation scope name as "aws-durable-execution-sdk-java", unlike the JS (instrumentationName) and Python (instrument_name) plugins which expose it. Add a new fullest public constructor overload on each plugin that accepts an instrumentationName parameter; the existing 4-arg constructor delegates to it with the INSTRUMENTATION_NAME default (non-breaking). A null value falls back to the default. The no-arg ADOT constructor keeps the default (zero-config path); custom scope names are set via the builder constructors. No instance field is needed -- the scope name is only used at tracer creation (.get(...)), so it is resolved locally in the constructor. Tests: +1 per plugin asserting the exported spans' instrumentation scope name equals the custom value (Invocation 50, Execution 31). spotless clean. --- .../durable/otel/ExecutionOtelPlugin.java | 21 ++++++++++++++++++- .../durable/otel/InvocationOtelPlugin.java | 21 ++++++++++++++++++- .../durable/otel/ExecutionOtelPluginTest.java | 19 +++++++++++++++++ .../otel/InvocationOtelPluginTest.java | 20 ++++++++++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java index 885ec1dd8..743009300 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java @@ -158,6 +158,25 @@ public ExecutionOtelPlugin( ContextExtractor contextExtractor, boolean enableMdc, String workflowSpanName) { + this(tracerProviderBuilder, contextExtractor, enableMdc, workflowSpanName, INSTRUMENTATION_NAME); + } + + /** + * Creates an OTel plugin with full configuration, including a custom instrumentation scope name. + * + * @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden) + * @param contextExtractor extracts parent trace context from the Lambda environment + * @param enableMdc if true, injects traceId/spanId/otelTraceSampled into SLF4J MDC for log correlation + * @param workflowSpanName the name for the Workflow root span + * @param instrumentationName the instrumentation scope name registered with the tracer (defaults to + * "aws-durable-execution-sdk-java" when null) + */ + public ExecutionOtelPlugin( + SdkTracerProviderBuilder tracerProviderBuilder, + ContextExtractor contextExtractor, + boolean enableMdc, + String workflowSpanName, + String instrumentationName) { this.idGenerator = new DeterministicIdGenerator(); // Set service.name so this plugin's spans group under a distinct "workflow" node in X-Ray/OTLP backends. @@ -166,7 +185,7 @@ public ExecutionOtelPlugin( this.sdkTracerProvider = tracerProviderBuilder.setIdGenerator(idGenerator).build(); - this.tracer = sdkTracerProvider.get(INSTRUMENTATION_NAME); + this.tracer = sdkTracerProvider.get(instrumentationName != null ? instrumentationName : INSTRUMENTATION_NAME); this.contextExtractor = contextExtractor; this.enableMdc = enableMdc; this.workflowSpanName = workflowSpanName != null ? workflowSpanName : DEFAULT_WORKFLOW_SPAN_NAME; diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java index aaae21e9d..35f911d67 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java @@ -185,11 +185,30 @@ public InvocationOtelPlugin( ContextExtractor contextExtractor, boolean enableMdc, String workflowSpanName) { + this(tracerProviderBuilder, contextExtractor, enableMdc, workflowSpanName, INSTRUMENTATION_NAME); + } + + /** + * Creates an OTel plugin with full configuration, including a custom instrumentation scope name. + * + * @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden) + * @param contextExtractor extracts parent trace context from the Lambda environment + * @param enableMdc if true, injects traceId/spanId/otelTraceSampled into SLF4J MDC for log correlation + * @param workflowSpanName the name for the Workflow span + * @param instrumentationName the instrumentation scope name registered with the tracer (defaults to + * "aws-durable-execution-sdk-java" when null) + */ + public InvocationOtelPlugin( + SdkTracerProviderBuilder tracerProviderBuilder, + ContextExtractor contextExtractor, + boolean enableMdc, + String workflowSpanName, + String instrumentationName) { this.idGenerator = new DeterministicIdGenerator(); this.sdkTracerProvider = tracerProviderBuilder.setIdGenerator(idGenerator).build(); - this.tracer = sdkTracerProvider.get(INSTRUMENTATION_NAME); + this.tracer = sdkTracerProvider.get(instrumentationName != null ? instrumentationName : INSTRUMENTATION_NAME); this.contextExtractor = contextExtractor; this.enableMdc = enableMdc; this.workflowSpanName = workflowSpanName != null ? workflowSpanName : DEFAULT_WORKFLOW_SPAN_NAME; diff --git a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java index ed6533e05..d4c838f2b 100644 --- a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java +++ b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java @@ -45,6 +45,25 @@ void tearDown() { // ─── Default constructor ───────────────────────────────────────────── + @Test + void customInstrumentationName_isUsedForTracerScope() { + var exporter = InMemorySpanExporter.create(); + var customPlugin = new ExecutionOtelPlugin( + SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)), + () -> null, + false, + "Workflow", + "my-custom-scope"); + customPlugin.onInvocationStart(new InvocationInfo("req-1", ARN, true, Instant.now())); + customPlugin.onInvocationEnd(new InvocationEndInfo("req-1", ARN, true, InvocationStatus.SUCCEEDED, null)); + + var spans = exporter.getFinishedSpanItems(); + assertFalse(spans.isEmpty()); + for (var span : spans) { + assertEquals("my-custom-scope", span.getInstrumentationScopeInfo().getName()); + } + } + @Test void defaultConstructor_throwsWhenAutoConfigurationCustomizerProviderIsNotInstalled() { GlobalOpenTelemetry.resetForTest(); diff --git a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java index 1d9c5eb9d..80966d8fe 100644 --- a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java +++ b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java @@ -235,6 +235,26 @@ void invocationStart_and_end_createsSpan() { assertEquals(StatusCode.OK, span.getStatus().getStatusCode()); } + @Test + void customInstrumentationName_isUsedForTracerScope() { + var exporter = InMemorySpanExporter.create(); + var customPlugin = new InvocationOtelPlugin( + SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)), + () -> null, + false, + "Workflow", + "my-custom-scope"); + customPlugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec1", true, Instant.now())); + customPlugin.onInvocationEnd( + new InvocationEndInfo("req-1", "arn:exec1", true, InvocationStatus.SUCCEEDED, null)); + + var spans = exporter.getFinishedSpanItems(); + assertFalse(spans.isEmpty()); + for (var span : spans) { + assertEquals("my-custom-scope", span.getInstrumentationScopeInfo().getName()); + } + } + @Test void invocationSpan_hasInternalKind() { plugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec1", true, Instant.now())); From 298cdb7e47e3a9d82b5ce29727ef39babc89387b Mon Sep 17 00:00:00 2001 From: silanhe Date: Wed, 5 Aug 2026 20:40:47 +0000 Subject: [PATCH 2/7] refactor(otel): replace telescoping constructors with OtelPluginConfig builder Both InvocationOtelPlugin and ExecutionOtelPlugin used a chain of positional constructor overloads (up to 5 args). Adding each new knob (most recently instrumentationName) meant another overload on both plugins and increasingly ambiguous call sites (two String args, a boolean). This introduces an immutable OtelPluginConfig value object with a named-field builder and collapses the overloads to a single (SdkTracerProviderBuilder, OtelPluginConfig) constructor per plugin. - New OtelPluginConfig with builder: contextExtractor, enableMdc, workflowSpanName, instrumentationName (null-safe defaults). - Kept no-arg ADOT and single-builder convenience constructors; removed the 2/3/4/5-arg telescoping constructors. - Matches the OtelPluginConfig object in the JS and Python SDKs (cross-SDK parity) and is forward-compatible: future options are builder methods, not new constructors. - Migrated all test call sites and updated the README + examples docs. InvocationOtelPluginTest 50, ExecutionOtelPluginTest 31, integration 17, MdcSpanEnricherTest 3 all pass; examples compile; spotless clean. --- otel-plugin/README.md | 73 ++++----- .../durable/otel/ExecutionOtelPlugin.java | 58 ++----- .../durable/otel/InvocationOtelPlugin.java | 69 ++------ .../lambda/durable/otel/OtelPluginConfig.java | 148 ++++++++++++++++++ .../durable/otel/ExecutionOtelPluginTest.java | 42 +++-- .../InvocationOtelPluginIntegrationTest.java | 12 +- .../otel/InvocationOtelPluginTest.java | 79 ++++++---- .../durable/otel/MdcSpanEnricherTest.java | 6 +- 8 files changed, 304 insertions(+), 183 deletions(-) create mode 100644 otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java diff --git a/otel-plugin/README.md b/otel-plugin/README.md index 8dfaa8d5d..76cd2f96e 100644 --- a/otel-plugin/README.md +++ b/otel-plugin/README.md @@ -215,21 +215,9 @@ With Lambda's `LoggingConfig: JSON` (required for durable functions), CloudWatch ## Configuration -### Constructor Options - -```java -// Default: ADOT Java agent global provider, X-Ray context extraction, MDC enabled -new InvocationOtelPlugin(); - -// Custom tracer provider pipeline -new InvocationOtelPlugin(tracerProviderBuilder); - -// Custom context extractor, MDC enabled -new InvocationOtelPlugin(tracerProviderBuilder, contextExtractor); - -// Full configuration -new InvocationOtelPlugin(tracerProviderBuilder, contextExtractor, enableMdc); -``` +Both plugins take a required `SdkTracerProviderBuilder` (your exporter/processor pipeline) plus an optional +`OtelPluginConfig` built with a named-field builder. This replaces the older telescoping constructors, giving readable, +type-safe call sites, and matches the `OtelPluginConfig` object in the JavaScript and Python SDKs. ### InvocationOtelPlugin @@ -237,46 +225,53 @@ new InvocationOtelPlugin(tracerProviderBuilder, contextExtractor, enableMdc); // Default: ADOT Java agent global provider, X-Ray context extraction, MDC enabled new InvocationOtelPlugin(); -// Custom tracer provider pipeline +// Custom tracer provider pipeline, all other options defaulted new InvocationOtelPlugin(tracerProviderBuilder); -// Custom context extractor, MDC enabled -new InvocationOtelPlugin(tracerProviderBuilder, contextExtractor); - -// Full configuration -new InvocationOtelPlugin(tracerProviderBuilder, contextExtractor, enableMdc); +// Full configuration via the builder +new InvocationOtelPlugin( + tracerProviderBuilder, + OtelPluginConfig.builder() + .contextExtractor(new XRayContextExtractor()) + .enableMdc(true) + .workflowSpanName("Workflow") + .instrumentationName("aws-durable-execution-sdk-java") + .build()); ``` -| Parameter | Description | Default | -|-----------|-------------|---------| -| `tracerProviderBuilder` | `SdkTracerProviderBuilder` with your exporter/processor configured | Not used by `new InvocationOtelPlugin()`; the default constructor uses the ADOT Java agent provider | -| `contextExtractor` | Extracts parent trace context from the Lambda environment | `XRayContextExtractor` | -| `enableMdc` | If true, injects `trace_id`/`span_id`/`traceSampled` into SLF4J MDC | `true` | - ### ExecutionOtelPlugin -The `ExecutionOtelPlugin` renders the Workflow span as the trace root with operations as siblings of the invocation span. It supports the same constructor options: +The `ExecutionOtelPlugin` renders the Workflow span as the trace root with operations as siblings of the invocation +span. It takes the same `(SdkTracerProviderBuilder, OtelPluginConfig)` constructor: ```java // Default: ADOT Java agent global provider, X-Ray context extraction, MDC enabled new ExecutionOtelPlugin(); -// Custom tracer provider pipeline +// Custom tracer provider pipeline, all other options defaulted new ExecutionOtelPlugin(tracerProviderBuilder); -// Custom context extractor, MDC enabled -new ExecutionOtelPlugin(tracerProviderBuilder, contextExtractor); - -// Full configuration -new ExecutionOtelPlugin(tracerProviderBuilder, contextExtractor, enableMdc, workflowSpanName); +// Full configuration via the builder +new ExecutionOtelPlugin( + tracerProviderBuilder, + OtelPluginConfig.builder() + .enableMdc(false) + .workflowSpanName("Workflow") + .build()); ``` -| Parameter | Description | Default | +### OtelPluginConfig options + +| Builder method | Description | Default | |-----------|-------------|---------| -| `tracerProviderBuilder` | `SdkTracerProviderBuilder` with your exporter/processor configured | Not used by `new ExecutionOtelPlugin()`; the default constructor uses the ADOT Java agent provider | -| `contextExtractor` | Extracts parent trace context from the Lambda environment | `XRayContextExtractor` | -| `enableMdc` | If true, injects `trace_id`/`span_id`/`traceSampled` into SLF4J MDC | `true` | -| `workflowSpanName` | Name for the Workflow root span | `"Workflow"` | +| `contextExtractor(...)` | Extracts parent trace context from the Lambda environment | `new XRayContextExtractor()` | +| `enableMdc(...)` | If true, injects `trace_id`/`span_id`/`traceSampled` into SLF4J MDC | `true` | +| `workflowSpanName(...)` | Name for the Workflow span | `"Workflow"` | +| `instrumentationName(...)` | Instrumentation scope name registered with the tracer | `"aws-durable-execution-sdk-java"` | + +> The `tracerProviderBuilder` argument is not used by the no-arg `new InvocationOtelPlugin()` / +> `new ExecutionOtelPlugin()` constructors; those use the ADOT Java agent's global provider. A `null` passed to any +> `OtelPluginConfig` builder setter falls back to that option's default. ## Known Limitations diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java index cd9f9e670..fa4aaac0a 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java @@ -117,7 +117,7 @@ public class ExecutionOtelPlugin implements DurableExecutionPlugin { * @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden) */ public ExecutionOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder) { - this(tracerProviderBuilder, new XRayContextExtractor(), true, DEFAULT_WORKFLOW_SPAN_NAME); + this(tracerProviderBuilder, OtelPluginConfig.defaults()); } /** @@ -131,56 +131,30 @@ public ExecutionOtelPlugin() { } /** - * Creates a Workflow-rooted OTel plugin with a custom context extractor, MDC enabled, root span named - * {@code "Workflow"}. + * Creates a Workflow-rooted OTel plugin from the given tracer provider builder and configuration. * - * @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden) - * @param contextExtractor extracts parent trace context from the Lambda environment - */ - public ExecutionOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder, ContextExtractor contextExtractor) { - this(tracerProviderBuilder, contextExtractor, true, DEFAULT_WORKFLOW_SPAN_NAME); - } - - /** - * Creates a Workflow-rooted OTel plugin with full configuration. + *

Customers configure exporters and span processors on the builder; all other tunables (context extractor, MDC + * toggle, Workflow span name, instrumentation scope name) come from {@link OtelPluginConfig}. Use + * {@link OtelPluginConfig#builder()} for readable, named configuration: * - * @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden) - * @param contextExtractor extracts parent trace context from the Lambda environment - * @param enableMdc if true, injects traceId/spanId/otelTraceSampled into SLF4J MDC for log correlation - * @param workflowSpanName the name for the Workflow root span - */ - public ExecutionOtelPlugin( - SdkTracerProviderBuilder tracerProviderBuilder, - ContextExtractor contextExtractor, - boolean enableMdc, - String workflowSpanName) { - this(tracerProviderBuilder, contextExtractor, enableMdc, workflowSpanName, INSTRUMENTATION_NAME); - } - - /** - * Creates an OTel plugin with full configuration, including a custom instrumentation scope name. + *

{@code
+     * var plugin = new ExecutionOtelPlugin(
+     *     SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)),
+     *     OtelPluginConfig.builder().enableMdc(false).workflowSpanName("Workflow").build());
+     * }
* * @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden) - * @param contextExtractor extracts parent trace context from the Lambda environment - * @param enableMdc if true, injects traceId/spanId/otelTraceSampled into SLF4J MDC for log correlation - * @param workflowSpanName the name for the Workflow root span - * @param instrumentationName the instrumentation scope name registered with the tracer (defaults to - * "aws-durable-execution-sdk-java" when null) + * @param config the plugin configuration */ - public ExecutionOtelPlugin( - SdkTracerProviderBuilder tracerProviderBuilder, - ContextExtractor contextExtractor, - boolean enableMdc, - String workflowSpanName, - String instrumentationName) { + public ExecutionOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder, OtelPluginConfig config) { this.idGenerator = new DeterministicIdGenerator(); this.sdkTracerProvider = tracerProviderBuilder.setIdGenerator(idGenerator).build(); - this.tracer = sdkTracerProvider.get(instrumentationName != null ? instrumentationName : INSTRUMENTATION_NAME); - this.contextExtractor = contextExtractor; - this.enableMdc = enableMdc; - this.workflowSpanName = workflowSpanName != null ? workflowSpanName : DEFAULT_WORKFLOW_SPAN_NAME; + this.tracer = sdkTracerProvider.get(config.instrumentationName()); + this.contextExtractor = config.contextExtractor(); + this.enableMdc = config.enableMdc(); + this.workflowSpanName = config.workflowSpanName(); } private ExecutionOtelPlugin(TracerProvider tracerProvider, DeterministicIdGenerator idGenerator) { diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java index 829501ac7..1f51dd254 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java @@ -137,7 +137,7 @@ public class InvocationOtelPlugin implements DurableExecutionPlugin { * @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden) */ public InvocationOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder) { - this(tracerProviderBuilder, new XRayContextExtractor(), true); + this(tracerProviderBuilder, OtelPluginConfig.defaults()); } /** @@ -151,67 +151,30 @@ public InvocationOtelPlugin() { } /** - * Creates an OTel plugin with a custom context extractor, MDC enabled. + * Creates an OTel plugin from the given tracer provider builder and configuration. * - * @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden) - * @param contextExtractor extracts parent trace context from the Lambda environment - */ - public InvocationOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder, ContextExtractor contextExtractor) { - this(tracerProviderBuilder, contextExtractor, true); - } - - /** - * Creates an OTel plugin with the given context extractor and MDC setting, using the default Workflow span name. + *

Customers configure exporters and span processors on the builder; all other tunables (context extractor, MDC + * toggle, Workflow span name, instrumentation scope name) come from {@link OtelPluginConfig}. Use + * {@link OtelPluginConfig#builder()} for readable, named configuration: * - * @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden) - * @param contextExtractor extracts parent trace context from the Lambda environment - * @param enableMdc if true, injects traceId/spanId/otelTraceSampled into SLF4J MDC for log correlation - */ - public InvocationOtelPlugin( - SdkTracerProviderBuilder tracerProviderBuilder, ContextExtractor contextExtractor, boolean enableMdc) { - this(tracerProviderBuilder, contextExtractor, enableMdc, DEFAULT_WORKFLOW_SPAN_NAME); - } - - /** - * Creates an OTel plugin with full configuration. - * - * @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden) - * @param contextExtractor extracts parent trace context from the Lambda environment - * @param enableMdc if true, injects traceId/spanId/otelTraceSampled into SLF4J MDC for log correlation - * @param workflowSpanName the name for the Workflow span - */ - public InvocationOtelPlugin( - SdkTracerProviderBuilder tracerProviderBuilder, - ContextExtractor contextExtractor, - boolean enableMdc, - String workflowSpanName) { - this(tracerProviderBuilder, contextExtractor, enableMdc, workflowSpanName, INSTRUMENTATION_NAME); - } - - /** - * Creates an OTel plugin with full configuration, including a custom instrumentation scope name. + *

{@code
+     * var plugin = new InvocationOtelPlugin(
+     *     SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)),
+     *     OtelPluginConfig.builder().enableMdc(false).workflowSpanName("Workflow").build());
+     * }
* * @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden) - * @param contextExtractor extracts parent trace context from the Lambda environment - * @param enableMdc if true, injects traceId/spanId/otelTraceSampled into SLF4J MDC for log correlation - * @param workflowSpanName the name for the Workflow span - * @param instrumentationName the instrumentation scope name registered with the tracer (defaults to - * "aws-durable-execution-sdk-java" when null) + * @param config the plugin configuration */ - public InvocationOtelPlugin( - SdkTracerProviderBuilder tracerProviderBuilder, - ContextExtractor contextExtractor, - boolean enableMdc, - String workflowSpanName, - String instrumentationName) { + public InvocationOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder, OtelPluginConfig config) { this.idGenerator = new DeterministicIdGenerator(); this.sdkTracerProvider = tracerProviderBuilder.setIdGenerator(idGenerator).build(); - this.tracer = sdkTracerProvider.get(instrumentationName != null ? instrumentationName : INSTRUMENTATION_NAME); - this.contextExtractor = contextExtractor; - this.enableMdc = enableMdc; - this.workflowSpanName = workflowSpanName != null ? workflowSpanName : DEFAULT_WORKFLOW_SPAN_NAME; + this.tracer = sdkTracerProvider.get(config.instrumentationName()); + this.contextExtractor = config.contextExtractor(); + this.enableMdc = config.enableMdc(); + this.workflowSpanName = config.workflowSpanName(); } private InvocationOtelPlugin(TracerProvider tracerProvider, DeterministicIdGenerator idGenerator) { diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java new file mode 100644 index 000000000..d46659493 --- /dev/null +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java @@ -0,0 +1,148 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.lambda.durable.otel; + +/** + * Immutable configuration for {@link InvocationOtelPlugin} and {@link ExecutionOtelPlugin}. + * + *

Replaces the previous telescoping constructor overloads with a single named-field builder, giving readable, + * type-safe call sites and forward compatibility (new options are added as builder methods, not new constructors). This + * mirrors the {@code OtelPluginConfig} object in the JavaScript SDK and the {@code OtelPluginConfig} dataclass in the + * Python SDK for cross-SDK parity. + * + *

Construct via {@link #builder()} and pass to a plugin's {@code (SdkTracerProviderBuilder, OtelPluginConfig)} + * constructor: + * + *

{@code
+ * var config = OtelPluginConfig.builder()
+ *     .contextExtractor(new XRayContextExtractor())
+ *     .enableMdc(true)
+ *     .workflowSpanName("Workflow")
+ *     .instrumentationName("my-scope")
+ *     .build();
+ * var plugin = new InvocationOtelPlugin(tracerProviderBuilder, config);
+ * }
+ * + *

Defaults: {@code contextExtractor = new XRayContextExtractor()}, {@code enableMdc = true}, {@code workflowSpanName + * = "Workflow"}, {@code instrumentationName = "aws-durable-execution-sdk-java"}. A {@code null} passed to any builder + * setter falls back to the corresponding default. + * + * @deprecated This is a preview API that is experimental and may be changed or removed in future releases. + */ +@Deprecated +public final class OtelPluginConfig { + + static final String DEFAULT_INSTRUMENTATION_NAME = "aws-durable-execution-sdk-java"; + static final String DEFAULT_WORKFLOW_SPAN_NAME = "Workflow"; + + private final ContextExtractor contextExtractor; + private final boolean enableMdc; + private final String workflowSpanName; + private final String instrumentationName; + + private OtelPluginConfig(Builder builder) { + this.contextExtractor = + builder.contextExtractor != null ? builder.contextExtractor : new XRayContextExtractor(); + this.enableMdc = builder.enableMdc; + this.workflowSpanName = + builder.workflowSpanName != null ? builder.workflowSpanName : DEFAULT_WORKFLOW_SPAN_NAME; + this.instrumentationName = + builder.instrumentationName != null ? builder.instrumentationName : DEFAULT_INSTRUMENTATION_NAME; + } + + /** Returns a new builder with all fields defaulted. */ + public static Builder builder() { + return new Builder(); + } + + /** Returns a config with all default values. */ + public static OtelPluginConfig defaults() { + return new Builder().build(); + } + + /** The context extractor used to read parent trace context from the Lambda environment. */ + public ContextExtractor contextExtractor() { + return contextExtractor; + } + + /** Whether traceId/spanId/otelTraceSampled are injected into the SLF4J MDC for log correlation. */ + public boolean enableMdc() { + return enableMdc; + } + + /** The name used for the Workflow span. */ + public String workflowSpanName() { + return workflowSpanName; + } + + /** The instrumentation scope name registered with the tracer. */ + public String instrumentationName() { + return instrumentationName; + } + + /** + * Builder for {@link OtelPluginConfig}. + * + * @deprecated This is a preview API that is experimental and may be changed or removed in future releases. + */ + @Deprecated + public static final class Builder { + + private ContextExtractor contextExtractor; + private boolean enableMdc = true; + private String workflowSpanName; + private String instrumentationName; + + private Builder() {} + + /** + * Sets the context extractor. Defaults to {@link XRayContextExtractor} when null. + * + * @param contextExtractor extracts parent trace context from the Lambda environment + * @return this builder + */ + public Builder contextExtractor(ContextExtractor contextExtractor) { + this.contextExtractor = contextExtractor; + return this; + } + + /** + * Sets whether to inject traceId/spanId/otelTraceSampled into the SLF4J MDC. Defaults to {@code true}. + * + * @param enableMdc if true, enables MDC log correlation + * @return this builder + */ + public Builder enableMdc(boolean enableMdc) { + this.enableMdc = enableMdc; + return this; + } + + /** + * Sets the Workflow span name. Defaults to {@code "Workflow"} when null. + * + * @param workflowSpanName the name for the Workflow span + * @return this builder + */ + public Builder workflowSpanName(String workflowSpanName) { + this.workflowSpanName = workflowSpanName; + return this; + } + + /** + * Sets the instrumentation scope name registered with the tracer. Defaults to + * {@code "aws-durable-execution-sdk-java"} when null. + * + * @param instrumentationName the instrumentation scope name + * @return this builder + */ + public Builder instrumentationName(String instrumentationName) { + this.instrumentationName = instrumentationName; + return this; + } + + /** Builds an immutable {@link OtelPluginConfig}. */ + public OtelPluginConfig build() { + return new OtelPluginConfig(this); + } + } +} diff --git a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java index f9b5cafe7..71c9924ba 100644 --- a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java +++ b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java @@ -39,9 +39,11 @@ void setUp() { SdkTracerProvider.builder() .setResource(resource) .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)), - () -> null, - false, - "Workflow"); + OtelPluginConfig.builder() + .contextExtractor(() -> null) + .enableMdc(false) + .workflowSpanName("Workflow") + .build()); } @AfterEach @@ -58,10 +60,12 @@ void customInstrumentationName_isUsedForTracerScope() { var exporter = InMemorySpanExporter.create(); var customPlugin = new ExecutionOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)), - () -> null, - false, - "Workflow", - "my-custom-scope"); + OtelPluginConfig.builder() + .contextExtractor(() -> null) + .enableMdc(false) + .workflowSpanName("Workflow") + .instrumentationName("my-custom-scope") + .build()); customPlugin.onInvocationStart(new InvocationInfo("req-1", ARN, true, Instant.now())); customPlugin.onInvocationEnd(new InvocationEndInfo("req-1", ARN, true, InvocationStatus.SUCCEEDED, null)); @@ -616,9 +620,11 @@ void deterministicWorkflowSpanId_stableAcrossInvocations() { var exporter2 = InMemorySpanExporter.create(); var plugin2 = new ExecutionOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter2)), - () -> null, - false, - "Workflow"); + OtelPluginConfig.builder() + .contextExtractor(() -> null) + .enableMdc(false) + .workflowSpanName("Workflow") + .build()); plugin2.onInvocationStart(new InvocationInfo("req-9", ARN, true, Instant.now())); plugin2.onInvocationEnd(new InvocationEndInfo("req-9", ARN, true, InvocationStatus.SUCCEEDED, null)); var secondWorkflowSpanId = @@ -639,9 +645,11 @@ void sampling_disabled_producesNoSpans() { SdkTracerProvider.builder() .setSampler(io.opentelemetry.sdk.trace.samplers.Sampler.alwaysOff()) .addSpanProcessor(SimpleSpanProcessor.create(exporter)), - () -> null, - false, - "Workflow"); + OtelPluginConfig.builder() + .contextExtractor(() -> null) + .enableMdc(false) + .workflowSpanName("Workflow") + .build()); sampledPlugin.onInvocationStart(new InvocationInfo("req-1", ARN, true, Instant.now())); sampledPlugin.onInvocationEnd(new InvocationEndInfo("req-1", ARN, true, InvocationStatus.SUCCEEDED, null)); assertTrue(exporter.getFinishedSpanItems().isEmpty(), "No spans should be exported with 0% sampling"); @@ -655,9 +663,11 @@ void xrayExtraction_allSpansShareExtractedTraceId() { var exporter = InMemorySpanExporter.create(); var xrayPlugin = new ExecutionOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)), - () -> new ExtractedContext(xrayTraceId, null), - false, - "Workflow"); + OtelPluginConfig.builder() + .contextExtractor(() -> new ExtractedContext(xrayTraceId, null)) + .enableMdc(false) + .workflowSpanName("Workflow") + .build()); xrayPlugin.onInvocationStart(new InvocationInfo("req-1", ARN, true, Instant.now())); xrayPlugin.onOperationStart( new OperationInfo("op-1", "step-a", "STEP", "Step", null, Instant.now(), null, false)); diff --git a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginIntegrationTest.java b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginIntegrationTest.java index d4e85c241..2266b5a94 100644 --- a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginIntegrationTest.java +++ b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginIntegrationTest.java @@ -46,8 +46,10 @@ void setUp() { var plugin = new InvocationOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(spanExporter)), - () -> null, - false); + OtelPluginConfig.builder() + .contextExtractor(() -> null) + .enableMdc(false) + .build()); otelConfig = DurableConfig.builder().withPlugins(plugin).build(); } @@ -326,8 +328,10 @@ void sampling_off_producesNoSpans() { SdkTracerProvider.builder() .setSampler(Sampler.alwaysOff()) .addSpanProcessor(SimpleSpanProcessor.create(sampledExporter)), - () -> null, - false); + OtelPluginConfig.builder() + .contextExtractor(() -> null) + .enableMdc(false) + .build()); var noSampleConfig = DurableConfig.builder().withPlugins(noSamplePlugin).build(); diff --git a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java index 80966d8fe..67957b139 100644 --- a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java +++ b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java @@ -51,8 +51,10 @@ void setUp() { plugin = new InvocationOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(spanExporter)), - () -> null, - false); + OtelPluginConfig.builder() + .contextExtractor(() -> null) + .enableMdc(false) + .build()); } @AfterEach @@ -240,10 +242,12 @@ void customInstrumentationName_isUsedForTracerScope() { var exporter = InMemorySpanExporter.create(); var customPlugin = new InvocationOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)), - () -> null, - false, - "Workflow", - "my-custom-scope"); + OtelPluginConfig.builder() + .contextExtractor(() -> null) + .enableMdc(false) + .workflowSpanName("Workflow") + .instrumentationName("my-custom-scope") + .build()); customPlugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec1", true, Instant.now())); customPlugin.onInvocationEnd( new InvocationEndInfo("req-1", "arn:exec1", true, InvocationStatus.SUCCEEDED, null)); @@ -671,8 +675,10 @@ void sampling_disabled_producesNoSpans() { SdkTracerProvider.builder() .setSampler(io.opentelemetry.sdk.trace.samplers.Sampler.alwaysOff()) .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)), - () -> null, - false); + OtelPluginConfig.builder() + .contextExtractor(() -> null) + .enableMdc(false) + .build()); sampledPlugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec1", true, Instant.now())); sampledPlugin.onUserFunctionStart( @@ -697,8 +703,10 @@ void xrayExtraction_usesExtractedTraceId_overArnDerived() { spanExporter = InMemorySpanExporter.create(); var xrayPlugin = new InvocationOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(spanExporter)), - () -> extractedContext, - false); + OtelPluginConfig.builder() + .contextExtractor(() -> extractedContext) + .enableMdc(false) + .build()); xrayPlugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec1", true, Instant.now())); xrayPlugin.onInvocationEnd(new InvocationEndInfo("req-1", "arn:exec1", true, InvocationStatus.SUCCEEDED, null)); @@ -716,8 +724,10 @@ void xrayExtraction_allSpansShareExtractedTraceId() { spanExporter = InMemorySpanExporter.create(); var xrayPlugin = new InvocationOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(spanExporter)), - () -> extractedContext, - false); + OtelPluginConfig.builder() + .contextExtractor(() -> extractedContext) + .enableMdc(false) + .build()); xrayPlugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec1", true, Instant.now())); xrayPlugin.onOperationStart( @@ -746,8 +756,10 @@ void xrayExtraction_withParentSpanId_invocationSpanHasCorrectParent() { spanExporter = InMemorySpanExporter.create(); var xrayPlugin = new InvocationOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(spanExporter)), - () -> extractedContext, - false); + OtelPluginConfig.builder() + .contextExtractor(() -> extractedContext) + .enableMdc(false) + .build()); xrayPlugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec1", true, Instant.now())); xrayPlugin.onInvocationEnd(new InvocationEndInfo("req-1", "arn:exec1", true, InvocationStatus.SUCCEEDED, null)); @@ -771,8 +783,10 @@ void xrayExtraction_withoutParentSpanId_invocationSpanIsRoot() { spanExporter = InMemorySpanExporter.create(); var xrayPlugin = new InvocationOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(spanExporter)), - () -> extractedContext, - false); + OtelPluginConfig.builder() + .contextExtractor(() -> extractedContext) + .enableMdc(false) + .build()); xrayPlugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec1", true, Instant.now())); xrayPlugin.onInvocationEnd(new InvocationEndInfo("req-1", "arn:exec1", true, InvocationStatus.SUCCEEDED, null)); @@ -801,8 +815,10 @@ void xrayExtraction_multipleInvocations_sameTraceId_unifiedTrace() { spanExporter = InMemorySpanExporter.create(); var xrayPlugin = new InvocationOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(spanExporter)), - () -> extractedContext, - false); + OtelPluginConfig.builder() + .contextExtractor(() -> extractedContext) + .enableMdc(false) + .build()); // First invocation xrayPlugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec1", true, Instant.now())); @@ -835,8 +851,10 @@ void xrayExtraction_nullExtractor_fallsBackToArnDerived() { spanExporter = InMemorySpanExporter.create(); var noXrayPlugin = new InvocationOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(spanExporter)), - () -> null, - false); + OtelPluginConfig.builder() + .contextExtractor(() -> null) + .enableMdc(false) + .build()); var arn = "arn:aws:lambda:us-east-1:123:function:test:$LATEST/durable/exec1"; noXrayPlugin.onInvocationStart(new InvocationInfo("req-1", arn, true, Instant.now())); @@ -866,8 +884,10 @@ void xrayExtraction_extractedTraceIdMatchesXrayConversion() { spanExporter = InMemorySpanExporter.create(); var xrayPlugin = new InvocationOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(spanExporter)), - () -> extractedContext, - false); + OtelPluginConfig.builder() + .contextExtractor(() -> extractedContext) + .enableMdc(false) + .build()); xrayPlugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec1", true, Instant.now())); xrayPlugin.onInvocationEnd(new InvocationEndInfo("req-1", "arn:exec1", true, InvocationStatus.SUCCEEDED, null)); @@ -1301,8 +1321,11 @@ void operationLinksToWorkflow_withXRayContext() { var exporter = InMemorySpanExporter.create(); var xrayPlugin = new InvocationOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)), - () -> new ExtractedContext("5759e988bd862e3fe1be46a994272793", "53995c3f42cd8ad8"), - false); + OtelPluginConfig.builder() + .contextExtractor( + () -> new ExtractedContext("5759e988bd862e3fe1be46a994272793", "53995c3f42cd8ad8")) + .enableMdc(false) + .build()); xrayPlugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec1", true, Instant.now())); xrayPlugin.onOperationStart( new OperationInfo("op-1", "step-a", "STEP", "Step", null, Instant.now(), null, false)); @@ -1330,9 +1353,11 @@ void workflowSpanName_isConfigurable() { var exporter = InMemorySpanExporter.create(); var customPlugin = new InvocationOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)), - () -> null, - false, - "MyWorkflow"); + OtelPluginConfig.builder() + .contextExtractor(() -> null) + .enableMdc(false) + .workflowSpanName("MyWorkflow") + .build()); customPlugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec1", true, Instant.now())); customPlugin.onInvocationEnd( new InvocationEndInfo("req-1", "arn:exec1", true, InvocationStatus.SUCCEEDED, null)); diff --git a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/MdcSpanEnricherTest.java b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/MdcSpanEnricherTest.java index 747aee695..2318e4075 100644 --- a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/MdcSpanEnricherTest.java +++ b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/MdcSpanEnricherTest.java @@ -58,8 +58,10 @@ void plugin_withMdcEnabled_setsFieldsInMdc() { var plugin = new InvocationOtelPlugin( SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(spanExporter)), - () -> null, - true); + OtelPluginConfig.builder() + .contextExtractor(() -> null) + .enableMdc(true) + .build()); plugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec-mdc-test", true, Instant.now())); From 2ab8eed0132e9836a962a23b5c72e23497a4d3ae Mon Sep 17 00:00:00 2001 From: silanhe Date: Wed, 5 Aug 2026 22:33:00 +0000 Subject: [PATCH 3/7] feat(otel): add auto-OTLP provider default and ProviderSource enum Bring the Java plugins to full 3-tier parity with the JS and Python SDK plugins, resolved via config: - ProviderSource enum (EXPLICIT / GLOBAL / AUTO_OTLP) with an ownsProvider() helper; surfaced through OtelPluginConfig.resolveSource() and exposed on each plugin via providerSource(). - New (OtelPluginConfig) constructor on both plugins: when no builder is supplied it resolves GLOBAL (useDefaultTracerProvider=true) or, by default, AUTO_OTLP -- a plugin-owned SdkTracerProvider that exports over OTLP/HTTP (OtlpHttpSpanExporter + BatchSpanProcessor), with an env-driven sampler (OTEL_DURABLE_SAMPLING_RATIO) and Lambda resource attributes, mirroring JS/Python. - OtelPluginConfig gains useDefaultTracerProvider, otlpEndpoint and otlpHeaders builder options. - pom: add opentelemetry-exporter-otlp and move opentelemetry-sdk to compile scope so the auto path works without the ADOT agent. The no-arg constructor still uses the ADOT/global provider (unchanged); the builder constructors remain EXPLICIT. otel-plugin suite green (Invocation 53, Execution 33); spotless clean. --- otel-plugin/pom.xml | 10 +- .../durable/otel/ExecutionOtelPlugin.java | 37 ++++++++ .../durable/otel/InvocationOtelPlugin.java | 37 ++++++++ .../lambda/durable/otel/OtelPluginConfig.java | 75 +++++++++++++++ .../durable/otel/OtelPluginSupport.java | 92 +++++++++++++++++++ .../lambda/durable/otel/ProviderSource.java | 34 +++++++ .../durable/otel/ExecutionOtelPluginTest.java | 13 +++ .../otel/InvocationOtelPluginTest.java | 24 +++++ 8 files changed, 320 insertions(+), 2 deletions(-) create mode 100644 otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ProviderSource.java diff --git a/otel-plugin/pom.xml b/otel-plugin/pom.xml index c3391934b..2a709f7f4 100644 --- a/otel-plugin/pom.xml +++ b/otel-plugin/pom.xml @@ -33,12 +33,18 @@ ${opentelemetry.version} - + io.opentelemetry opentelemetry-sdk ${opentelemetry.version} - provided + + + + io.opentelemetry + opentelemetry-exporter-otlp + ${opentelemetry.version} diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java index fa4aaac0a..fe6d9aac4 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java @@ -91,6 +91,7 @@ public class ExecutionOtelPlugin implements DurableExecutionPlugin { private final ContextExtractor contextExtractor; private final boolean enableMdc; private final String workflowSpanName; + private final ProviderSource providerSource; // Per-invocation state private volatile Span workflowSpan; @@ -155,6 +156,36 @@ public ExecutionOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder, OtelP this.contextExtractor = config.contextExtractor(); this.enableMdc = config.enableMdc(); this.workflowSpanName = config.workflowSpanName(); + this.providerSource = ProviderSource.EXPLICIT; + } + + /** + * Creates a Workflow-rooted OTel plugin from configuration alone (no caller-supplied tracer provider builder). + * + *

The provider is resolved from {@link OtelPluginConfig#resolveSource()}: {@link ProviderSource#GLOBAL} when + * {@code useDefaultTracerProvider(true)} (the ADOT/global provider), otherwise the default + * {@link ProviderSource#AUTO_OTLP} — a plugin-owned OTLP/HTTP provider (matching the JavaScript and Python SDK + * plugins). + * + * @param config the plugin configuration + */ + public ExecutionOtelPlugin(OtelPluginConfig config) { + this.contextExtractor = config.contextExtractor(); + this.enableMdc = config.enableMdc(); + this.workflowSpanName = config.workflowSpanName(); + this.providerSource = config.resolveSource(); + + if (this.providerSource == ProviderSource.GLOBAL) { + this.idGenerator = OtelPluginSupport.createDefaultIdGenerator(); + var tracerProvider = getDefaultTracerProvider(); + this.sdkTracerProvider = + OtelPluginSupport.getSdkTracerProviderForFlush(tracerProvider, "ExecutionOtelPlugin"); + this.tracer = tracerProvider.get(config.instrumentationName()); + } else { + this.idGenerator = new DeterministicIdGenerator(); + this.sdkTracerProvider = OtelPluginSupport.buildAutoOtlpProvider(config, this.idGenerator, null); + this.tracer = this.sdkTracerProvider.get(config.instrumentationName()); + } } private ExecutionOtelPlugin(TracerProvider tracerProvider, DeterministicIdGenerator idGenerator) { @@ -165,6 +196,12 @@ private ExecutionOtelPlugin(TracerProvider tracerProvider, DeterministicIdGenera this.contextExtractor = new XRayContextExtractor(); this.enableMdc = true; this.workflowSpanName = DEFAULT_WORKFLOW_SPAN_NAME; + this.providerSource = ProviderSource.GLOBAL; + } + + /** The tier that produced this plugin's tracer provider. */ + public ProviderSource providerSource() { + return providerSource; } // ─── Invocation hooks ──────────────────────────────────────────────── diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java index 1f51dd254..cf6e9b4d7 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java @@ -103,6 +103,7 @@ public class InvocationOtelPlugin implements DurableExecutionPlugin { private final ContextExtractor contextExtractor; private final boolean enableMdc; private final String workflowSpanName; + private final ProviderSource providerSource; // Per-invocation state private volatile Span workflowSpan; @@ -175,6 +176,36 @@ public InvocationOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder, Otel this.contextExtractor = config.contextExtractor(); this.enableMdc = config.enableMdc(); this.workflowSpanName = config.workflowSpanName(); + this.providerSource = ProviderSource.EXPLICIT; + } + + /** + * Creates an OTel plugin from configuration alone (no caller-supplied tracer provider builder). + * + *

The provider is resolved from {@link OtelPluginConfig#resolveSource()}: {@link ProviderSource#GLOBAL} when + * {@code useDefaultTracerProvider(true)} (the ADOT/global provider), otherwise the default + * {@link ProviderSource#AUTO_OTLP} — a plugin-owned OTLP/HTTP provider (matching the JavaScript and Python SDK + * plugins). + * + * @param config the plugin configuration + */ + public InvocationOtelPlugin(OtelPluginConfig config) { + this.contextExtractor = config.contextExtractor(); + this.enableMdc = config.enableMdc(); + this.workflowSpanName = config.workflowSpanName(); + this.providerSource = config.resolveSource(); + + if (this.providerSource == ProviderSource.GLOBAL) { + this.idGenerator = OtelPluginSupport.createDefaultIdGenerator(); + var tracerProvider = getDefaultTracerProvider(); + this.sdkTracerProvider = + OtelPluginSupport.getSdkTracerProviderForFlush(tracerProvider, "InvocationOtelPlugin"); + this.tracer = tracerProvider.get(config.instrumentationName()); + } else { + this.idGenerator = new DeterministicIdGenerator(); + this.sdkTracerProvider = OtelPluginSupport.buildAutoOtlpProvider(config, this.idGenerator, null); + this.tracer = this.sdkTracerProvider.get(config.instrumentationName()); + } } private InvocationOtelPlugin(TracerProvider tracerProvider, DeterministicIdGenerator idGenerator) { @@ -185,6 +216,12 @@ private InvocationOtelPlugin(TracerProvider tracerProvider, DeterministicIdGener this.contextExtractor = new XRayContextExtractor(); this.enableMdc = true; this.workflowSpanName = DEFAULT_WORKFLOW_SPAN_NAME; + this.providerSource = ProviderSource.GLOBAL; + } + + /** The tier that produced this plugin's tracer provider. */ + public ProviderSource providerSource() { + return providerSource; } // ─── Invocation hooks ──────────────────────────────────────────────── diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java index d46659493..ca88f91fb 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 package software.amazon.lambda.durable.otel; +import java.util.Map; + /** * Immutable configuration for {@link InvocationOtelPlugin} and {@link ExecutionOtelPlugin}. * @@ -39,6 +41,9 @@ public final class OtelPluginConfig { private final boolean enableMdc; private final String workflowSpanName; private final String instrumentationName; + private final boolean useDefaultTracerProvider; + private final String otlpEndpoint; + private final Map otlpHeaders; private OtelPluginConfig(Builder builder) { this.contextExtractor = @@ -48,6 +53,9 @@ private OtelPluginConfig(Builder builder) { builder.workflowSpanName != null ? builder.workflowSpanName : DEFAULT_WORKFLOW_SPAN_NAME; this.instrumentationName = builder.instrumentationName != null ? builder.instrumentationName : DEFAULT_INSTRUMENTATION_NAME; + this.useDefaultTracerProvider = builder.useDefaultTracerProvider; + this.otlpEndpoint = builder.otlpEndpoint; + this.otlpHeaders = builder.otlpHeaders != null ? Map.copyOf(builder.otlpHeaders) : Map.of(); } /** Returns a new builder with all fields defaulted. */ @@ -80,6 +88,35 @@ public String instrumentationName() { return instrumentationName; } + /** + * Whether to use the globally configured (ADOT) provider instead of an auto-configured OTLP provider. Only + * consulted when no {@code SdkTracerProviderBuilder} was supplied. Defaults to {@code false}. + */ + public boolean useDefaultTracerProvider() { + return useDefaultTracerProvider; + } + + /** OTLP/HTTP endpoint for the auto-configured provider, or {@code null} to use the OTel default / env var. */ + public String otlpEndpoint() { + return otlpEndpoint; + } + + /** Extra headers sent by the auto-configured OTLP exporter (never {@code null}). */ + public Map otlpHeaders() { + return otlpHeaders; + } + + /** + * The provider source selected by this config when no {@code SdkTracerProviderBuilder} is supplied. + * + *

Returns {@link ProviderSource#GLOBAL} when {@link #useDefaultTracerProvider()} is true, otherwise + * {@link ProviderSource#AUTO_OTLP}. (A supplied builder is always {@link ProviderSource#EXPLICIT}, decided by the + * constructor rather than the config.) + */ + public ProviderSource resolveSource() { + return useDefaultTracerProvider ? ProviderSource.GLOBAL : ProviderSource.AUTO_OTLP; + } + /** * Builder for {@link OtelPluginConfig}. * @@ -92,6 +129,9 @@ public static final class Builder { private boolean enableMdc = true; private String workflowSpanName; private String instrumentationName; + private boolean useDefaultTracerProvider = false; + private String otlpEndpoint; + private Map otlpHeaders; private Builder() {} @@ -140,6 +180,41 @@ public Builder instrumentationName(String instrumentationName) { return this; } + /** + * Sets whether to use the globally configured (ADOT) provider instead of an auto-configured OTLP provider. Only + * consulted when no {@code SdkTracerProviderBuilder} is supplied. Defaults to {@code false} (auto-OTLP). + * + * @param useDefaultTracerProvider if true, resolve to {@link ProviderSource#GLOBAL} + * @return this builder + */ + public Builder useDefaultTracerProvider(boolean useDefaultTracerProvider) { + this.useDefaultTracerProvider = useDefaultTracerProvider; + return this; + } + + /** + * Sets the OTLP/HTTP endpoint for the auto-configured provider. When null, the OTel default (or + * {@code OTEL_EXPORTER_OTLP_ENDPOINT}) is used. + * + * @param otlpEndpoint the OTLP/HTTP traces endpoint + * @return this builder + */ + public Builder otlpEndpoint(String otlpEndpoint) { + this.otlpEndpoint = otlpEndpoint; + return this; + } + + /** + * Sets extra headers for the auto-configured OTLP exporter (e.g. auth headers for a third-party endpoint). + * + * @param otlpHeaders header name/value pairs; null is treated as empty + * @return this builder + */ + public Builder otlpHeaders(Map otlpHeaders) { + this.otlpHeaders = otlpHeaders; + return this; + } + /** Builds an immutable {@link OtelPluginConfig}. */ public OtelPluginConfig build() { return new OtelPluginConfig(this); diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginSupport.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginSupport.java index 1408b28cf..291e1db68 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginSupport.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginSupport.java @@ -3,9 +3,15 @@ package software.amazon.lambda.durable.otel; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.semconv.ServiceAttributes; import java.nio.file.Files; import java.nio.file.Path; import org.slf4j.Logger; @@ -45,6 +51,92 @@ static DeterministicIdGenerator createDefaultIdGenerator() { return new DeterministicIdGenerator(); } + /** + * Builds a plugin-owned {@link SdkTracerProvider} that exports over OTLP/HTTP (the {@link ProviderSource#AUTO_OTLP} + * default). Mirrors the auto-configured provider in the JavaScript and Python SDK plugins: an OTLP/HTTP exporter, a + * batch span processor, an env-driven sampler, Lambda resource attributes, and the deterministic ID generator. + * + * @param config the plugin configuration (endpoint + headers) + * @param idGenerator the deterministic ID generator to install + * @param additionalResource extra resource attributes to merge (e.g. ExecutionOtelPlugin's service.name), or null + */ + static SdkTracerProvider buildAutoOtlpProvider( + OtelPluginConfig config, DeterministicIdGenerator idGenerator, Resource additionalResource) { + var exporterBuilder = OtlpHttpSpanExporter.builder(); + var endpoint = resolveOtlpEndpoint(config); + if (endpoint != null) { + exporterBuilder.setEndpoint(endpoint); + } + for (var header : config.otlpHeaders().entrySet()) { + exporterBuilder.addHeader(header.getKey(), header.getValue()); + } + + var resource = buildLambdaResource(); + if (additionalResource != null) { + resource = resource.merge(additionalResource); + } + + return SdkTracerProvider.builder() + .setIdGenerator(idGenerator) + .setSampler(resolveSampler()) + .setResource(resource) + .addSpanProcessor( + BatchSpanProcessor.builder(exporterBuilder.build()).build()) + .build(); + } + + /** Resolves the OTLP/HTTP traces endpoint (config -> env -> exporter default), appending the signal path. */ + private static String resolveOtlpEndpoint(OtelPluginConfig config) { + if (config.otlpEndpoint() != null && !config.otlpEndpoint().isBlank()) { + return config.otlpEndpoint(); + } + var envEndpoint = System.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"); + if (envEndpoint != null && !envEndpoint.isBlank()) { + var base = envEndpoint.endsWith("/") ? envEndpoint.substring(0, envEndpoint.length() - 1) : envEndpoint; + return base.endsWith("/v1/traces") ? base : base + "/v1/traces"; + } + // null -> the OTLP/HTTP exporter's own default (http://localhost:4318/v1/traces) + return null; + } + + /** Builds the sampler from {@code OTEL_DURABLE_SAMPLING_RATIO}, falling back to always-on. */ + private static Sampler resolveSampler() { + var raw = System.getenv("OTEL_DURABLE_SAMPLING_RATIO"); + if (raw != null) { + try { + var ratio = Double.parseDouble(raw); + if (ratio >= 0.0 && ratio <= 1.0) { + return Sampler.traceIdRatioBased(ratio); + } + } catch (NumberFormatException ignored) { + // fall through to always-on + } + } + return Sampler.alwaysOn(); + } + + /** Builds Lambda resource attributes from AWS_* env vars, merged onto the default resource. */ + private static Resource buildLambdaResource() { + var functionName = System.getenv("AWS_LAMBDA_FUNCTION_NAME"); + if (functionName == null || functionName.isBlank()) { + return Resource.getDefault(); + } + var attributes = Attributes.builder() + .put(ServiceAttributes.SERVICE_NAME, functionName) + .put("faas.name", functionName) + .put("cloud.provider", "aws") + .put("cloud.platform", "aws_lambda"); + var region = System.getenv("AWS_REGION"); + if (region != null && !region.isBlank()) { + attributes.put("cloud.region", region); + } + var version = System.getenv("AWS_LAMBDA_FUNCTION_VERSION"); + if (version != null && !version.isBlank()) { + attributes.put("faas.version", version); + } + return Resource.getDefault().merge(Resource.create(attributes.build())); + } + /** Extracts trace context from the current OTel span (fallback when X-Ray header is unavailable). */ static ExtractedContext extractCurrentSpanContext() { var spanContext = Span.current().getSpanContext(); diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ProviderSource.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ProviderSource.java new file mode 100644 index 000000000..dc085661b --- /dev/null +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ProviderSource.java @@ -0,0 +1,34 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.lambda.durable.otel; + +/** + * Which of the three resolution tiers produced a plugin's tracer provider. + * + *

Mirrors the {@code ProviderSource} used by the JavaScript and Python SDK OTel plugins for cross-SDK parity: + * + *

+ * + * @deprecated This is a preview API that is experimental and may be changed or removed in future releases. + */ +@Deprecated +public enum ProviderSource { + /** Caller-supplied {@code SdkTracerProviderBuilder}; plugin-owned. */ + EXPLICIT, + /** Globally configured provider (ADOT Java agent); not plugin-owned. */ + GLOBAL, + /** Auto-configured OTLP/HTTP provider; plugin-owned. */ + AUTO_OTLP; + + /** True when the plugin created (and therefore manages/flushes) the provider. */ + public boolean ownsProvider() { + return this == EXPLICIT || this == AUTO_OTLP; + } +} diff --git a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java index 71c9924ba..7bbc98e86 100644 --- a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java +++ b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java @@ -76,6 +76,19 @@ void customInstrumentationName_isUsedForTracerScope() { } } + @Test + void configOnlyConstructor_defaultsToAutoOtlpProvider() { + var plugin = new ExecutionOtelPlugin(OtelPluginConfig.defaults()); + assertEquals(ProviderSource.AUTO_OTLP, plugin.providerSource()); + assertTrue(plugin.providerSource().ownsProvider()); + } + + @Test + void builderConstructor_isExplicitSource() { + var plugin = new ExecutionOtelPlugin(SdkTracerProvider.builder(), OtelPluginConfig.defaults()); + assertEquals(ProviderSource.EXPLICIT, plugin.providerSource()); + } + @Test void defaultConstructor_throwsWhenAutoConfigurationCustomizerProviderIsNotInstalled() { GlobalOpenTelemetry.resetForTest(); diff --git a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java index 67957b139..957da656f 100644 --- a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java +++ b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java @@ -259,6 +259,30 @@ void customInstrumentationName_isUsedForTracerScope() { } } + @Test + void configOnlyConstructor_defaultsToAutoOtlpProvider() { + var plugin = new InvocationOtelPlugin(OtelPluginConfig.defaults()); + assertEquals(ProviderSource.AUTO_OTLP, plugin.providerSource()); + assertTrue(plugin.providerSource().ownsProvider()); + } + + @Test + void builderConstructor_isExplicitSource() { + var plugin = new InvocationOtelPlugin(SdkTracerProvider.builder(), OtelPluginConfig.defaults()); + assertEquals(ProviderSource.EXPLICIT, plugin.providerSource()); + } + + @Test + void configResolveSource_reflectsUseDefaultTracerProvider() { + assertEquals(ProviderSource.AUTO_OTLP, OtelPluginConfig.defaults().resolveSource()); + assertEquals( + ProviderSource.GLOBAL, + OtelPluginConfig.builder() + .useDefaultTracerProvider(true) + .build() + .resolveSource()); + } + @Test void invocationSpan_hasInternalKind() { plugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec1", true, Instant.now())); From 4aaba3c93f1a4ab875c5cd510e6156620d6b6cec Mon Sep 17 00:00:00 2001 From: silanhe Date: Wed, 5 Aug 2026 22:51:57 +0000 Subject: [PATCH 4/7] refactor(otel): drive OtelPluginConfig provider selection via ProviderSource Replace the redundant useDefaultTracerProvider boolean and the derived resolveSource() with a single ProviderSource field on OtelPluginConfig (default AUTO_OTLP). Drop the now-dead ProviderSource.ownsProvider() helper. The config-only plugin constructors read config.providerSource() directly and reject EXPLICIT (which requires the (SdkTracerProviderBuilder, OtelPluginConfig) constructor). --- .../durable/otel/ExecutionOtelPlugin.java | 16 +++++-- .../durable/otel/InvocationOtelPlugin.java | 16 +++++-- .../lambda/durable/otel/OtelPluginConfig.java | 47 +++++++++---------- .../lambda/durable/otel/ProviderSource.java | 17 ++++--- .../durable/otel/ExecutionOtelPluginTest.java | 21 ++++++++- .../otel/InvocationOtelPluginTest.java | 18 +++++-- 6 files changed, 85 insertions(+), 50 deletions(-) diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java index fe6d9aac4..c637c7b9a 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java @@ -162,18 +162,24 @@ public ExecutionOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder, OtelP /** * Creates a Workflow-rooted OTel plugin from configuration alone (no caller-supplied tracer provider builder). * - *

The provider is resolved from {@link OtelPluginConfig#resolveSource()}: {@link ProviderSource#GLOBAL} when - * {@code useDefaultTracerProvider(true)} (the ADOT/global provider), otherwise the default - * {@link ProviderSource#AUTO_OTLP} — a plugin-owned OTLP/HTTP provider (matching the JavaScript and Python SDK - * plugins). + *

The provider is taken from {@link OtelPluginConfig#providerSource()}: {@link ProviderSource#GLOBAL} uses the + * ADOT/global provider, otherwise the default {@link ProviderSource#AUTO_OTLP} builds a plugin-owned OTLP/HTTP + * provider (matching the JavaScript and Python SDK plugins). {@link ProviderSource#EXPLICIT} is rejected here — + * supply a {@code SdkTracerProviderBuilder} via the two-arg constructor for that. * * @param config the plugin configuration + * @throws IllegalArgumentException if {@code config.providerSource()} is {@link ProviderSource#EXPLICIT} */ public ExecutionOtelPlugin(OtelPluginConfig config) { this.contextExtractor = config.contextExtractor(); this.enableMdc = config.enableMdc(); this.workflowSpanName = config.workflowSpanName(); - this.providerSource = config.resolveSource(); + this.providerSource = config.providerSource(); + + if (this.providerSource == ProviderSource.EXPLICIT) { + throw new IllegalArgumentException("OtelPluginConfig.providerSource(EXPLICIT) requires a caller-supplied " + + "SdkTracerProviderBuilder; use the (SdkTracerProviderBuilder, OtelPluginConfig) constructor."); + } if (this.providerSource == ProviderSource.GLOBAL) { this.idGenerator = OtelPluginSupport.createDefaultIdGenerator(); diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java index cf6e9b4d7..9263bab6b 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java @@ -182,18 +182,24 @@ public InvocationOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder, Otel /** * Creates an OTel plugin from configuration alone (no caller-supplied tracer provider builder). * - *

The provider is resolved from {@link OtelPluginConfig#resolveSource()}: {@link ProviderSource#GLOBAL} when - * {@code useDefaultTracerProvider(true)} (the ADOT/global provider), otherwise the default - * {@link ProviderSource#AUTO_OTLP} — a plugin-owned OTLP/HTTP provider (matching the JavaScript and Python SDK - * plugins). + *

The provider is taken from {@link OtelPluginConfig#providerSource()}: {@link ProviderSource#GLOBAL} uses the + * ADOT/global provider, otherwise the default {@link ProviderSource#AUTO_OTLP} builds a plugin-owned OTLP/HTTP + * provider (matching the JavaScript and Python SDK plugins). {@link ProviderSource#EXPLICIT} is rejected here — + * supply a {@code SdkTracerProviderBuilder} via the two-arg constructor for that. * * @param config the plugin configuration + * @throws IllegalArgumentException if {@code config.providerSource()} is {@link ProviderSource#EXPLICIT} */ public InvocationOtelPlugin(OtelPluginConfig config) { this.contextExtractor = config.contextExtractor(); this.enableMdc = config.enableMdc(); this.workflowSpanName = config.workflowSpanName(); - this.providerSource = config.resolveSource(); + this.providerSource = config.providerSource(); + + if (this.providerSource == ProviderSource.EXPLICIT) { + throw new IllegalArgumentException("OtelPluginConfig.providerSource(EXPLICIT) requires a caller-supplied " + + "SdkTracerProviderBuilder; use the (SdkTracerProviderBuilder, OtelPluginConfig) constructor."); + } if (this.providerSource == ProviderSource.GLOBAL) { this.idGenerator = OtelPluginSupport.createDefaultIdGenerator(); diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java index ca88f91fb..12085240c 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java @@ -26,8 +26,8 @@ * } * *

Defaults: {@code contextExtractor = new XRayContextExtractor()}, {@code enableMdc = true}, {@code workflowSpanName - * = "Workflow"}, {@code instrumentationName = "aws-durable-execution-sdk-java"}. A {@code null} passed to any builder - * setter falls back to the corresponding default. + * = "Workflow"}, {@code instrumentationName = "aws-durable-execution-sdk-java"}, {@code providerSource = + * ProviderSource.AUTO_OTLP}. A {@code null} passed to any builder setter falls back to the corresponding default. * * @deprecated This is a preview API that is experimental and may be changed or removed in future releases. */ @@ -41,7 +41,7 @@ public final class OtelPluginConfig { private final boolean enableMdc; private final String workflowSpanName; private final String instrumentationName; - private final boolean useDefaultTracerProvider; + private final ProviderSource providerSource; private final String otlpEndpoint; private final Map otlpHeaders; @@ -53,7 +53,7 @@ private OtelPluginConfig(Builder builder) { builder.workflowSpanName != null ? builder.workflowSpanName : DEFAULT_WORKFLOW_SPAN_NAME; this.instrumentationName = builder.instrumentationName != null ? builder.instrumentationName : DEFAULT_INSTRUMENTATION_NAME; - this.useDefaultTracerProvider = builder.useDefaultTracerProvider; + this.providerSource = builder.providerSource != null ? builder.providerSource : ProviderSource.AUTO_OTLP; this.otlpEndpoint = builder.otlpEndpoint; this.otlpHeaders = builder.otlpHeaders != null ? Map.copyOf(builder.otlpHeaders) : Map.of(); } @@ -89,11 +89,15 @@ public String instrumentationName() { } /** - * Whether to use the globally configured (ADOT) provider instead of an auto-configured OTLP provider. Only - * consulted when no {@code SdkTracerProviderBuilder} was supplied. Defaults to {@code false}. + * The tracer-provider source to use when no {@code SdkTracerProviderBuilder} is supplied (the config-only + * constructors). {@link ProviderSource#GLOBAL} uses the globally configured (ADOT) provider; + * {@link ProviderSource#AUTO_OTLP} (the default) makes the plugin build and own an OTLP/HTTP provider. + * + *

{@link ProviderSource#EXPLICIT} is not valid here — it is implied by using a {@code (SdkTracerProviderBuilder, + * OtelPluginConfig)} constructor and is rejected by the config-only constructors. */ - public boolean useDefaultTracerProvider() { - return useDefaultTracerProvider; + public ProviderSource providerSource() { + return providerSource; } /** OTLP/HTTP endpoint for the auto-configured provider, or {@code null} to use the OTel default / env var. */ @@ -106,17 +110,6 @@ public Map otlpHeaders() { return otlpHeaders; } - /** - * The provider source selected by this config when no {@code SdkTracerProviderBuilder} is supplied. - * - *

Returns {@link ProviderSource#GLOBAL} when {@link #useDefaultTracerProvider()} is true, otherwise - * {@link ProviderSource#AUTO_OTLP}. (A supplied builder is always {@link ProviderSource#EXPLICIT}, decided by the - * constructor rather than the config.) - */ - public ProviderSource resolveSource() { - return useDefaultTracerProvider ? ProviderSource.GLOBAL : ProviderSource.AUTO_OTLP; - } - /** * Builder for {@link OtelPluginConfig}. * @@ -129,7 +122,7 @@ public static final class Builder { private boolean enableMdc = true; private String workflowSpanName; private String instrumentationName; - private boolean useDefaultTracerProvider = false; + private ProviderSource providerSource = ProviderSource.AUTO_OTLP; private String otlpEndpoint; private Map otlpHeaders; @@ -181,14 +174,18 @@ public Builder instrumentationName(String instrumentationName) { } /** - * Sets whether to use the globally configured (ADOT) provider instead of an auto-configured OTLP provider. Only - * consulted when no {@code SdkTracerProviderBuilder} is supplied. Defaults to {@code false} (auto-OTLP). + * Sets the tracer-provider source used when no {@code SdkTracerProviderBuilder} is supplied. Defaults to + * {@link ProviderSource#AUTO_OTLP} (a plugin-owned OTLP/HTTP provider); pass {@link ProviderSource#GLOBAL} to + * use the globally configured (ADOT) provider. A {@code null} falls back to {@link ProviderSource#AUTO_OTLP}. + * + *

{@link ProviderSource#EXPLICIT} is not accepted through the config-only constructors — supply a + * {@code SdkTracerProviderBuilder} via the two-arg constructor instead. * - * @param useDefaultTracerProvider if true, resolve to {@link ProviderSource#GLOBAL} + * @param providerSource the provider source, {@link ProviderSource#GLOBAL} or {@link ProviderSource#AUTO_OTLP} * @return this builder */ - public Builder useDefaultTracerProvider(boolean useDefaultTracerProvider) { - this.useDefaultTracerProvider = useDefaultTracerProvider; + public Builder providerSource(ProviderSource providerSource) { + this.providerSource = providerSource != null ? providerSource : ProviderSource.AUTO_OTLP; return this; } diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ProviderSource.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ProviderSource.java index dc085661b..28d2e659a 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ProviderSource.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ProviderSource.java @@ -11,11 +11,15 @@ *

  • {@link #EXPLICIT} — the caller supplied a {@link io.opentelemetry.sdk.trace.SdkTracerProviderBuilder} (the * {@code (SdkTracerProviderBuilder, OtelPluginConfig)} constructors); the plugin builds and owns that provider. *
  • {@link #GLOBAL} — {@code GlobalOpenTelemetry} / the ADOT Java agent (the no-arg constructor, or a config with - * {@code useDefaultTracerProvider(true)}); the plugin does not own the provider. - *
  • {@link #AUTO_OTLP} — the default when only an {@link OtelPluginConfig} is supplied and - * {@code useDefaultTracerProvider} is false: the plugin builds and owns an OTLP/HTTP provider. + * {@code providerSource(GLOBAL)}); the plugin does not own the provider. + *
  • {@link #AUTO_OTLP} — the default when only an {@link OtelPluginConfig} is supplied (its {@code providerSource} + * left at {@code AUTO_OTLP}): the plugin builds and owns an OTLP/HTTP provider. * * + *

    This is the single knob that selects a plugin's tracer provider. {@link OtelPluginConfig#providerSource()} carries + * it for the config-only constructors; the {@code (SdkTracerProviderBuilder, OtelPluginConfig)} constructors always + * report {@link #EXPLICIT}. + * * @deprecated This is a preview API that is experimental and may be changed or removed in future releases. */ @Deprecated @@ -25,10 +29,5 @@ public enum ProviderSource { /** Globally configured provider (ADOT Java agent); not plugin-owned. */ GLOBAL, /** Auto-configured OTLP/HTTP provider; plugin-owned. */ - AUTO_OTLP; - - /** True when the plugin created (and therefore manages/flushes) the provider. */ - public boolean ownsProvider() { - return this == EXPLICIT || this == AUTO_OTLP; - } + AUTO_OTLP } diff --git a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java index 7bbc98e86..97c108046 100644 --- a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java +++ b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java @@ -80,7 +80,6 @@ void customInstrumentationName_isUsedForTracerScope() { void configOnlyConstructor_defaultsToAutoOtlpProvider() { var plugin = new ExecutionOtelPlugin(OtelPluginConfig.defaults()); assertEquals(ProviderSource.AUTO_OTLP, plugin.providerSource()); - assertTrue(plugin.providerSource().ownsProvider()); } @Test @@ -89,6 +88,26 @@ void builderConstructor_isExplicitSource() { assertEquals(ProviderSource.EXPLICIT, plugin.providerSource()); } + @Test + void configProviderSource_defaultsToAutoOtlpAndHonorsGlobal() { + assertEquals(ProviderSource.AUTO_OTLP, OtelPluginConfig.defaults().providerSource()); + assertEquals( + ProviderSource.GLOBAL, + OtelPluginConfig.builder() + .providerSource(ProviderSource.GLOBAL) + .build() + .providerSource()); + } + + @Test + void configOnlyConstructor_rejectsExplicitProviderSource() { + var config = OtelPluginConfig.builder() + .providerSource(ProviderSource.EXPLICIT) + .build(); + var error = assertThrows(IllegalArgumentException.class, () -> new ExecutionOtelPlugin(config)); + assertTrue(error.getMessage().contains("SdkTracerProviderBuilder")); + } + @Test void defaultConstructor_throwsWhenAutoConfigurationCustomizerProviderIsNotInstalled() { GlobalOpenTelemetry.resetForTest(); diff --git a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java index 957da656f..4aa6047b4 100644 --- a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java +++ b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java @@ -263,7 +263,6 @@ void customInstrumentationName_isUsedForTracerScope() { void configOnlyConstructor_defaultsToAutoOtlpProvider() { var plugin = new InvocationOtelPlugin(OtelPluginConfig.defaults()); assertEquals(ProviderSource.AUTO_OTLP, plugin.providerSource()); - assertTrue(plugin.providerSource().ownsProvider()); } @Test @@ -273,14 +272,23 @@ void builderConstructor_isExplicitSource() { } @Test - void configResolveSource_reflectsUseDefaultTracerProvider() { - assertEquals(ProviderSource.AUTO_OTLP, OtelPluginConfig.defaults().resolveSource()); + void configProviderSource_defaultsToAutoOtlpAndHonorsGlobal() { + assertEquals(ProviderSource.AUTO_OTLP, OtelPluginConfig.defaults().providerSource()); assertEquals( ProviderSource.GLOBAL, OtelPluginConfig.builder() - .useDefaultTracerProvider(true) + .providerSource(ProviderSource.GLOBAL) .build() - .resolveSource()); + .providerSource()); + } + + @Test + void configOnlyConstructor_rejectsExplicitProviderSource() { + var config = OtelPluginConfig.builder() + .providerSource(ProviderSource.EXPLICIT) + .build(); + var error = assertThrows(IllegalArgumentException.class, () -> new InvocationOtelPlugin(config)); + assertTrue(error.getMessage().contains("SdkTracerProviderBuilder")); } @Test From dc782f8c5f0229dcc5f6bd7026b10017276381b8 Mon Sep 17 00:00:00 2001 From: silanhe Date: Thu, 6 Aug 2026 17:28:10 +0000 Subject: [PATCH 5/7] refactor(otel): extract ProviderSource resolver helper Move the config-only constructors' ProviderSource branching (GLOBAL vs AUTO_OTLP, EXPLICIT rejection) out of InvocationOtelPlugin and ExecutionOtelPlugin into a shared OtelPluginSupport.resolveConfiguredProvider helper returning a ProviderSetup record. Removes duplicated logic; no behavior change. --- .../durable/otel/ExecutionOtelPlugin.java | 22 ++----- .../durable/otel/InvocationOtelPlugin.java | 22 ++----- .../durable/otel/OtelPluginSupport.java | 60 +++++++++++++++++++ 3 files changed, 70 insertions(+), 34 deletions(-) diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java index c637c7b9a..b8f30752d 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java @@ -174,24 +174,12 @@ public ExecutionOtelPlugin(OtelPluginConfig config) { this.contextExtractor = config.contextExtractor(); this.enableMdc = config.enableMdc(); this.workflowSpanName = config.workflowSpanName(); - this.providerSource = config.providerSource(); - if (this.providerSource == ProviderSource.EXPLICIT) { - throw new IllegalArgumentException("OtelPluginConfig.providerSource(EXPLICIT) requires a caller-supplied " - + "SdkTracerProviderBuilder; use the (SdkTracerProviderBuilder, OtelPluginConfig) constructor."); - } - - if (this.providerSource == ProviderSource.GLOBAL) { - this.idGenerator = OtelPluginSupport.createDefaultIdGenerator(); - var tracerProvider = getDefaultTracerProvider(); - this.sdkTracerProvider = - OtelPluginSupport.getSdkTracerProviderForFlush(tracerProvider, "ExecutionOtelPlugin"); - this.tracer = tracerProvider.get(config.instrumentationName()); - } else { - this.idGenerator = new DeterministicIdGenerator(); - this.sdkTracerProvider = OtelPluginSupport.buildAutoOtlpProvider(config, this.idGenerator, null); - this.tracer = this.sdkTracerProvider.get(config.instrumentationName()); - } + var setup = OtelPluginSupport.resolveConfiguredProvider(config, "ExecutionOtelPlugin"); + this.providerSource = setup.source(); + this.idGenerator = setup.idGenerator(); + this.sdkTracerProvider = setup.sdkTracerProvider(); + this.tracer = setup.tracer(); } private ExecutionOtelPlugin(TracerProvider tracerProvider, DeterministicIdGenerator idGenerator) { diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java index 097e503f6..bbdf41463 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java @@ -194,24 +194,12 @@ public InvocationOtelPlugin(OtelPluginConfig config) { this.contextExtractor = config.contextExtractor(); this.enableMdc = config.enableMdc(); this.workflowSpanName = config.workflowSpanName(); - this.providerSource = config.providerSource(); - if (this.providerSource == ProviderSource.EXPLICIT) { - throw new IllegalArgumentException("OtelPluginConfig.providerSource(EXPLICIT) requires a caller-supplied " - + "SdkTracerProviderBuilder; use the (SdkTracerProviderBuilder, OtelPluginConfig) constructor."); - } - - if (this.providerSource == ProviderSource.GLOBAL) { - this.idGenerator = OtelPluginSupport.createDefaultIdGenerator(); - var tracerProvider = getDefaultTracerProvider(); - this.sdkTracerProvider = - OtelPluginSupport.getSdkTracerProviderForFlush(tracerProvider, "InvocationOtelPlugin"); - this.tracer = tracerProvider.get(config.instrumentationName()); - } else { - this.idGenerator = new DeterministicIdGenerator(); - this.sdkTracerProvider = OtelPluginSupport.buildAutoOtlpProvider(config, this.idGenerator, null); - this.tracer = this.sdkTracerProvider.get(config.instrumentationName()); - } + var setup = OtelPluginSupport.resolveConfiguredProvider(config, "InvocationOtelPlugin"); + this.providerSource = setup.source(); + this.idGenerator = setup.idGenerator(); + this.sdkTracerProvider = setup.sdkTracerProvider(); + this.tracer = setup.tracer(); } private InvocationOtelPlugin(TracerProvider tracerProvider, DeterministicIdGenerator idGenerator) { diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginSupport.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginSupport.java index 291e1db68..2f602a8c8 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginSupport.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginSupport.java @@ -5,6 +5,7 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.api.trace.TracerProvider; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; import io.opentelemetry.sdk.resources.Resource; @@ -85,6 +86,65 @@ static SdkTracerProvider buildAutoOtlpProvider( .build(); } + /** + * The tracer provider, tracer, and ID generator resolved for a config-only plugin constructor, plus the + * {@link ProviderSource} that produced them. + * + * @deprecated This is a preview API that is experimental and may be changed or removed in future releases. + */ + @Deprecated + record ProviderSetup( + ProviderSource source, + SdkTracerProvider sdkTracerProvider, + Tracer tracer, + DeterministicIdGenerator idGenerator) {} + + /** + * Resolves the tracer provider for the config-only plugin constructors from + * {@link OtelPluginConfig#providerSource()}, centralizing the {@link ProviderSource} branching shared by + * {@link InvocationOtelPlugin} and {@link ExecutionOtelPlugin}: + * + *

      + *
    • {@link ProviderSource#GLOBAL} — binds to the ADOT/global provider (not plugin-owned); the deterministic ID + * generator is created for the application-side state bridge. + *
    • {@link ProviderSource#AUTO_OTLP} — builds a plugin-owned OTLP/HTTP provider (see + * {@link #buildAutoOtlpProvider}). + *
    • {@link ProviderSource#EXPLICIT} — rejected: an explicit provider requires the + * {@code (SdkTracerProviderBuilder, OtelPluginConfig)} constructor. + *
    + * + * @param config the plugin configuration + * @param pluginName the plugin name used in diagnostics/flush logging + * @return the resolved provider, tracer, ID generator, and source + * @throws IllegalArgumentException if {@code config.providerSource()} is {@link ProviderSource#EXPLICIT} + */ + static ProviderSetup resolveConfiguredProvider(OtelPluginConfig config, String pluginName) { + return switch (config.providerSource()) { + case GLOBAL -> { + var idGenerator = createDefaultIdGenerator(); + var tracerProvider = getDefaultTracerProvider(pluginName); + yield new ProviderSetup( + ProviderSource.GLOBAL, + getSdkTracerProviderForFlush(tracerProvider, pluginName), + tracerProvider.get(config.instrumentationName()), + idGenerator); + } + case AUTO_OTLP -> { + var idGenerator = new DeterministicIdGenerator(); + var sdkTracerProvider = buildAutoOtlpProvider(config, idGenerator, null); + yield new ProviderSetup( + ProviderSource.AUTO_OTLP, + sdkTracerProvider, + sdkTracerProvider.get(config.instrumentationName()), + idGenerator); + } + case EXPLICIT -> + throw new IllegalArgumentException( + "OtelPluginConfig.providerSource(EXPLICIT) requires a caller-supplied SdkTracerProviderBuilder; " + + "use the (SdkTracerProviderBuilder, OtelPluginConfig) constructor."); + }; + } + /** Resolves the OTLP/HTTP traces endpoint (config -> env -> exporter default), appending the signal path. */ private static String resolveOtlpEndpoint(OtelPluginConfig config) { if (config.otlpEndpoint() != null && !config.otlpEndpoint().isBlank()) { From f03a8c4242a0794b042c9b71190a1c4006a68d1f Mon Sep 17 00:00:00 2001 From: silanhe Date: Thu, 6 Aug 2026 17:28:10 +0000 Subject: [PATCH 6/7] refactor(otel): default config to GLOBAL provider Flip OtelPluginConfig's providerSource default from AUTO_OTLP to GLOBAL so a default config resolves to the same provider as the no-arg constructor, matching the Python and JS SDK plugins (single consistent default). BREAKING CHANGE: new Plugin(OtelPluginConfig.defaults()) now binds to the global (ADOT) provider; set providerSource(AUTO_OTLP) for the plugin-owned OTLP/HTTP provider. --- .../lambda/durable/otel/OtelPluginConfig.java | 17 ++++++------- .../lambda/durable/otel/ProviderSource.java | 9 +++---- .../durable/otel/ExecutionOtelPluginTest.java | 24 +++++++++++++++---- .../otel/InvocationOtelPluginTest.java | 24 +++++++++++++++---- 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java index 12085240c..61d40f7bf 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java @@ -27,7 +27,7 @@ * *

    Defaults: {@code contextExtractor = new XRayContextExtractor()}, {@code enableMdc = true}, {@code workflowSpanName * = "Workflow"}, {@code instrumentationName = "aws-durable-execution-sdk-java"}, {@code providerSource = - * ProviderSource.AUTO_OTLP}. A {@code null} passed to any builder setter falls back to the corresponding default. + * ProviderSource.GLOBAL}. A {@code null} passed to any builder setter falls back to the corresponding default. * * @deprecated This is a preview API that is experimental and may be changed or removed in future releases. */ @@ -53,7 +53,7 @@ private OtelPluginConfig(Builder builder) { builder.workflowSpanName != null ? builder.workflowSpanName : DEFAULT_WORKFLOW_SPAN_NAME; this.instrumentationName = builder.instrumentationName != null ? builder.instrumentationName : DEFAULT_INSTRUMENTATION_NAME; - this.providerSource = builder.providerSource != null ? builder.providerSource : ProviderSource.AUTO_OTLP; + this.providerSource = builder.providerSource != null ? builder.providerSource : ProviderSource.GLOBAL; this.otlpEndpoint = builder.otlpEndpoint; this.otlpHeaders = builder.otlpHeaders != null ? Map.copyOf(builder.otlpHeaders) : Map.of(); } @@ -90,8 +90,8 @@ public String instrumentationName() { /** * The tracer-provider source to use when no {@code SdkTracerProviderBuilder} is supplied (the config-only - * constructors). {@link ProviderSource#GLOBAL} uses the globally configured (ADOT) provider; - * {@link ProviderSource#AUTO_OTLP} (the default) makes the plugin build and own an OTLP/HTTP provider. + * constructors). {@link ProviderSource#GLOBAL} (the default) uses the globally configured (ADOT) provider; + * {@link ProviderSource#AUTO_OTLP} makes the plugin build and own an OTLP/HTTP provider. * *

    {@link ProviderSource#EXPLICIT} is not valid here — it is implied by using a {@code (SdkTracerProviderBuilder, * OtelPluginConfig)} constructor and is rejected by the config-only constructors. @@ -122,7 +122,7 @@ public static final class Builder { private boolean enableMdc = true; private String workflowSpanName; private String instrumentationName; - private ProviderSource providerSource = ProviderSource.AUTO_OTLP; + private ProviderSource providerSource = ProviderSource.GLOBAL; private String otlpEndpoint; private Map otlpHeaders; @@ -175,8 +175,9 @@ public Builder instrumentationName(String instrumentationName) { /** * Sets the tracer-provider source used when no {@code SdkTracerProviderBuilder} is supplied. Defaults to - * {@link ProviderSource#AUTO_OTLP} (a plugin-owned OTLP/HTTP provider); pass {@link ProviderSource#GLOBAL} to - * use the globally configured (ADOT) provider. A {@code null} falls back to {@link ProviderSource#AUTO_OTLP}. + * {@link ProviderSource#GLOBAL} (the globally configured ADOT provider); pass {@link ProviderSource#AUTO_OTLP} + * to make the plugin build and own an OTLP/HTTP provider. A {@code null} falls back to + * {@link ProviderSource#GLOBAL}. * *

    {@link ProviderSource#EXPLICIT} is not accepted through the config-only constructors — supply a * {@code SdkTracerProviderBuilder} via the two-arg constructor instead. @@ -185,7 +186,7 @@ public Builder instrumentationName(String instrumentationName) { * @return this builder */ public Builder providerSource(ProviderSource providerSource) { - this.providerSource = providerSource != null ? providerSource : ProviderSource.AUTO_OTLP; + this.providerSource = providerSource != null ? providerSource : ProviderSource.GLOBAL; return this; } diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ProviderSource.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ProviderSource.java index 28d2e659a..4fb164050 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ProviderSource.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ProviderSource.java @@ -10,10 +10,11 @@ *

      *
    • {@link #EXPLICIT} — the caller supplied a {@link io.opentelemetry.sdk.trace.SdkTracerProviderBuilder} (the * {@code (SdkTracerProviderBuilder, OtelPluginConfig)} constructors); the plugin builds and owns that provider. - *
    • {@link #GLOBAL} — {@code GlobalOpenTelemetry} / the ADOT Java agent (the no-arg constructor, or a config with - * {@code providerSource(GLOBAL)}); the plugin does not own the provider. - *
    • {@link #AUTO_OTLP} — the default when only an {@link OtelPluginConfig} is supplied (its {@code providerSource} - * left at {@code AUTO_OTLP}): the plugin builds and owns an OTLP/HTTP provider. + *
    • {@link #GLOBAL} — {@code GlobalOpenTelemetry} / the ADOT Java agent (the no-arg constructor, or an + * {@link OtelPluginConfig} with its {@code providerSource} left at the default {@code GLOBAL}); the plugin does + * not own the provider. + *
    • {@link #AUTO_OTLP} — opt-in via {@code providerSource(AUTO_OTLP)} on a config-only constructor: the plugin + * builds and owns an OTLP/HTTP provider. *
    * *

    This is the single knob that selects a plugin's tracer provider. {@link OtelPluginConfig#providerSource()} carries diff --git a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java index aa9374bf6..7545ee6ed 100644 --- a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java +++ b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/ExecutionOtelPluginTest.java @@ -77,8 +77,22 @@ void customInstrumentationName_isUsedForTracerScope() { } @Test - void configOnlyConstructor_defaultsToAutoOtlpProvider() { + void configOnlyConstructor_defaultsToGlobalProvider() { + OtelPluginAutoConfigurationState.markInstalled(); + GlobalOpenTelemetry.resetForTest(); + OpenTelemetrySdk.builder() + .setTracerProvider(SdkTracerProvider.builder().build()) + .buildAndRegisterGlobal(); + var plugin = new ExecutionOtelPlugin(OtelPluginConfig.defaults()); + assertEquals(ProviderSource.GLOBAL, plugin.providerSource()); + } + + @Test + void configWithAutoOtlp_buildsPluginOwnedProvider() { + var plugin = new ExecutionOtelPlugin(OtelPluginConfig.builder() + .providerSource(ProviderSource.AUTO_OTLP) + .build()); assertEquals(ProviderSource.AUTO_OTLP, plugin.providerSource()); } @@ -89,12 +103,12 @@ void builderConstructor_isExplicitSource() { } @Test - void configProviderSource_defaultsToAutoOtlpAndHonorsGlobal() { - assertEquals(ProviderSource.AUTO_OTLP, OtelPluginConfig.defaults().providerSource()); + void configProviderSource_defaultsToGlobalAndHonorsAutoOtlp() { + assertEquals(ProviderSource.GLOBAL, OtelPluginConfig.defaults().providerSource()); assertEquals( - ProviderSource.GLOBAL, + ProviderSource.AUTO_OTLP, OtelPluginConfig.builder() - .providerSource(ProviderSource.GLOBAL) + .providerSource(ProviderSource.AUTO_OTLP) .build() .providerSource()); } diff --git a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java index 74413377e..037334fa7 100644 --- a/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java +++ b/otel-plugin/src/test/java/software/amazon/lambda/durable/otel/InvocationOtelPluginTest.java @@ -260,8 +260,22 @@ void customInstrumentationName_isUsedForTracerScope() { } @Test - void configOnlyConstructor_defaultsToAutoOtlpProvider() { + void configOnlyConstructor_defaultsToGlobalProvider() { + OtelPluginAutoConfigurationState.markInstalled(); + GlobalOpenTelemetry.resetForTest(); + OpenTelemetrySdk.builder() + .setTracerProvider(SdkTracerProvider.builder().build()) + .buildAndRegisterGlobal(); + var plugin = new InvocationOtelPlugin(OtelPluginConfig.defaults()); + assertEquals(ProviderSource.GLOBAL, plugin.providerSource()); + } + + @Test + void configWithAutoOtlp_buildsPluginOwnedProvider() { + var plugin = new InvocationOtelPlugin(OtelPluginConfig.builder() + .providerSource(ProviderSource.AUTO_OTLP) + .build()); assertEquals(ProviderSource.AUTO_OTLP, plugin.providerSource()); } @@ -272,12 +286,12 @@ void builderConstructor_isExplicitSource() { } @Test - void configProviderSource_defaultsToAutoOtlpAndHonorsGlobal() { - assertEquals(ProviderSource.AUTO_OTLP, OtelPluginConfig.defaults().providerSource()); + void configProviderSource_defaultsToGlobalAndHonorsAutoOtlp() { + assertEquals(ProviderSource.GLOBAL, OtelPluginConfig.defaults().providerSource()); assertEquals( - ProviderSource.GLOBAL, + ProviderSource.AUTO_OTLP, OtelPluginConfig.builder() - .providerSource(ProviderSource.GLOBAL) + .providerSource(ProviderSource.AUTO_OTLP) .build() .providerSource()); } From 53ca46ee25eed15da47c16e1a4fd24e7743803b1 Mon Sep 17 00:00:00 2001 From: silanhe Date: Thu, 6 Aug 2026 18:17:26 +0000 Subject: [PATCH 7/7] refactor(otel): route no-arg ctor through config The no-arg constructor now delegates to the config-only constructor with OtelPluginConfig.defaults() (which defaults to GLOBAL), collapsing the two separate GLOBAL entry points into one path through resolveConfiguredProvider. Removes the private (TracerProvider, DeterministicIdGenerator) constructor, the per-plugin getDefaultTracerProvider/createDefaultIdGenerator wrappers, the INSTRUMENTATION_NAME/DEFAULT_WORKFLOW_SPAN_NAME constants, and the unused TracerProvider import from both plugins. No behavior change. --- .../durable/otel/ExecutionOtelPlugin.java | 24 +------------------ .../durable/otel/InvocationOtelPlugin.java | 24 +------------------ 2 files changed, 2 insertions(+), 46 deletions(-) diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java index b8f30752d..f991d9c19 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/ExecutionOtelPlugin.java @@ -13,7 +13,6 @@ import io.opentelemetry.api.trace.TraceFlags; import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.api.trace.Tracer; -import io.opentelemetry.api.trace.TracerProvider; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.sdk.trace.SdkTracerProvider; @@ -82,8 +81,6 @@ public class ExecutionOtelPlugin implements DurableExecutionPlugin { private static final Logger logger = LoggerFactory.getLogger(ExecutionOtelPlugin.class); - private static final String INSTRUMENTATION_NAME = "aws-durable-execution-sdk-java"; - private static final String DEFAULT_WORKFLOW_SPAN_NAME = "Workflow"; private final SdkTracerProvider sdkTracerProvider; private final Tracer tracer; @@ -128,7 +125,7 @@ public ExecutionOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder) { * {@code OtelPluginAutoConfigurationCustomizerProvider}. */ public ExecutionOtelPlugin() { - this(getDefaultTracerProvider(), createDefaultIdGenerator()); + this(OtelPluginConfig.defaults()); } /** @@ -182,17 +179,6 @@ public ExecutionOtelPlugin(OtelPluginConfig config) { this.tracer = setup.tracer(); } - private ExecutionOtelPlugin(TracerProvider tracerProvider, DeterministicIdGenerator idGenerator) { - this.idGenerator = idGenerator; - this.sdkTracerProvider = OtelPluginSupport.getSdkTracerProviderForFlush(tracerProvider, "ExecutionOtelPlugin"); - this.tracer = tracerProvider.get(INSTRUMENTATION_NAME); - - this.contextExtractor = new XRayContextExtractor(); - this.enableMdc = true; - this.workflowSpanName = DEFAULT_WORKFLOW_SPAN_NAME; - this.providerSource = ProviderSource.GLOBAL; - } - /** The tier that produced this plugin's tracer provider. */ public ProviderSource providerSource() { return providerSource; @@ -602,12 +588,4 @@ private static String attemptKey(String operationId, Integer attempt) { private static ExtractedContext extractCurrentSpanContext() { return OtelPluginSupport.extractCurrentSpanContext(); } - - private static TracerProvider getDefaultTracerProvider() { - return OtelPluginSupport.getDefaultTracerProvider("ExecutionOtelPlugin"); - } - - private static DeterministicIdGenerator createDefaultIdGenerator() { - return OtelPluginSupport.createDefaultIdGenerator(); - } } diff --git a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java index bbdf41463..5f3b00041 100644 --- a/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/InvocationOtelPlugin.java @@ -13,7 +13,6 @@ import io.opentelemetry.api.trace.TraceFlags; import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.api.trace.Tracer; -import io.opentelemetry.api.trace.TracerProvider; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.sdk.trace.SdkTracerProvider; @@ -94,8 +93,6 @@ public class InvocationOtelPlugin implements DurableExecutionPlugin { private static final Logger logger = LoggerFactory.getLogger(InvocationOtelPlugin.class); - private static final String INSTRUMENTATION_NAME = "aws-durable-execution-sdk-java"; - private static final String DEFAULT_WORKFLOW_SPAN_NAME = "Workflow"; private final SdkTracerProvider sdkTracerProvider; private final Tracer tracer; @@ -148,7 +145,7 @@ public InvocationOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder) { * {@code OtelPluginAutoConfigurationCustomizerProvider}. */ public InvocationOtelPlugin() { - this(getDefaultTracerProvider(), createDefaultIdGenerator()); + this(OtelPluginConfig.defaults()); } /** @@ -202,17 +199,6 @@ public InvocationOtelPlugin(OtelPluginConfig config) { this.tracer = setup.tracer(); } - private InvocationOtelPlugin(TracerProvider tracerProvider, DeterministicIdGenerator idGenerator) { - this.idGenerator = idGenerator; - this.sdkTracerProvider = OtelPluginSupport.getSdkTracerProviderForFlush(tracerProvider, "InvocationOtelPlugin"); - this.tracer = tracerProvider.get(INSTRUMENTATION_NAME); - - this.contextExtractor = new XRayContextExtractor(); - this.enableMdc = true; - this.workflowSpanName = DEFAULT_WORKFLOW_SPAN_NAME; - this.providerSource = ProviderSource.GLOBAL; - } - /** The tier that produced this plugin's tracer provider. */ public ProviderSource providerSource() { return providerSource; @@ -658,12 +644,4 @@ private static String attemptKey(String operationId, Integer attempt) { private static ExtractedContext extractCurrentSpanContext() { return OtelPluginSupport.extractCurrentSpanContext(); } - - private static TracerProvider getDefaultTracerProvider() { - return OtelPluginSupport.getDefaultTracerProvider("InvocationOtelPlugin"); - } - - private static DeterministicIdGenerator createDefaultIdGenerator() { - return OtelPluginSupport.createDefaultIdGenerator(); - } }