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: + * + *
{@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 config the plugin configuration
*/
- public ExecutionOtelPlugin(
- SdkTracerProviderBuilder tracerProviderBuilder,
- ContextExtractor contextExtractor,
- boolean enableMdc,
- String workflowSpanName) {
+ public ExecutionOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder, OtelPluginConfig config) {
this.idGenerator = new DeterministicIdGenerator();
this.sdkTracerProvider =
tracerProviderBuilder.setIdGenerator(idGenerator).build();
- this.tracer = sdkTracerProvider.get(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();
+ this.providerSource = ProviderSource.EXPLICIT;
}
- private ExecutionOtelPlugin(TracerProvider tracerProvider, DeterministicIdGenerator idGenerator) {
- this.idGenerator = idGenerator;
- this.sdkTracerProvider = OtelPluginSupport.getSdkTracerProviderForFlush(tracerProvider, "ExecutionOtelPlugin");
- this.tracer = tracerProvider.get(INSTRUMENTATION_NAME);
+ /**
+ * Creates a Workflow-rooted OTel plugin from configuration alone (no caller-supplied tracer provider builder).
+ *
+ * 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(); + + var setup = OtelPluginSupport.resolveConfiguredProvider(config, "ExecutionOtelPlugin"); + this.providerSource = setup.source(); + this.idGenerator = setup.idGenerator(); + this.sdkTracerProvider = setup.sdkTracerProvider(); + this.tracer = setup.tracer(); + } - this.contextExtractor = new XRayContextExtractor(); - this.enableMdc = true; - this.workflowSpanName = DEFAULT_WORKFLOW_SPAN_NAME; + /** The tier that produced this plugin's tracer provider. */ + public ProviderSource providerSource() { + return providerSource; } // ─── Invocation hooks ──────────────────────────────────────────────── @@ -581,12 +591,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 227542d8a..56f6c4881 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; @@ -103,6 +100,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; @@ -137,7 +135,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()); } /** @@ -147,62 +145,63 @@ public InvocationOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder) { * {@code OtelPluginAutoConfigurationCustomizerProvider}. */ public InvocationOtelPlugin() { - this(getDefaultTracerProvider(), createDefaultIdGenerator()); + this(OtelPluginConfig.defaults()); } /** - * 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. + *
{@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 config the plugin configuration
*/
- public InvocationOtelPlugin(
- SdkTracerProviderBuilder tracerProviderBuilder,
- ContextExtractor contextExtractor,
- boolean enableMdc,
- String workflowSpanName) {
+ public InvocationOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder, OtelPluginConfig config) {
this.idGenerator = new DeterministicIdGenerator();
this.sdkTracerProvider =
tracerProviderBuilder.setIdGenerator(idGenerator).build();
- this.tracer = sdkTracerProvider.get(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();
+ this.providerSource = ProviderSource.EXPLICIT;
}
- private InvocationOtelPlugin(TracerProvider tracerProvider, DeterministicIdGenerator idGenerator) {
- this.idGenerator = idGenerator;
- this.sdkTracerProvider = OtelPluginSupport.getSdkTracerProviderForFlush(tracerProvider, "InvocationOtelPlugin");
- this.tracer = tracerProvider.get(INSTRUMENTATION_NAME);
+ /**
+ * Creates an OTel plugin from configuration alone (no caller-supplied tracer provider builder).
+ *
+ * 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(); + + var setup = OtelPluginSupport.resolveConfiguredProvider(config, "InvocationOtelPlugin"); + this.providerSource = setup.source(); + this.idGenerator = setup.idGenerator(); + this.sdkTracerProvider = setup.sdkTracerProvider(); + this.tracer = setup.tracer(); + } - this.contextExtractor = new XRayContextExtractor(); - this.enableMdc = true; - this.workflowSpanName = DEFAULT_WORKFLOW_SPAN_NAME; + /** The tier that produced this plugin's tracer provider. */ + public ProviderSource providerSource() { + return providerSource; } // ─── Invocation hooks ──────────────────────────────────────────────── @@ -641,12 +640,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(); - } } 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..61d40f7bf --- /dev/null +++ b/otel-plugin/src/main/java/software/amazon/lambda/durable/otel/OtelPluginConfig.java @@ -0,0 +1,221 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.lambda.durable.otel; + +import java.util.Map; + +/** + * 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"}, {@code providerSource =
+ * 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.
+ */
+@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 final ProviderSource providerSource;
+ private final String otlpEndpoint;
+ private final Map {@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 ProviderSource providerSource() {
+ return providerSource;
+ }
+
+ /** 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 {@link ProviderSource#EXPLICIT} is not accepted through the config-only constructors — supply a
+ * {@code SdkTracerProviderBuilder} via the two-arg constructor instead.
+ *
+ * @param providerSource the provider source, {@link ProviderSource#GLOBAL} or {@link ProviderSource#AUTO_OTLP}
+ * @return this builder
+ */
+ public Builder providerSource(ProviderSource providerSource) {
+ this.providerSource = providerSource != null ? providerSource : ProviderSource.GLOBAL;
+ 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 Mirrors the {@code ProviderSource} used by the JavaScript and Python SDK OTel plugins for cross-SDK parity:
+ *
+ * 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
+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
+}
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 a693be2e4..605bbaff8 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
@@ -53,6 +55,73 @@ void tearDown() {
// ─── Default constructor ─────────────────────────────────────────────
+ @Test
+ void customInstrumentationName_isUsedForTracerScope() {
+ var exporter = InMemorySpanExporter.create();
+ var customPlugin = new ExecutionOtelPlugin(
+ SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)),
+ 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));
+
+ var spans = exporter.getFinishedSpanItems();
+ assertFalse(spans.isEmpty());
+ for (var span : spans) {
+ assertEquals("my-custom-scope", span.getInstrumentationScopeInfo().getName());
+ }
+ }
+
+ @Test
+ 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());
+ }
+
+ @Test
+ void builderConstructor_isExplicitSource() {
+ var plugin = new ExecutionOtelPlugin(SdkTracerProvider.builder(), OtelPluginConfig.defaults());
+ assertEquals(ProviderSource.EXPLICIT, plugin.providerSource());
+ }
+
+ @Test
+ void configProviderSource_defaultsToGlobalAndHonorsAutoOtlp() {
+ assertEquals(ProviderSource.GLOBAL, OtelPluginConfig.defaults().providerSource());
+ assertEquals(
+ ProviderSource.AUTO_OTLP,
+ OtelPluginConfig.builder()
+ .providerSource(ProviderSource.AUTO_OTLP)
+ .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();
@@ -623,9 +692,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 =
@@ -646,9 +717,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");
@@ -662,9 +735,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, 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 020e3cbbe..d14d8140c 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
@@ -235,6 +237,74 @@ 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)),
+ 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));
+
+ var spans = exporter.getFinishedSpanItems();
+ assertFalse(spans.isEmpty());
+ for (var span : spans) {
+ assertEquals("my-custom-scope", span.getInstrumentationScopeInfo().getName());
+ }
+ }
+
+ @Test
+ 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());
+ }
+
+ @Test
+ void builderConstructor_isExplicitSource() {
+ var plugin = new InvocationOtelPlugin(SdkTracerProvider.builder(), OtelPluginConfig.defaults());
+ assertEquals(ProviderSource.EXPLICIT, plugin.providerSource());
+ }
+
+ @Test
+ void configProviderSource_defaultsToGlobalAndHonorsAutoOtlp() {
+ assertEquals(ProviderSource.GLOBAL, OtelPluginConfig.defaults().providerSource());
+ assertEquals(
+ ProviderSource.AUTO_OTLP,
+ OtelPluginConfig.builder()
+ .providerSource(ProviderSource.AUTO_OTLP)
+ .build()
+ .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
void invocationSpan_hasInternalKind() {
plugin.onInvocationStart(new InvocationInfo("req-1", "arn:exec1", true, Instant.now()));
@@ -675,8 +745,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(
@@ -701,8 +773,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));
@@ -720,8 +794,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(
@@ -750,8 +826,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));
@@ -775,8 +853,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));
@@ -805,8 +885,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()));
@@ -839,8 +921,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()));
@@ -870,8 +954,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));
@@ -1313,8 +1399,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, null, false));
@@ -1342,9 +1431,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()));
+ *
+ *
+ * @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()) {
+ 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..4fb164050
--- /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.
+ *
+ *
+ *
+ *
+ *