Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
/**
* Shared instrumentation plugin for the plugin conformance suite.
*
* <p>Emits lifecycle log lines with a configurable prefix (e.g. {@code CONFPLUGIN}, {@code CONFPLUGIN-A}) so one
* plugin — or two, for the multiple-plugins case — can be registered on a handler. Operation- and attempt-level hooks
* are filtered to step-type operations to match the requirement vocabulary. All lines are emitted from the real SDK
* plugin hooks; nothing is hand-rolled.
* <p>Emits lifecycle log lines with a configurable prefix (e.g. {@code CONFPLUGIN}, {@code CONFPLUGIN-A}) so one plugin
* — or two, for the multiple-plugins case — can be registered on a handler. Operation- and attempt-level hooks are
* filtered to step-type operations to match the requirement vocabulary. All lines are emitted from the real SDK plugin
* hooks; nothing is hand-rolled.
*/
@SuppressWarnings("deprecation")
public class ConformanceLoggingPlugin implements DurableExecutionPlugin {
Expand Down
24 changes: 12 additions & 12 deletions conformance-tests/src/main/java/plugin/FaultyConformancePlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,50 +35,50 @@ private String arnField() {
@Override
public void onInvocationStart(InvocationInfo info) {
this.executionArn = info.durableExecutionArn();
System.out.println(String.format(
"{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"invocation-start\"%s}", arnField()));
System.out.println(
String.format("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"invocation-start\"%s}", arnField()));
throw new RuntimeException("faulty invocation-start");
}

@Override
public void onInvocationEnd(InvocationEndInfo info) {
System.out.println(String.format(
"{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"invocation-end\"%s}", arnField()));
System.out.println(
String.format("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"invocation-end\"%s}", arnField()));
throw new RuntimeException("faulty invocation-end");
}

@Override
public void onOperationStart(OperationInfo info) {
if (isStep(info.type())) {
System.out.println(String.format(
"{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"operation-start\"%s}", arnField()));
System.out.println(
String.format("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"operation-start\"%s}", arnField()));
throw new RuntimeException("faulty operation-start");
}
}

@Override
public void onOperationEnd(OperationEndInfo info) {
if (isStep(info.type())) {
System.out.println(String.format(
"{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"operation-end\"%s}", arnField()));
System.out.println(
String.format("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"operation-end\"%s}", arnField()));
throw new RuntimeException("faulty operation-end");
}
}

@Override
public void onUserFunctionStart(UserFunctionStartInfo info) {
if (isStep(info.type())) {
System.out.println(String.format(
"{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"attempt-start\"%s}", arnField()));
System.out.println(
String.format("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"attempt-start\"%s}", arnField()));
throw new RuntimeException("faulty attempt-start");
}
}

@Override
public void onUserFunctionEnd(UserFunctionEndInfo info) {
if (isStep(info.type())) {
System.out.println(String.format(
"{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"attempt-end\"%s}", arnField()));
System.out.println(
String.format("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"attempt-end\"%s}", arnField()));
throw new RuntimeException("faulty attempt-end");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ public class PluginErrorIsolation extends DurableHandler<String, String> {

@Override
protected DurableConfig createConfiguration() {
return DurableConfig.builder().withPlugins(new FaultyConformancePlugin()).build();
return DurableConfig.builder()
.withPlugins(new FaultyConformancePlugin())
.build();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
* 10-1: Plugin invocation lifecycle hooks (start and end on a single invocation).
*
* <p>A single greeting step configured with {@link ConformanceLoggingPlugin}. The plugin's invocation-start hook logs
* {@code first=true} before the handler runs and its invocation-end hook logs {@code status=SUCCEEDED} after the
* result is finalized. The step body logs its running line via the context logger.
* {@code first=true} before the handler runs and its invocation-end hook logs {@code status=SUCCEEDED} after the result
* is finalized. The step body logs its running line via the context logger.
*/
@SuppressWarnings("deprecation")
public class PluginInvocationLifecycle extends DurableHandler<String, String> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ private String arnField() {
@Override
public void onInvocationStart(InvocationInfo info) {
this.executionArn = info.durableExecutionArn();
System.out.println(String.format(
"{\"plugin\": \"%s\", \"hook\": \"invocation-start\"%s}", prefix, arnField()));
System.out.println(
String.format("{\"plugin\": \"%s\", \"hook\": \"invocation-start\"%s}", prefix, arnField()));
}

@Override
Expand All @@ -53,8 +53,7 @@ public void onInvocationEnd(InvocationEndInfo info) {
@Override
protected DurableConfig createConfiguration() {
return DurableConfig.builder()
.withPlugins(
new InvocationLoggingPlugin("CONFPLUGIN-A"), new InvocationLoggingPlugin("CONFPLUGIN-B"))
.withPlugins(new InvocationLoggingPlugin("CONFPLUGIN-A"), new InvocationLoggingPlugin("CONFPLUGIN-B"))
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
* 10-2: Plugin operation lifecycle hooks (step start and terminal end).
*
* <p>A single greeting step configured with {@link ConformanceLoggingPlugin}. The plugin, filtering to step-type
* operations, logs {@code operation-start} when the step's STARTED checkpoint is observed and
* {@code operation-end status=SUCCEEDED} when the step reaches its terminal status.
* operations, logs {@code operation-start} when the step's STARTED checkpoint is observed and {@code operation-end
* status=SUCCEEDED} when the step reaches its terminal status.
*/
@SuppressWarnings("deprecation")
public class PluginOperationLifecycle extends DurableHandler<String, String> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
/**
* 10-7: Plugin invocation-end hook receives FAILED status when execution fails.
*
* <p>A single step that always throws, configured with no retries and {@link ConformanceLoggingPlugin}. The plugin
* logs {@code invocation-start first=true}; the step throws, no retry is attempted, and the plugin's invocation-end
* hook fires with {@code status=FAILED}.
* <p>A single step that always throws, configured with no retries and {@link ConformanceLoggingPlugin}. The plugin logs
* {@code invocation-start first=true}; the step throws, no retry is attempted, and the plugin's invocation-end hook
* fires with {@code status=FAILED}.
*/
@SuppressWarnings("deprecation")
public class PluginTerminalFailure extends DurableHandler<Object, String> {
Expand Down
55 changes: 45 additions & 10 deletions examples/generate-template.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import argparse
import re
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from pathlib import Path

Expand All @@ -13,6 +14,17 @@
EXAMPLE_PACKAGE_ROOT = SOURCE_ROOT / "software/amazon/lambda/durable/examples"
DEFAULT_OUTPUT = EXAMPLES_DIR / "template.yaml"
TEMPLATE_ANNOTATION = "ExampleTemplate"
POM_NAMESPACE = {"m": "http://maven.apache.org/POM/4.0.0"}


def read_otel_plugin_jar_path() -> str:
root = ET.parse(EXAMPLES_DIR / "pom.xml").getroot()
version = root.findtext("m:version", namespaces=POM_NAMESPACE)
if version is None:
version = root.findtext("m:parent/m:version", namespaces=POM_NAMESPACE)
if version is None:
raise ValueError("Unable to read examples version from pom.xml")
return f"/var/task/lib/aws-durable-execution-sdk-java-plugin-otel-{version}.jar"


@dataclass(frozen=True)
Expand All @@ -22,6 +34,7 @@ class ExampleFunction:
suffix: str
condition: str | None
tracing: bool
java_agent: bool

@property
def logical_id(self) -> str:
Expand Down Expand Up @@ -58,24 +71,26 @@ def is_top_level_durable_handler(source: str, class_name: str) -> bool:
return bool(match and "extends DurableHandler" in match.group("header"))


def read_template_annotation(source: str, class_name: str) -> tuple[str | None, bool]:
def read_template_annotation(source: str, class_name: str) -> tuple[str | None, bool, bool]:
class_match = re.search(rf"public\s+(?:final\s+)?class\s+{class_name}\b", source)
if not class_match:
return None, False
return None, False, False

prefix = source[: class_match.start()]
matches = list(
re.finditer(rf"@(?:[A-Za-z_][\w.]*\.)?{TEMPLATE_ANNOTATION}\s*(?:\((?P<body>.*?)\))?", prefix, re.DOTALL)
)
if not matches:
return None, False
return None, False, False

body = matches[-1].group("body") or ""
condition_match = re.search(r'condition\s*=\s*"([^"]+)"', body)
tracing_match = re.search(r"tracing\s*=\s*(true|false)", body)
java_agent_match = re.search(r"javaAgent\s*=\s*(true|false)", body)
condition = condition_match.group(1) if condition_match else None
tracing = tracing_match.group(1) == "true" if tracing_match else False
return condition, tracing
java_agent = java_agent_match.group(1) == "true" if java_agent_match else False
return condition, tracing, java_agent


def discover_examples() -> list[ExampleFunction]:
Expand All @@ -86,7 +101,7 @@ def discover_examples() -> list[ExampleFunction]:
if not is_top_level_durable_handler(source, class_name):
continue

condition, tracing = read_template_annotation(source, class_name)
condition, tracing, java_agent = read_template_annotation(source, class_name)
package_name = read_package(source, path)
examples.append(
ExampleFunction(
Expand All @@ -95,12 +110,13 @@ def discover_examples() -> list[ExampleFunction]:
suffix=kebab_case(class_name),
condition=condition,
tracing=tracing,
java_agent=java_agent,
)
)
return examples


def emit_function(lines: list[str], example: ExampleFunction) -> None:
def emit_function(lines: list[str], example: ExampleFunction, java_agent_extension_path: str) -> None:
lines.extend(
[
f" {example.logical_id}:",
Expand All @@ -123,9 +139,19 @@ def emit_function(lines: list[str], example: ExampleFunction) -> None:
[
" Tracing: Active",
" Layers:",
" - !Sub",
" - arn:aws:lambda:${AWS::Region}:901920570463:layer:aws-otel-java-agent-${AdotArch}-ver-1-32-0:6",
" - AdotArch: amd64",
" - !Ref AdotLayerArn",
]
)
if example.java_agent:
lines.extend(
[
" LoggingConfig:",
" LogFormat: JSON",
" Environment:",
" Variables:",
" AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-instrument",
f' JAVA_TOOL_OPTIONS: "-Dotel.javaagent.extensions={java_agent_extension_path}"',
f" OTEL_JAVAAGENT_EXTENSIONS: {java_agent_extension_path}",
]
)
lines.append("")
Expand All @@ -151,6 +177,7 @@ def emit_log_group(lines: list[str], example: ExampleFunction) -> None:


def render_template(examples: list[ExampleFunction]) -> str:
java_agent_extension_path = read_otel_plugin_jar_path()
lines = [
"# This file is generated by examples/generate-template.py. Do not edit it by hand.",
'AWSTemplateFormatVersion: "2010-09-09"',
Expand All @@ -176,6 +203,14 @@ def render_template(examples: list[ExampleFunction]) -> str:
" RoleArn:",
" Type: String",
" Description: IAM Role ARN for Lambda function execution",
" AdotLayerArn:",
" Type: String",
" Default: arn:aws:lambda:us-west-2:615299751070:layer:AWSOpenTelemetryDistroJava:16",
" Description: >-",
" ARN of the ADOT (AWS Distro for OpenTelemetry) Lambda layer used by the tracing examples.",
" The layer is regional: its account ID and version vary by region, so override this per",
" deployment region. The default targets us-west-2 (the region used by e2e tests); CI",
" resolves the latest ARN for its region.",
"",
"Conditions:",
" IsJava21OrLater:",
Expand All @@ -202,7 +237,7 @@ def render_template(examples: list[ExampleFunction]) -> str:

for example in examples:
emit_log_group(lines, example)
emit_function(lines, example)
emit_function(lines, example, java_agent_extension_path)

lines.append("Outputs:")
for index, example in enumerate(examples):
Expand Down
36 changes: 24 additions & 12 deletions examples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,6 @@
<version>1.64.0</version>
</dependency>

<!-- OTLP gRPC exporter (sends spans to ADOT collector layer at localhost:4317) -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
<version>1.64.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.82.2</version>
</dependency>

<!-- AWS Lambda Java Core -->
<dependency>
<groupId>com.amazonaws</groupId>
Expand Down Expand Up @@ -154,6 +142,30 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.9.0</version>
<executions>
<execution>
<id>copy-otel-javaagent-extension</id>
<phase>prepare-package</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>${project.groupId}</groupId>
<artifactId>aws-durable-execution-sdk-java-plugin-otel</artifactId>
<version>${project.version}</version>
<outputDirectory>${project.build.outputDirectory}/lib</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@
String condition() default "";

boolean tracing() default false;

boolean javaAgent() default false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.amazon.lambda.durable.examples.otel;

import software.amazon.lambda.durable.DurableConfig;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.examples.ExampleTemplate;
import software.amazon.lambda.durable.examples.types.GreetingRequest;
import software.amazon.lambda.durable.otel.InvocationOtelPlugin;

/**
* OTel + X-Ray example that uses the no-arg plugin constructor.
*
* <p>{@link InvocationOtelPlugin#InvocationOtelPlugin()} uses the global provider initialized by the ADOT Java agent
* with deterministic span ID generation installed by the plugin's OpenTelemetry autoconfigure SPI.
*/
@ExampleTemplate(tracing = true, javaAgent = true)
public class OtelXRayDefaultConstructorExample extends DurableHandler<GreetingRequest, String> {

@Override
protected DurableConfig createConfiguration() {
return DurableConfig.builder().withPlugins(new InvocationOtelPlugin()).build();
}

@Override
public String handleRequest(GreetingRequest input, DurableContext context) {
context.getLogger().info("Starting OTel X-Ray default constructor example for {}", input.getName());

var greeting = context.step("default-create-greeting", String.class, stepCtx -> "Hello, " + input.getName());

var result = context.step("default-transform", String.class, stepCtx -> greeting.toUpperCase() + "!");

context.getLogger().info("OTel X-Ray default constructor example complete: {}", result);
return result;
}
}
Loading
Loading