OpenTelemetry¶
The OpenTelemetry plugins trace durable operations across Lambda invocations. Create a factory for the execution view (the default) or select the invocation view explicitly. Configure an OpenTelemetry provider through a telemetry layer or application code. The provider owns exporters, resources, library instrumentation, and sampling policy.
The plugins are available for TypeScript, Python, and Java. Go, Rust, and C# do not yet provide this integration.
Install the plugin¶
These examples use the unified factory APIs available in the versions below. Upgrade the core and plugin together, including copies supplied by Lambda layers.
Requires Node.js 22 or later and OTel plugin 2.1.0 or later.
Requires OTel plugin 2.1.0 or later.
When a telemetry layer supplies the OpenTelemetry libraries:
The package installs a compatible core SDK but leaves OpenTelemetry libraries
to the layer. When the application supplies its own provider, install the
standalone extra instead:
Choose and register a view¶
Register one factory using the unified entry point for your language. Omitting the mode selects the execution view. The SDK creates a separate plugin instance for each invocation, while the provider can be shared across invocations.
Choosing a view¶
For both views, your observability platform's quotas and limits apply.
| Consideration | Execution view | Invocation view |
|---|---|---|
| Top-level operation parent | Workflow |
Current Invocation |
| Correlation link | Operation or attempt to current Invocation |
Operation or attempt to Workflow |
| Continuation link | Completed operation links only to the invocation that exports it | Later segment links to the initial logical operation span |
| Operation visibility | When the operation completes | At each invocation boundary |
| In-progress visibility | Limited until operations and the execution complete | Each invocation appears as it completes |
| Better for | Shorter executions and a workflow-centered hierarchy | Long executions and progress through individual invocations |
| Platform compatibility | Long-open spans can exceed ingestion time limits | Operation segments stay within an invocation; long Workflow spans still face ingestion limits |
Nested operations remain under their child-context operation span; attempts
remain under their operation. Both views export Invocation spans as invocations
end and Workflow only when the SDK observes execution success or failure.
Workflow spans can cover long durations, so backend ingestion limits still apply
to both views. Export and flush are best effort; a timeout, stop, or terminated
runtime can leave spans missing.
The diagrams below show an execution with a valid remote backend parent and ambient Lambda spans in the same trace. See trace identity and sampling for other parent contexts. The tree shows parent-child relationships; arrows show span links.
Execution view¶
Top-level operation spans are children of Workflow and link to the current Invocation.
The factory selects this view by default. An operation that suspends is exported
when it completes, using its durable
start and end times. Its span links to the invocation that exports the completion;
it does not accumulate links to every earlier invocation.
import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";
import { createOtelPluginFactory } from "@aws/durable-execution-sdk-js-otel";
export const handler = withDurableExecution(
async (event: { id: string }, context: DurableContext) => {
const data = await context.step("fetch-data", async () => event.id);
await context.wait("cooldown", { seconds: 5 });
return await context.step("process", async () => `processed ${data}`);
},
{ plugins: [createOtelPluginFactory()] },
);
from aws_durable_execution_sdk_python import DurableContext, durable_execution
from aws_durable_execution_sdk_python.config import Duration
from aws_durable_execution_sdk_python_otel import create_otel_plugin_factory
@durable_execution(plugins=[create_otel_plugin_factory()])
def handler(event: dict, context: DurableContext) -> str:
data = context.step(lambda ctx: event["id"], name="fetch-data")
context.wait(Duration.from_seconds(5), name="cooldown")
return context.step(lambda ctx: f"processed {data}", name="process")
import java.time.Duration;
import java.util.Map;
import software.amazon.lambda.durable.DurableConfig;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.otel.OtelPlugin;
public class ExecutionOtelExample extends DurableHandler<Map<String, String>, String> {
@Override
protected DurableConfig createConfiguration() {
return DurableConfig.builder().withPlugins(OtelPlugin.factory()).build();
}
@Override
public String handleRequest(Map<String, String> event, DurableContext context) {
String data = context.step("fetch-data", String.class, ctx -> event.get("id"));
context.wait("cooldown", Duration.ofSeconds(5));
return context.step("process", String.class, ctx -> "processed " + data);
}
}
For an execution that suspends at cooldown and resumes in a second invocation,
the trace resembles:
Remote backend server span
|-- Workflow (exported on terminal status)
| |-- Operation: fetch-data (STEP) -> link to Invocation #1
| | `-- Attempt: fetch-data attempt 1 -> link to Invocation #1
| |-- Operation: cooldown (WAIT) -> link to Invocation #2
| `-- Operation: process (STEP) -> link to Invocation #2
| `-- Attempt: process attempt 1 -> link to Invocation #2
|-- Ambient Lambda span #1
| `-- Invocation #1
`-- Ambient Lambda span #2
`-- Invocation #2
The links to Invocation do not make invocation spans children of Workflow.
Invocation view¶
Top-level operation spans are children of the Invocation that emitted them and link to
Workflow, so each invocation's spans export as it ends. Select invocation mode
explicitly. Completed work uses checkpointed results on replay. A resumed operation or
replayed context can produce another span segment without rerunning a completed
step's body.
import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";
import { createOtelPluginFactory } from "@aws/durable-execution-sdk-js-otel";
export const handler = withDurableExecution(
async (event, context: DurableContext) => {
return await context.step("process", async () => "done");
},
{ plugins: [createOtelPluginFactory({ mode: "invocation" })] },
);
from aws_durable_execution_sdk_python import DurableContext, durable_execution
from aws_durable_execution_sdk_python_otel import create_otel_plugin_factory
@durable_execution(plugins=[create_otel_plugin_factory(mode="invocation")])
def handler(event: dict, context: DurableContext) -> str:
return context.step(lambda ctx: "done", name="process")
import software.amazon.lambda.durable.DurableConfig;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.otel.OtelPlugin;
public class InvocationOtelExample extends DurableHandler<Object, String> {
@Override
protected DurableConfig createConfiguration() {
return DurableConfig.builder().withPlugins(OtelPlugin.factory(OtelPlugin.Mode.INVOCATION)).build();
}
@Override
public String handleRequest(Object event, DurableContext context) {
return context.step("process", String.class, ctx -> "done");
}
}
The same execution, using invocation view, produces this hierarchy:
Remote backend server span
|-- Workflow (exported on terminal status)
|-- Ambient Lambda span #1
| `-- Invocation #1
| |-- Operation: fetch-data (STEP) -> link to Workflow
| | `-- Attempt: fetch-data attempt 1
| | `-> link to Workflow
| `-- Operation: cooldown (WAIT) -> link to Workflow
`-- Ambient Lambda span #2
`-- Invocation #2
|-- Operation: cooldown (WAIT) -> links to initial cooldown
| span and Workflow
`-- Operation: process (STEP) -> link to Workflow
`-- Attempt: process attempt 1 -> link to Workflow
An operation that crosses an invocation boundary can produce more than one
span. A continuation segment uses a new span ID and links to the
initial logical operation span, which uses the deterministic operation span ID.
It also retains the Workflow link. CloudWatch does not yet visualize span
links.
Configure a telemetry layer¶
A language auto-instrumentation layer registers the global provider used by the default factories. Select a layer compatible with your runtime, architecture, and region:
| Layer | AWS_LAMBDA_EXEC_WRAPPER |
Setup |
|---|---|---|
| AWS Distro for OpenTelemetry (ADOT) | /opt/otel-instrument |
ADOT Lambda layers |
| OpenTelemetry community language layer | /opt/otel-handler |
Lambda auto-instrumentation |
The following SAM snippets show the telemetry settings for an
AWS::Serverless::Function resource. Merge the entries under Properties with
your function's existing layers, environment variables, and policies.
SAM applies Policies only when it creates
the execution role. If you specify Role, attach the required permissions
directly to that role.
ADOT layer¶
For ADOT with X-Ray export, enable Active tracing and grant
AWSXRayDaemonWriteAccess alongside the durable execution permissions.
MyFunction:
Type: AWS::Serverless::Function
Properties:
Layers:
- <adot-language-layer-arn>
Environment:
Variables:
AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-instrument
Tracing: Active
Policies:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy
- arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess
Community language layer¶
For a community language layer exporting directly to an OTLP backend, use
PassThrough tracing.
Lambda passes the upstream X-Ray trace header to the
function without emitting native Lambda X-Ray segments. The layer and durable
plugin still provide application instrumentation.
Propagate the caller's trace context and sampling decision. Without an upstream
header, Lambda generates a header with Sampled=0; the documented plugins honor
that decision even with a local always_on sampler. See
supported sampling modes.
MyFunction:
Type: AWS::Serverless::Function
Properties:
Layers:
- <community-language-layer-arn>
Environment:
Variables:
AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-handler
OTEL_TRACES_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT: https://<otlp-endpoint>
OTEL_METRICS_EXPORTER: none
OTEL_LOGS_EXPORTER: none
Tracing: PassThrough
Policies:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy
Exporting to CloudWatch: Enable Transaction Search in the destination account and Region, and confirm that Ingest OpenTelemetry spans shows Enabled. Otherwise, CloudWatch's OTLP endpoint rejects the spans. This setting is not required when exporting directly to another observability provider.
Configure OTEL_EXPORTER_OTLP_HEADERS if your backend requires authentication.
Direct OTLP export to another observability provider does not require X-Ray
write permissions. SAM still adds an X-Ray policy when it creates a role for
Tracing: PassThrough.
With Active tracing, a durable span can reference a native Lambda parent sent
only to X-Ray. The plugin preserves that parent without re-exporting it, so a
shared trace ID does not guarantee a complete parent tree in an OTLP backend.
If you need native Lambda platform spans, use an integration that receives both
OTLP spans and their X-Ray ancestors.
PassThrough does not supply missing caller spans. For a complete upstream
hierarchy, export the real caller span to the same backend and propagate its
trace ID, span ID, and sampling decision through X-Amzn-Trace-Id when using the
default plugin extractors. Preserve this propagation across downstream durable
invokes.
You can optionally export telemetry through a Collector extension, which does not initialize the OpenTelemetry provider required by the plugin.
Provider initialization¶
These requirements apply to both layer choices.
The default factory resolves the global provider at invocation start.
Use the base plugin package with a telemetry layer so function dependencies do not shadow the layer's OpenTelemetry libraries.
The Java agent must also load the plugin JAR as an extension to install
durable ID generation and sampling. Package the JAR in a layer under
java/lib, add that layer to the function, and set:
Obtain the ADOT Java layer from the Java instrumentation releases.
If the global SDK provider or required durable ID integration is unavailable, the plugin logs a warning and disables its telemetry for that invocation. The next invocation retries initialization.
Load the factory without handler registration¶
DURABLE_EXECUTION_PLUGINS selects a default-configured OTel factory from an installed
package or layer. These factories use the global OpenTelemetry tracer provider
initialized by your telemetry layer or application. Choose one view through either
code or environment configuration.
The value follows each SDK's discovery mechanism: an importable module specifier or a registered provider name.
| Language and lookup | Execution view (default) | Invocation view |
|---|---|---|
| TypeScript: Node.js module specifier | @aws/durable-execution-sdk-js-otel |
@aws/durable-execution-sdk-js-otel/otel-invocation |
| Python: package entry-point name | otel |
otel-invocation |
Java: ServiceLoader provider name |
otel |
otel-invocation |
Configure exactly one OTel view
Registering two distinct OTel factories fails validation because the OTel
views share an exclusive group in each SDK. This applies to naming two views
in DURABLE_EXECUTION_PLUGINS, and to naming one there while also registering
a separate factory in handler code.
@aws/durable-execution-sdk-js-otel/otel-execution selects the same execution
view as the bare @aws/durable-execution-sdk-js-otel package name. Configure
only one of these entry points in DURABLE_EXECUTION_PLUGINS.
otel-execution is an alias for the default otel execution view. Both names
resolve to the same factory object. If both appear in
DURABLE_EXECUTION_PLUGINS, the SDK loads one factory and logs a warning that
it skipped the duplicate. Configure one name.
otel-execution also selects the execution view. Each provider name creates
a separate factory, so DURABLE_EXECUTION_PLUGINS=otel-execution,otel fails
at startup:
Conflicting plugin factories otel-execution (ExecutionOtelPlugin) and otel (ExecutionOtelPlugin) in exclusive group 'durable-otel-view'. Configure only one plugin from this group.
Configure one provider name.
Configure an application-owned provider¶
This setup is for advanced use cases that require direct control over the OpenTelemetry provider. You do not need it when using a language auto-instrumentation layer: the layer initializes the global provider used by the plugin's default factories.
Configure the exporter and processors once, then register the resulting factory as in the handler examples above. The plugin flushes at invocation end but does not take over provider shutdown. These examples send OTLP HTTP to a collector listening on loopback port 4318.
Install @opentelemetry/exporter-trace-otlp-http:
The provider factory receives the durable ID-generator factory; pass its result to the provider constructor.
import { createOtelPluginFactory } from "@aws/durable-execution-sdk-js-otel";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { BatchSpanProcessor, NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
const pluginFactory = createOtelPluginFactory({
tracerProviderFactory: (createIdGenerator) => {
const provider = new NodeTracerProvider({
idGenerator: createIdGenerator(),
spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({
url: "http://localhost:4318/v1/traces",
}))],
});
provider.register();
return provider;
},
});
Set mode: "invocation" in the same options object to select the invocation view.
Install the standalone extra and opentelemetry-exporter-otlp-proto-http.
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from aws_durable_execution_sdk_python_otel import (
create_otel_plugin_factory,
OtelPluginConfig,
)
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(
OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
))
trace.set_tracer_provider(provider)
plugin_factory = create_otel_plugin_factory(
OtelPluginConfig(tracer_provider=provider)
)
For invocation view, replace the factory call with:
plugin_factory = create_otel_plugin_factory(
OtelPluginConfig(tracer_provider=provider), mode="invocation"
)
tracer_provider is what the plugin uses, so set_tracer_provider is only
needed to point other instrumentation at the same provider. It takes effect
once per process and is ignored with a warning if a telemetry layer already
registered a global provider; omit it in that case.
Add io.opentelemetry:opentelemetry-exporter-otlp to your dependencies,
using OpenTelemetry 1.67.0 to match plugin 3.1.0:
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
<version>1.67.0</version>
</dependency>
The factory wraps the provider builder's ID generator and sampler, then builds the provider once.
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import software.amazon.lambda.durable.otel.OtelPlugin;
var exporter = OtlpHttpSpanExporter.builder()
.setEndpoint("http://localhost:4318/v1/traces")
.build();
var pluginFactory = OtelPlugin.factory(
OtelPlugin.Mode.EXECUTION,
SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(exporter).build()));
Use OtelPlugin.Mode.INVOCATION as the first argument to select the invocation view.
Plugin options¶
Pass OtelPluginFactoryOptions to createOtelPluginFactory(options). These
options extend OtelPluginConfig with the view selection.
| Option | Default | Purpose |
|---|---|---|
mode |
"execution" |
Select "execution" or "invocation" |
tracerProviderFactory |
Global provider | Build an application-owned provider |
contextExtractor |
xRayContextExtractor |
Extract this execution's upstream context |
instrumentationName |
aws-durable-execution-sdk-js |
Instrumentation scope |
workflowSpanName |
Workflow |
Workflow span name |
enrichLogger |
true |
Add trace fields to durable logs |
flushTimeoutMillis |
1000 |
Total time budget for awaiting provider flushes at invocation end |
The default X-Ray extractor reads invocation-local xRayTraceId when
present, otherwise _X_AMZN_TRACE_ID.
A flush timeout stops waiting; it does not cancel exports or synchronous
provider work.
Pass OtelPluginConfig to create_otel_plugin_factory(config). Select the
invocation view with create_otel_plugin_factory(config, mode="invocation");
mode is a keyword argument, separate from OtelPluginConfig.
| Option | Default | Purpose |
|---|---|---|
tracer_provider |
Global provider | Use an application-owned SDK provider |
context_extractor |
xray_context_extractor |
Extract upstream context from _X_AMZN_TRACE_ID |
instrument_name |
aws-durable-execution-sdk-python |
Instrumentation scope |
workflow_span_name |
Workflow |
Workflow span name |
enrich_logger |
True |
Add trace fields through root logger handlers |
Use the default X-Ray extractor or implement a custom extractor.
OtelPlugin exposes factory(), factory(mode), factory(mode, config),
factory(mode, providerBuilder), and factory(mode, providerBuilder, config).
Use OtelPlugin.Mode.EXECUTION or OtelPlugin.Mode.INVOCATION for mode.
Only factory() omits the mode and defaults to execution. Build
OtelPluginConfig with:
| Builder method | Default | Purpose |
|---|---|---|
contextExtractor(...) |
new XRayContextExtractor() |
Extract upstream trace context |
instrumentationName(...) |
aws-durable-execution-sdk-java |
Instrumentation scope |
workflowSpanName(...) |
Workflow |
Workflow span name |
enableMdc(...) |
true |
Add trace fields to SLF4J MDC |
A custom extractor must return the durable execution's own context consistently across resumes. An unrelated or per-invocation trace ID splits the execution trace.
Trace identity and sampling¶
Both views keep one canonical trace ID per execution. A valid propagated X-Ray
Root supplies that ID, and a valid Parent becomes the remote execution
ancestor. Missing Sampled does not invalidate a complete remote parent.
Workflow has a stable execution-scoped span ID and parents to that ancestor.
Invocation uses an active ambient span, such as the Lambda span, only when it
belongs to the same trace; otherwise it parents to the execution ancestor.
Execution ancestor
|-- Workflow
|-- Ambient Lambda span #1
| `-- Invocation #1
`-- Ambient Lambda span #2
`-- Invocation #2
When no complete remote parent is available, the SDK uses a synthetic
DurableExecutionRoot. It retains a valid propagated trace ID, or derives one
from the execution ARN and stable execution start time. With a supported provider
and that start time, sampled invocations export a zero-duration root with stable
identity to preserve ancestry before the execution completes. Resumes can export
another copy. Workflow carries duration and outcome; the fallback root carries
neither. Remote backend spans are owned by the backend and are never exported
by the plugin.
Configure a supported sampling mode through the provider,
or through OTEL_TRACES_SAMPLER and
OTEL_TRACES_SAMPLER_ARG when using auto-configuration. Keep the plugin's sampler
integration installed so durable spans share the resolved decision while other
instrumentation retains its own policy.
An explicit Sampled=1 or Sampled=0 on a valid extracted trace takes
precedence. Otherwise, the configured sampler evaluates its root policy
using the canonical trace ID. Missing Sampled therefore does not make a
ParentBased sampler automatically drop the trace.
A tracer that hides its sampler cannot enforce a shared durable sampling decision and retains terminal-only fallback-root export.
Sampling follows the explicit backend decision, then the recording/sampled state of a same-trace ambient span, then the configured sampler's root policy. The plugin resolves the result once per invocation for its durable spans.
Sampling follows the explicit backend decision, then a same-trace ambient
span, then the configured sampler. Retain DurableSampler, installed by the
provider-builder and agent-extension paths, as the final sampler. Replacing
it with a plain sampler removes the shared-decision guarantee.
Supported sampling modes¶
These rules apply to TypeScript core 3.0.0 / OTel 2.1.0, Python core 3.0.2 / OTel 2.1.0, and Java SDK/plugin 3.1.0, in both views. These versions include the unified factory APIs used by this guide.
- Explicit upstream decision: a valid
Sampled=0orSampled=1overrides the local sampler for durable spans. ThusSampled=1still exports durable spans with localalways_off, andSampled=0drops them with localalways_on. Active tracing alone does not implySampled=1. - Otherwise: use a deterministic policy based on the canonical trace ID, such
as
always_on,always_off, ortraceidratiowith a fixed ratio. Fortraceidratio,OTEL_TRACES_SAMPLER_ARG=0.5selects whole execution traces when the local policy is consulted.
Keep the policy, ratio, and any inherited upstream or ambient decision stable across retries, resumes, and processes. Random, time-, load-, or mutable-state-dependent policies are unsupported and can produce partial traces. The plugins do not checkpoint sampling decisions; caching or application persistence does not make another policy supported.
The provider or auto-configuration layer resolves the environment settings, defaults, and invalid values. They do not override an explicitly configured application provider. Keep the plugin's sampler integration installed; other application and ADOT spans retain their own policy.
Sampling behavior may change in future SDK/plugin releases.
Issue #295 proposes
letting the configured sampler decide whether to inherit upstream sampling, using
standard direct and parentbased_* semantics. That proposal is not implemented in
the versions listed above. Check the release notes and this section when upgrading.
Sampling does not guarantee export or a complete parent tree: a durable span may be sampled while its application parent is dropped, or its native X-Ray parent may be absent from OTLP. See the community layer setup for trace propagation and native X-Ray parents.
Span attributes and failures¶
Optional attributes such as durable.operation.name and
durable.operation.subtype are emitted only when supplied.
| Span | Main attributes |
|---|---|
| Workflow | durable.execution.arn, durable.execution.status |
| Invocation | durable.execution.arn, durable.invocation.status, durable.invocation.first |
| Operation | durable.execution.arn, durable.operation.id, durable.operation.type, durable.operation.name, durable.operation.subtype, durable.operation.status |
| Attempt | durable.execution.arn, durable.operation.id, durable.operation.type, durable.operation.name, durable.attempt.number, durable.attempt.outcome |
Operation types include STEP, WAIT, CONTEXT, CHAINED_INVOKE, and CALLBACK.
A context has an operation span without a separate attempt span. An invocation
that will retry reports durable.invocation.status=RETRYING with OTel UNSET.
A caught operation failure can have an ERROR span while the enclosing invocation
and workflow succeed. Inspect operation status as well as exception events;
a backend can report failure without diagnostic details.
FAILED, TIMED_OUT, and STOPPED operations set ERROR even without
diagnostics. The plugins export newly observed external completion updates
for waits, callbacks, and invokes when their metadata is visible, even if replay
skips the operation's body. When completed child contexts would otherwise
prune that metadata, configure
pluginsConfig.childOperationsDepth as described in
operation detail.
FAILED, TIMED_OUT, and STOPPED operations set ERROR even without an
error object. Exception events preserve the checkpointed type, message, and
stack trace; missing fields are omitted. No exception event is emitted when
both type and message are absent. Terminal operation and attempt exception
events use the span end timestamp.
Completion spans can be missing when replay returns a cached parent result without visiting its children, even if their operation metadata is still visible. Pruned operation metadata cannot be reconstructed.
FAILED, TIMED_OUT, and STOPPED operations set ERROR even without
diagnostics. Missing details do not produce an exception event. Execution-view
operation exceptions use the known backend end timestamp when available;
invocation-view exceptions retain normal OpenTelemetry event timing.
Since SDK/plugin 3.0.1, operation exception events report the stored original
error type when available, including errors from another language, while
retaining the throwable's message and stack diagnostics.
Correlate logs and verify traces¶
Log correlation is enabled by default. traceId identifies the execution trace,
spanId identifies the active span, and otelTraceSampled carries its sampled
flag. Application-created spans on that trace can become the active context too.
enrichLogger defaults to true. The default logger includes trace fields
when you call context.logger.info() or another log method.
For a custom Powertools logger, use createPowertoolsLogger to include the
same fields. Install Powertools alongside the SDK and plugin:
With the OpenTelemetry provider configured as shown above:
import { Logger } from "@aws-lambda-powertools/logger";
import {
createPowertoolsLogger,
DurableContext,
withDurableExecution,
} from "@aws/durable-execution-sdk-js";
import { createOtelPluginFactory } from "@aws/durable-execution-sdk-js-otel";
const logger = createPowertoolsLogger(new Logger({ serviceName: "orders" }));
export const handler = withDurableExecution(
async (_event: unknown, context: DurableContext) => {
context.configureLogger({ customLogger: logger });
context.logger.info("Processing order");
return "processed";
},
{ plugins: [createOtelPluginFactory()] },
);
The resulting JSON record includes these fields; trace and span IDs vary:
{
"message": "Processing order",
"traceId": "1234567890abcdef1234567890abcdef",
"spanId": "1234567890abcdef",
"otelTraceSampled": true
}
The adapter reads the current trace fields on each log call. In execution
view, handler code between operations uses Workflow; invocation view uses
Invocation. Step bodies use their attempt span. If you write your own
adapter, call getLogEnrichment() from its DurableLoggingContext on each
log call and include the returned fields in the output. See
custom loggers.
enrich_logger defaults to True. Its filter adds trace fields to log
records processed by the root logger's handlers. For standard-library text
logging, configure the formatter to print those fields:
import logging
from aws_durable_execution_sdk_python import DurableContext, durable_execution
from aws_durable_execution_sdk_python_otel import create_otel_plugin_factory
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
formatter = logging.Formatter(
"%(message)s traceId=%(traceId)s spanId=%(spanId)s "
"otelTraceSampled=%(otelTraceSampled)s",
defaults={"traceId": "-", "spanId": "-", "otelTraceSampled": "-"},
)
for log_handler in root_logger.handlers:
log_handler.setFormatter(formatter)
root_logger.info("Function initialized")
@durable_execution(plugins=[create_otel_plugin_factory()])
def handler(event: dict, context: DurableContext) -> str:
context.logger.info("Processing request")
return "done"
Configure the OpenTelemetry provider as shown above. The example keeps
existing handlers and sets their formatter before the first invocation.
Initialization logs and threads without invocation context have no trace
fields, so the formatter supplies - for missing values:
Function initialized traceId=- spanId=- otelTraceSampled=-
Processing request traceId=1234567890abcdef1234567890abcdef spanId=1234567890abcdef otelTraceSampled=True
Trace and span IDs vary. Handler code uses Invocation, step bodies use
their attempt, and child-context bodies use their context span.
enableMdc defaults to true. The plugin puts the trace fields in SLF4J's
mapped diagnostic context (MDC). Your logging layout must include those
fields to print them.
For text logging with Logback, add its SLF4J provider to your pom.xml:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.6.6</version>
<scope>runtime</scope>
</dependency>
Save this configuration as src/main/resources/logback.xml. If you already
have a Logback configuration, add the three %X fields to its existing
encoder pattern:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%msg traceId=%X{traceId:--} spanId=%X{spanId:--} otelTraceSampled=%X{otelTraceSampled:--}%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
With the OpenTelemetry provider configured as shown above, log through the durable context:
import java.util.Map;
import org.slf4j.LoggerFactory;
import software.amazon.lambda.durable.DurableConfig;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.otel.OtelPlugin;
public class LogCorrelationExample extends DurableHandler<Map<String, String>, String> {
static {
LoggerFactory.getLogger(LogCorrelationExample.class).info("Function initialized");
}
@Override
protected DurableConfig createConfiguration() {
return DurableConfig.builder().withPlugins(OtelPlugin.factory()).build();
}
@Override
public String handleRequest(Map<String, String> event, DurableContext context) {
context.getLogger().info("Processing request");
return context.step("process", String.class, stepContext -> {
stepContext.getLogger().info("Processing step");
return "done";
});
}
}
The %X expressions read the current MDC fields. Their defaults print -
when a field is absent. With the default execution view in SDK/plugin
3.1.0, the plugin sets only traceId between operations. The step log
includes all three fields and identifies its attempt span:
Function initialized traceId=- spanId=- otelTraceSampled=-
Processing request traceId=1234567890abcdef1234567890abcdef spanId=- otelTraceSampled=-
Processing step traceId=1234567890abcdef1234567890abcdef spanId=1234567890abcdef otelTraceSampled=true
Trace and span IDs vary. In CloudWatch, use Group by nodes to inspect
the hierarchy; the ungrouped X-Ray Segments Timeline cannot nest OTLP spans
under the Lambda platform segment. If spans are missing when using a Java
agent, check OTEL_JAVAAGENT_EXTENSIONS.
Test with an execution that waits and resumes. Confirm that its spans share a
trace ID, invocations appear as they finish, completed steps do not run again,
and logs identify the active span. Workflow appears after terminal completion.
Span links correlate the two views but do not change parent-child relationships;
CloudWatch does not currently visualize these links.
If spans are missing, check provider warnings, exporter configuration, the layer's
execution wrapper, and sampling.
Workflow requires a terminal invocation to export it. An unresolved parent can
belong to the remote backend or an undelivered fallback root.