Skip to content

Commit aa8f1b6

Browse files
committed
feat(otel): Add OpenTelemetry plugin module with deterministic tracing
1 parent 0cda956 commit aa8f1b6

20 files changed

Lines changed: 1743 additions & 0 deletions

File tree

examples/pom.xml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,25 @@
3131
<version>${project.version}</version>
3232
</dependency>
3333

34+
<!-- OTel Plugin -->
35+
<dependency>
36+
<groupId>software.amazon.lambda.durable</groupId>
37+
<artifactId>aws-durable-execution-sdk-java-otel</artifactId>
38+
<version>${project.version}</version>
39+
</dependency>
40+
41+
<!-- OpenTelemetry SDK (required for OTel example) -->
42+
<dependency>
43+
<groupId>io.opentelemetry</groupId>
44+
<artifactId>opentelemetry-sdk</artifactId>
45+
<version>1.62.0</version>
46+
</dependency>
47+
<dependency>
48+
<groupId>io.opentelemetry</groupId>
49+
<artifactId>opentelemetry-exporter-logging</artifactId>
50+
<version>1.62.0</version>
51+
</dependency>
52+
3453
<!-- AWS Lambda Java Core -->
3554
<dependency>
3655
<groupId>com.amazonaws</groupId>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package software.amazon.lambda.durable.examples.general;
4+
5+
import io.opentelemetry.exporter.logging.LoggingSpanExporter;
6+
import io.opentelemetry.sdk.trace.SdkTracerProvider;
7+
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
8+
import software.amazon.lambda.durable.DurableConfig;
9+
import software.amazon.lambda.durable.DurableContext;
10+
import software.amazon.lambda.durable.DurableHandler;
11+
import software.amazon.lambda.durable.examples.types.GreetingRequest;
12+
import software.amazon.lambda.durable.otel.DeterministicIdGenerator;
13+
import software.amazon.lambda.durable.otel.OpenTelemetryDurablePlugin;
14+
15+
/**
16+
* Example demonstrating OpenTelemetry instrumentation with the Durable Execution SDK.
17+
*
18+
* <p>This handler configures the OTel plugin with:
19+
*
20+
* <ul>
21+
* <li>Deterministic trace/span IDs (all invocations of the same execution share one trace)
22+
* <li>MDC log enrichment (trace_id and span_id in every log line)
23+
* <li>Logging exporter (spans printed to stdout → CloudWatch Logs)
24+
* </ul>
25+
*
26+
* <p>In production, replace {@code LoggingSpanExporter} with {@code OtlpGrpcSpanExporter} to send spans to an OTLP
27+
* collector (X-Ray, Datadog, etc.).
28+
*
29+
* <p>Expected trace structure:
30+
*
31+
* <pre>
32+
* durable.invocation
33+
* ├── durable.step:create-greeting [attempt 1]
34+
* ├── durable.step:create-greeting (operation, backfilled)
35+
* ├── durable.step:transform [attempt 1]
36+
* └── durable.step:transform (operation, backfilled)
37+
* </pre>
38+
*/
39+
public class OtelExample extends DurableHandler<GreetingRequest, String> {
40+
41+
@Override
42+
protected DurableConfig createConfiguration() {
43+
var idGenerator = new DeterministicIdGenerator();
44+
var tracerProvider = SdkTracerProvider.builder()
45+
.setIdGenerator(idGenerator)
46+
.addSpanProcessor(SimpleSpanProcessor.create(LoggingSpanExporter.create()))
47+
.build();
48+
var otelPlugin = new OpenTelemetryDurablePlugin(tracerProvider, idGenerator);
49+
50+
return DurableConfig.builder().withPlugins(otelPlugin).build();
51+
}
52+
53+
@Override
54+
public String handleRequest(GreetingRequest input, DurableContext context) {
55+
// Log with MDC — trace_id and span_id will be in the JSON output
56+
context.getLogger().info("Starting OTel example for {}", input.getName());
57+
58+
var greeting = context.step("create-greeting", String.class, stepCtx -> {
59+
context.getLogger().info("Inside step — this log has trace context in MDC");
60+
return "Hello, " + input.getName();
61+
});
62+
63+
var result = context.step("transform", String.class, stepCtx -> greeting.toUpperCase() + "!");
64+
65+
context.getLogger().info("OTel example complete: {}", result);
66+
return result;
67+
}
68+
}

