Skip to content

Add workflow-rooted ExecutionOtelPlugin and fix OtelPlugin status parity - #570

Draft
SilanHe wants to merge 6 commits into
mainfrom
feat/otel-execution-plugin
Draft

Add workflow-rooted ExecutionOtelPlugin and fix OtelPlugin status parity#570
SilanHe wants to merge 6 commits into
mainfrom
feat/otel-execution-plugin

Conversation

@SilanHe

@SilanHe SilanHe commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Issue Link, if available

Description

Port the Workflow-rooted ExecutionOtelPlugin from the Python/JS SDKs into the otel-plugin module, alongside the existing invocation-rooted OtelPlugin:

  • ExecutionOtelPlugin: deterministic Workflow root span (exported once per execution, terminal-only), invocation span as its child, operations parented to the Workflow span and linked to the invocation span; deterministic span IDs stitch suspended/resumed operations into a single logical span.
  • OtelPlugin parity fix: invocation-span status now maps SUCCEEDED/PENDING -> OK and RETRYING/FAILED -> ERROR (was FAILED-only).
  • DeterministicIdGenerator: add setNextSpanId(raw) and generateWorkflowSpanId().
  • SpanAttributes: add durable.execution.status for the Workflow span.
  • Tests: new 16-test ExecutionOtelPluginTest; update OtelPluginTest SUCCEEDED assertion to OK. otel-plugin 105/105, core SDK 1098/1098.

Demo/Screenshots

Checklist

  • I have filled out every section of the PR template
  • I have thoroughly tested this change

Testing

Unit Tests

Have unit tests been written for these changes?

Integration Tests

Have integration tests been written for these changes?

Examples

Has a new example been added for the change? (if applicable)

hsilan added 2 commits July 27, 2026 21:09
Port the Workflow-rooted ExecutionOtelPlugin from the Python/JS SDKs into the
otel-plugin module, alongside the existing invocation-rooted OtelPlugin:

- ExecutionOtelPlugin: deterministic Workflow root span (exported once per
  execution, terminal-only), invocation span as its child, operations parented
  to the Workflow span and linked to the invocation span; deterministic span
  IDs stitch suspended/resumed operations into a single logical span.
- OtelPlugin parity fix: invocation-span status now maps SUCCEEDED/PENDING -> OK
  and RETRYING/FAILED -> ERROR (was FAILED-only).
- DeterministicIdGenerator: add setNextSpanId(raw) and generateWorkflowSpanId().
- SpanAttributes: add durable.execution.status for the Workflow span.
- Tests: new 16-test ExecutionOtelPluginTest; update OtelPluginTest SUCCEEDED
  assertion to OK. otel-plugin 105/105, core SDK 1098/1098.
Rename the invocation-rooted plugin to InvocationOtelPlugin to match the
Python/JS SDK naming, now that ExecutionOtelPlugin (workflow-rooted) exists
alongside it. Prerelease, so no backwards-compat alias is kept.

- git mv OtelPlugin{,Test,IntegrationTest}.java -> InvocationOtelPlugin*.java
- Update all references: MdcSpanEnricher(+Test), ExecutionOtelPlugin javadoc,
  4 example handlers, otel-plugin/README.md, AGENTS.md.
- ExecutionOtelPlugin and all other classes unchanged.
Build: otel-plugin + examples green.
Comment thread otel-plugin/README.md
3. Register `InvocationOtelPlugin` in your handler's `DurableConfig`
4. Grant X-Ray write permissions

### 1. ADOT Lambda Layer

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll update the README in a separate PR

hsilan added 4 commits July 27, 2026 22:59
The workflow root span previously relied on OTel's implicit INTERNAL default;
set it explicitly via setSpanKind(SpanKind.INTERNAL) and add a test asserting
it. Workflow and invocation span status mappings are unchanged (already:
Workflow SUCCEEDED->OK/FAILED->ERROR, non-terminal->UNSET/not-exported;
Invocation SUCCEEDED+PENDING->OK, RETRYING+FAILED->ERROR).
Match the JS/Python ExecutionOtelPlugin export-once semantics: onInvocationEnd
no longer force-ends still-open operation spans as PENDING. An operation left
open when the invocation suspends is materialized exactly once (deterministic
span ID + link to the completing invocation) when onOperationEnd fires later.
Per-invocation state is still reset and lingering attempt scopes are closed
defensively, but their spans are no longer force-ended. InvocationOtelPlugin
(legacy, invocation-rooted) keeps its PENDING-at-end behavior unchanged.

Tests: replace operationNotCompleted_endedPendingAtInvocationEnd with
operationNotCompleted_notEndedAtInvocationEnd and add
operationOpenedThenCompletedNextInvocation_exportedOnceOnOperationEnd.
Invocation span status now maps SUCCEEDED/PENDING -> OK, FAILED -> ERROR, and
RETRYING -> UNSET (previously ERROR), in both ExecutionOtelPlugin and
InvocationOtelPlugin. RETRYING is left UNSET because the plugin interface does
not expose whether the invocation/workflow was STOPPED or TIMED_OUT versus
retried for a transient error, so ERROR cannot be asserted reliably.

Workflow/execution span mapping is unchanged (FAILED->ERROR, SUCCEEDED->OK,
RETRY/PENDING->UNSET via terminal-only export) and the workflow span kind
remains explicitly INTERNAL. Class javadoc updated; tests updated/added for the
RETRYING->UNSET behavior in both plugins.
The prior comment said the continuation span 'stitches to the original' with
the same deterministic ID. That is misleading: OTel spans cannot be edited
after export, and the prior invocation drops its in-memory span un-exported.
This branch is simply the single export of the operation's span, using its
deterministic ID. Comment-only change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants