Skip to content
Open
73 changes: 34 additions & 39 deletions otel-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,68 +215,63 @@ 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

```java
// 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

Expand Down
10 changes: 8 additions & 2 deletions otel-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,18 @@
<version>${opentelemetry.version}</version>
</dependency>

<!-- OpenTelemetry SDK (provided — users bring their own SDK configuration) -->
<!-- OpenTelemetry SDK (compile — needed for the auto-configured OTLP provider default;
the ADOT agent / a custom builder still override it at runtime). -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>${opentelemetry.version}</version>
<scope>provided</scope>
</dependency>
<!-- OTLP/HTTP span exporter for the auto-configured provider (ProviderSource.AUTO_OTLP). -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<!-- OpenTelemetry Java agent supplies this SPI when loading OTEL_JAVAAGENT_EXTENSIONS. -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -117,7 +118,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());
}

/**
Expand All @@ -131,37 +132,66 @@ 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.
*
* <p>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:
*
* <pre>{@code
* var plugin = new ExecutionOtelPlugin(
* SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)),
* OtelPluginConfig.builder().enableMdc(false).workflowSpanName("Workflow").build());
* }</pre>
*
* @param tracerProviderBuilder the tracer provider builder (ID generator will be overridden)
* @param contextExtractor extracts parent trace context from the Lambda environment
* @param config the plugin configuration
*/
public ExecutionOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder, ContextExtractor contextExtractor) {
this(tracerProviderBuilder, contextExtractor, true, DEFAULT_WORKFLOW_SPAN_NAME);
public ExecutionOtelPlugin(SdkTracerProviderBuilder tracerProviderBuilder, OtelPluginConfig config) {
this.idGenerator = new DeterministicIdGenerator();

this.sdkTracerProvider =
tracerProviderBuilder.setIdGenerator(idGenerator).build();
this.tracer = sdkTracerProvider.get(config.instrumentationName());
this.contextExtractor = config.contextExtractor();
this.enableMdc = config.enableMdc();
this.workflowSpanName = config.workflowSpanName();
this.providerSource = ProviderSource.EXPLICIT;
}

/**
* Creates a Workflow-rooted OTel plugin with full configuration.
* Creates a Workflow-rooted OTel plugin from configuration alone (no caller-supplied tracer provider builder).
*
* @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
* <p>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(
SdkTracerProviderBuilder tracerProviderBuilder,
ContextExtractor contextExtractor,
boolean enableMdc,
String workflowSpanName) {
this.idGenerator = new DeterministicIdGenerator();
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.");
}

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;
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) {
Expand All @@ -172,6 +202,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 ────────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -137,7 +138,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());
}

/**
Expand All @@ -151,48 +152,66 @@ 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.
* <p>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.
* <pre>{@code
* var plugin = new InvocationOtelPlugin(
* SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)),
* OtelPluginConfig.builder().enableMdc(false).workflowSpanName("Workflow").build());
* }</pre>
*
* @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;
}

/**
* Creates an OTel plugin from configuration alone (no caller-supplied tracer provider builder).
*
* <p>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.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());
}
}

private InvocationOtelPlugin(TracerProvider tracerProvider, DeterministicIdGenerator idGenerator) {
Expand All @@ -203,6 +222,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 ────────────────────────────────────────────────
Expand Down
Loading