examples/src/test/java/software/amazon/lambda/durable/examples/CloudBasedIntegrationTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,4 +847,17 @@ void testPluginExample() {
847847
assertNotNull(runner.getOperation("create-greeting"));
848848
assertNotNull(runner.getOperation("transform"));
849849
}
850+
851+
@Test
852+
void testOtelExample() {
853+
var runner =
854+
CloudDurableTestRunner.create(arn("otel-example"), GreetingRequest.class, String.class, lambdaClient);
855+
var result = runner.run(new GreetingRequest("World"));
856+
857+
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
858+
assertEquals("HELLO, WORLD!", result.getResult());
859+
860+
assertNotNull(runner.getOperation("create-greeting"));
861+
assertNotNull(runner.getOperation("transform"));
862+
}
850863
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package software.amazon.lambda.durable.examples.general;
4+
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertNotNull;
7+
8+
import org.junit.jupiter.api.Test;
9+
import software.amazon.lambda.durable.examples.types.GreetingRequest;
10+
import software.amazon.lambda.durable.model.ExecutionStatus;
11+
import software.amazon.lambda.durable.testing.LocalDurableTestRunner;
12+
13+
class OtelExampleTest {
14+
15+
@Test
16+
void testOtelExample_executesSuccessfully() {
17+
var handler = new OtelExample();
18+
var runner = LocalDurableTestRunner.create(GreetingRequest.class, handler);
19+
20+
var result = runner.run(new GreetingRequest("World"));
21+
22+
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
23+
assertEquals("HELLO, WORLD!", result.getResult(String.class));
24+
25+
assertNotNull(result.getOperation("create-greeting"));
26+
assertNotNull(result.getOperation("transform"));
27+
}
28+
}

examples/template.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,17 @@ Resources:
344344
Handler: "software.amazon.lambda.durable.examples.general.PluginExample"
345345
Role: !Ref RoleArn
346346

347+
OtelExampleFunction:
348+
Type: AWS::Serverless::Function
349+
Properties:
350+
FunctionName: !Join
351+
- '-'
352+
- - 'otel-example'
353+
- !Ref JavaVersion
354+
- runtime
355+
Handler: "software.amazon.lambda.durable.examples.general.OtelExample"
356+
Role: !Ref RoleArn
357+
347358
RetryInvokeExampleFunction:
348359
Type: AWS::Serverless::Function
349360
Properties:
@@ -546,6 +557,10 @@ Outputs:
546557
Description: Plugin Example Function ARN
547558
Value: !GetAtt PluginExampleFunction.Arn
548559

560+
OtelExampleFunction:
561+
Description: OTel Example Function ARN
562+
Value: !GetAtt OtelExampleFunction.Arn
563+
549564
RetryInvokeExampleFunction:
550565
Description: Retry Invoke Example Function ARN
551566
Value: !GetAtt RetryInvokeExampleFunction.Arn

