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
1 change: 1 addition & 0 deletions .github/scripts/maven_publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@ echo "=== Step 3: Upload to Sonatype Central Portal ==="
mvn clean deploy -s "${SETTINGS_FILE}" -pl sdk -P publishing -DskipTests --no-transfer-progress
mvn clean deploy -s "${SETTINGS_FILE}" -pl sdk-testing -P publishing -DskipTests --no-transfer-progress
mvn clean deploy -s "${SETTINGS_FILE}" -pl otel-plugin -P publishing -DskipTests --no-transfer-progress
mvn clean deploy -s "${SETTINGS_FILE}" -pl insight-plugin -P publishing -DskipTests --no-transfer-progress

echo "=== Release ${RELEASE_VERSION} uploaded successfully; review and publish it in Sonatype Central Portal. ==="
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ on:
- 'sdk/**'
- 'sdk-testing/**'
- 'sdk-integration-tests/**'
- 'insight-plugin/**'
- 'examples/**'
- 'pom.xml'
push:
Expand All @@ -40,6 +41,7 @@ on:
- 'sdk/**'
- 'sdk-testing/**'
- 'sdk-integration-tests/**'
- 'insight-plugin/**'
- 'examples/**'
- 'pom.xml'

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish_maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ jobs:
"sdk/target/aws-durable-execution-sdk-java-${RELEASE_VERSION}.jar" \
"sdk-testing/target/aws-durable-execution-sdk-java-testing-${RELEASE_VERSION}.jar" \
"otel-plugin/target/aws-durable-execution-sdk-java-plugin-otel-${RELEASE_VERSION}.jar" \
"insight-plugin/target/aws-durable-execution-sdk-java-plugin-insight-${RELEASE_VERSION}.jar" \
--clobber

- name: Checkout default branch
Expand Down
9 changes: 5 additions & 4 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ The publication workflow:

1. Verifies that the tag is a semantic version, points to a commit on the
default branch, and matches the Maven version in the tagged POM.
2. Builds, signs, and uploads the SDK, testing library, and OpenTelemetry plugin
to Sonatype Central Portal.
3. Uploads the three JARs to the existing GitHub release.
2. Builds, signs, and uploads the SDK, testing library, OpenTelemetry plugin,
and Workflow Insight plugin to Sonatype Central Portal.
3. Uploads the four JARs to the existing GitHub release.
4. Opens a pull request for the next development version. A final release
increments the patch version, so `2.1.1` produces `2.1.2-SNAPSHOT`. A
prerelease keeps the same base version, so `2.1.1-rc1` produces
Expand All @@ -56,7 +56,8 @@ After **Publish Maven Release** succeeds:
1. Open [Publishing Deployments](https://central.sonatype.com/publishing/deployments)
in Sonatype Central Portal.
2. Find the deployments for the release version and verify that they contain
the expected SDK, testing library, and OpenTelemetry plugin artifacts.
the expected SDK, testing library, OpenTelemetry plugin, and Workflow Insight
plugin artifacts.
3. Click **Publish** for each deployment and wait for publication to complete.
The workflow uses `autoPublish=false`, so this manual action is required.
4. Confirm that the GitHub release contains the expected JARs and that the
Expand Down
101 changes: 101 additions & 0 deletions insight-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Workflow Insight Plugin (preview)

Instrumentation plugin for the AWS Lambda Durable Execution Java SDK that emits a curated,
per-execution **Workflow Insight record** to one or more pluggable exporters. It ports the
JavaScript `workflowInsight()` contract (canonical record schema `1.0`) to the Java plugin hook
surface.

> **Preview API.** Every public type is annotated
> `@software.amazon.lambda.durable.annotations.Experimental` to signal it is experimental and may
> change or be removed in a future release without a major-version bump.

## Usage

```java
DurableConfig config = DurableConfig.builder()
.withPlugins(WorkflowInsight.workflowInsight(WorkflowInsightConfig.builder()
.samplingRate(1.0)
.emitMode(WorkflowInsightConfig.EmitMode.ON_COMPLETE) // ON_COMPLETE | ON_CHANGE | ON_FAILURE
.operationDetail(WorkflowInsightConfig.OperationDetail.TOP_LEVEL) // TOP_LEVEL | FULL_TREE
.content(ContentConfig.builder()
.input(true).output(true).includeErrors(true)
.addOverride(OperationOverride.withResult("compute", r -> r))
.build())
.addExporter(S3Exporter.builder().bucket("my-bucket").build())
.build()))
.build();
```

Exporters: `LambdaLogExporter` (default; writes the `operationsByName` map to stdout →
CloudWatch), `S3Exporter` (canonical `operations` array, one object per execution),
`CloudWatchLogsExporter` (PutLogEvents to a specific log group, `operationsByName` map). Implement
`InsightExporter` for custom sinks.

`LambdaLogExporter` needs no extra dependency. The AWS SDK service modules used by the remote
exporters are optional so applications that use only Lambda logs do not package them. Add the module
for each remote exporter you configure, using the AWS SDK for Java 2.x version managed by your
application:

```xml
<!-- Required only for S3Exporter -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>AWS_SDK_VERSION</version>
</dependency>

<!-- Required only for CloudWatchLogsExporter -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>cloudwatchlogs</artifactId>
<version>AWS_SDK_VERSION</version>
</dependency>
```

## Design

- **Snapshot-based, not accumulated.** Each record is built directly from the current-invocation
operation snapshot the SDK provides — `InvocationInfo.operations()` at start / operation change
and `InvocationEndInfo.operations()` at end. Execution input/output come from
`InvocationInfo.executionInput()` / `InvocationEndInfo.executionResult()`, and per-operation
results from `OperationChangeItemInfo.result()` (all surfaced by SDK PR #618). There is no global
"current ARN" or cross-hook operation accumulation.
- **Per-execution state keyed by execution ARN** holds the stable start time, parsed ARN, cached
input, and deterministic sampling decision for the current invocation. State is removed after
every invocation end, including PENDING/RETRYING, and recreated from stable hook data when the
execution resumes.
- **Deterministic sampling.** FNV-1a-32 over the execution ARN mapped into `[0,1)`, identical to
the JS implementation, so a resumed execution always reaches the same in/out decision.
- **Emission modes.** `ON_COMPLETE` emits one terminal record; `ON_FAILURE` emits only on terminal
failure; `ON_CHANGE` emits at invocation start, on every operation change, and at invocation end
(matching JS). Non-terminal statuses map to `RUNNING`.
- **Operation filtering** mirrors JS: the `EXECUTION` pseudo-operation and unnamed operations are
dropped; `TOP_LEVEL` detail drops any operation with a `parentId`; an `OperationOverride.exclude`
drops by name. Operation `result` is included only when an `OperationOverride.withResult`
transform opts in — the checkpointed JSON is parsed before the transform, falling back to the raw
string, and a throwing transform omits the field.
- **Content transforms receive detached, JSON-compatible values.** The `input`/`output` transforms
and an `OperationOverride.withResult` transform never receive the SDK's original Java object: a
POJO is presented as a `Map`, a list as a `List`, and a Java-time type as its JSON representation
(e.g. an `Instant` arrives as an ISO-8601 `String`). This is the maintainer's minimum unblock —
it is deliberately *not* a type-preserving clone. Mutating the argument is safe (it cannot corrupt
the cached input snapshot or any later emission), and a transform that throws omits the field and
logs the failure rather than silently dropping it or failing the execution.
- **`includeErrors`** gates **both** the execution-level error and each operation-level error; with
`includeErrors(false)` neither is emitted, so a sensitive failure message never reaches a record.
- **Plugin failures never disrupt execution.** Every plugin-owned boundary — record construction,
input snapshotting, transforms, truncation, and each exporter's render/export/flush (including a
`NoClassDefFoundError` from an optional exporter's absent SDK) — is guarded against any `Throwable`
and logged, so one failing exporter cannot block the others and no plugin fault propagates into
the durable execution.
- **Per-exporter size truncation** (`Truncation`) drops, in order: operation results oldest-first,
then whole operations oldest-first, then execution input, then output — setting `truncated`,
`droppedOperations`, `droppedInput`, `droppedOutput` as applicable. The size is measured against
the exact shape each exporter emits (its `render`).
- **Exporter isolation.** Every exporter is truncated, exported, and flushed independently; a
failing exporter is logged and never blocks the others or the execution.

## Conformance

Validated against the Workflow Insight conformance suite behaviors `insight-1 … insight-18`
(PR #73 Java examples). See the module tests for the behavior mapping.
114 changes: 114 additions & 0 deletions insight-plugin/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>software.amazon.lambda.durable</groupId>
<artifactId>aws-durable-execution-sdk-java-parent</artifactId>
<version>2.2.1-SNAPSHOT</version>
</parent>

<artifactId>aws-durable-execution-sdk-java-plugin-insight</artifactId>
<name>AWS Lambda Durable Execution SDK Workflow Insight Plugin</name>
<description>Workflow Insight instrumentation plugin for AWS Lambda Durable Execution SDK</description>

<dependencies>
<!-- Durable Execution SDK (for the plugin interface) -->
<dependency>
<groupId>software.amazon.lambda.durable</groupId>
<artifactId>aws-durable-execution-sdk-java</artifactId>
<version>${project.version}</version>
</dependency>

<!-- Jackson for wire-format JSON serialization (camelCase, identical to JS) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

<!-- Java-time (Instant/Duration) support so SDK-default payload types serialize as ISO-8601 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

<!-- AWS SDK exporters (S3 + CloudWatch Logs) -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>cloudwatchlogs</artifactId>
<optional>true</optional>
</dependency>

<!-- SLF4J for logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>software.amazon.lambda.durable</groupId>
<artifactId>aws-durable-execution-sdk-java-testing</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.amazon.lambda.durable.insight;

import software.amazon.lambda.durable.annotations.Experimental;

/**
* Parses the durable execution ARN into its component fields, mirroring the JS {@code parseExecutionArn}.
*
* <p>Format:
* {@code arn:<partition>:lambda:<region>:<accountId>:function:<functionName>:<qualifier>/durable-execution/<executionName>/<invocationId>}.
*/
@Experimental
public final class ArnParser {
private final String functionName;
private final String qualifier;
private final String region;
private final String accountId;
private final String executionName;

private ArnParser(String functionName, String qualifier, String region, String accountId, String executionName) {
this.functionName = functionName;
this.qualifier = qualifier;
this.region = region;
this.accountId = accountId;
this.executionName = executionName;
}

public static ArnParser parse(String executionArn) {
String[] parts = executionArn.split(":", -1);
String lastPart = parts.length > 7 ? parts[7] : "";
String[] segments = lastPart.split("/", -1);
return new ArnParser(
parts.length > 6 ? parts[6] : "",
segments.length > 0 ? segments[0] : "",
parts.length > 3 ? parts[3] : "",
parts.length > 4 ? parts[4] : "",
segments.length > 2 ? segments[2] : "");
}

public String functionName() {
return functionName;
}

public String qualifier() {
return qualifier;
}

public String region() {
return region;
}

public String accountId() {
return accountId;
}

public String executionName() {
return executionName;
}
}
Loading
Loading