otel-plugin/pom.xml

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>software.amazon.lambda.durable</groupId>
9+
<artifactId>aws-durable-execution-sdk-java-parent</artifactId>
10+
<version>1.1.1-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>aws-durable-execution-sdk-java-otel</artifactId>
14+
<name>AWS Lambda Durable Execution SDK - OpenTelemetry Plugin</name>
15+
<description>OpenTelemetry instrumentation plugin for AWS Lambda Durable Execution SDK</description>
16+
17+
<properties>
18+
<opentelemetry.version>1.62.0</opentelemetry.version>
19+
</properties>
20+
21+
<dependencies>
22+
<!-- Durable Execution SDK (for plugin interface) -->
23+
<dependency>
24+
<groupId>software.amazon.lambda.durable</groupId>
25+
<artifactId>aws-durable-execution-sdk-java</artifactId>
26+
<version>${project.version}</version>
27+
</dependency>
28+
29+
<!-- OpenTelemetry API (compile dependency — users bring the SDK implementation) -->
30+
<dependency>
31+
<groupId>io.opentelemetry</groupId>
32+
<artifactId>opentelemetry-api</artifactId>
33+
<version>${opentelemetry.version}</version>
34+
</dependency>
35+
36+
<!-- OpenTelemetry SDK (provided — users bring their own SDK configuration) -->
37+
<dependency>
38+
<groupId>io.opentelemetry</groupId>
39+
<artifactId>opentelemetry-sdk</artifactId>
40+
<version>${opentelemetry.version}</version>
41+
<scope>provided</scope>
42+
</dependency>
43+
44+
<!-- OpenTelemetry Context (transitive from API, but explicit for clarity) -->
45+
<dependency>
46+
<groupId>io.opentelemetry</groupId>
47+
<artifactId>opentelemetry-context</artifactId>
48+
<version>${opentelemetry.version}</version>
49+
</dependency>
50+
51+
<!-- SLF4J for logging -->
52+
<dependency>
53+
<groupId>org.slf4j</groupId>
54+
<artifactId>slf4j-api</artifactId>
55+
</dependency>
56+
57+
<!-- Test dependencies -->
58+
<dependency>
59+
<groupId>org.junit.jupiter</groupId>
60+
<artifactId>junit-jupiter</artifactId>
61+
<scope>test</scope>
62+
</dependency>
63+
<dependency>
64+
<groupId>io.opentelemetry</groupId>
65+
<artifactId>opentelemetry-sdk-testing</artifactId>
66+
<version>${opentelemetry.version}</version>
67+
<scope>test</scope>
68+
</dependency>
69+
<dependency>
70+
<groupId>org.mockito</groupId>
71+
<artifactId>mockito-core</artifactId>
72+
<scope>test</scope>
73+
</dependency>
74+
</dependencies>
75+
76+
<build>
77+
<plugins>
78+
<plugin>
79+
<groupId>org.apache.maven.plugins</groupId>
80+
<artifactId>maven-compiler-plugin</artifactId>
81+
</plugin>
82+
<plugin>
83+
<groupId>org.apache.maven.plugins</groupId>
84+
<artifactId>maven-surefire-plugin</artifactId>
85+
</plugin>
86+
<plugin>
87+
<groupId>com.diffplug.spotless</groupId>
88+
<artifactId>spotless-maven-plugin</artifactId>
89+
</plugin>
90+
</plugins>
91+
</build>
92+
</project>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package software.amazon.lambda.durable.otel;
4+
5+
import io.opentelemetry.context.Context;
6+
7+
/**
8+
* Extracts OTel trace context from the Lambda runtime environment.
9+
*
10+
* <p>Implementations read trace context from various sources (X-Ray trace header, W3C traceparent, etc.) and return an
11+
* OTel {@link Context} that can be used as the parent for invocation spans.
12+
*
13+
* <p>Called once per invocation in {@code onInvocationStart} to establish the parent trace context.
14+
*
15+
* @deprecated This is a preview API that is experimental and may be changed or removed in future releases.
16+
*/
17+
@Deprecated
18+
@FunctionalInterface
19+
public interface ContextExtractor {
20+
21+
/**
22+
* Extracts trace context from the runtime environment.
23+
*
24+
* @return the extracted OTel context, or {@link Context#root()} if no context is available
25+
*/
26+
Context extract();
27+
}

0 commit comments

Comments
 (0)