Repository navigation
Description
Context
The current OpenTelemetry conformance tests were initially derived from the Python SDK's telemetry behavior. Python therefore appears most conformant by construction. This issue documents the differences observed across the three SDKs so that we can decide which behaviors belong in a language-neutral contract and which are SDK defects.
The Python and TypeScript evidence below is unchanged from the previous runs because those SDK versions have not changed. Java was refreshed against SDK main at fd3505b (2.1.1-SNAPSHOT). Each ADOT + X-Ray job ran the same 19 OpenTelemetry scenarios:
| SDK | Run | Result | Scenarios with correlated telemetry |
|---|---|---|---|
| Python | 29985063295 | 0/19 passed | 19/19 |
Java (main) |
30027042347 | 0/19 passed | 19/19 |
| TypeScript | 29986336955 | 0/19 passed | 16/19 |
All three listed jobs failed their current conformance assertions. In the refreshed Java job, the SDK checkout, sdk and otel-plugin installation, and handler compilation all succeeded; the job failed while evaluating the telemetry scenarios. The Python community-layer/Dash0/Datadog jobs were skipped, so those exporters are outside this comparison.
Python
Python's durable operation and attempt spans match the current test model: expected attributes are present, operation and attempt spans use the expected parent, and failed attempts are exported as OpenTelemetry ERROR with durable.attempt.outcome=FAILED.
Its failures reduce to invocation-span semantics:
- All 28 invocation spans use
span.kind=INTERNAL; the tests expectSERVER. - All 28 invocation spans have OpenTelemetry status
OK, including failed handler invocations; the tests expectERRORfor failures. - The ambiguous selections in
otel-17andotel-18are downstream effects of failed and successful invocations all having statusOK.
Aside from those two invocation properties, the Python telemetry satisfies the current assertions.
Java
The SDK main run did not resolve the differences previously observed with the Maven Central 2.1.0 release. Java exports correlated telemetry for all 19 scenarios, and its invocation spans behave better than Python in two respects: all 28 are SERVER, and failed invocations are exported as ERROR (9 ERROR, 19 OK).
The remaining shape differs substantially from the Python-derived model:
- Invocation spans add
durable.invocation.firstanddurable.invocation.status. The current exact-key assertion permits onlydurable.execution.arn. - In-progress operations use
durable.operation.status=PENDINGrather thanSTARTED. - Attempt spans are children of operation spans. The current model expects operation and attempt spans to share the enclosing invocation/context parent.
- All 44 operation lifecycle spans omit
durable.attempt.number. - All 17 attempt spans omit
durable.operation.subtypeanddurable.operation.status. - All 17 attempt spans have OpenTelemetry status
OK; 14 emitdurable.attempt.outcome=SUCCEEDED, 3 omit the outcome, and none emitFAILED. - In the retry scenario (
otel-3), attempt 1 is exported asOK/SUCCEEDEDeven though it failed, while attempt 2 omitsdurable.attempt.outcome. - Similar parent and attribute differences occur for child contexts, parallel branches, map iterations, callbacks, invokes, and wait-for-condition attempts.
Some Java differences may be intentional extensions or modeling choices. In particular, the invocation attributes could be valid additions if the cross-SDK contract permits extensible durable.* attributes.
TypeScript
TypeScript has both telemetry-shape differences and execution/export failures:
- The exported invocation wrapper is
SERVER, butspan.nameis the Lambda function name rather thaninvocation. - Invocation spans have no
durable.execution.arn, so the harness reports zero matching SDK invocation spans. - All observed invocation spans have OpenTelemetry status
OK, including failure paths. - Operation spans never include
durable.operation.status. - Attempt numbering uses
durable.operation.attemptrather thandurable.attempt.number. - Attempt spans are children of operation spans rather than siblings under the enclosing invocation/context.
otel-10andotel-17do not complete because the harness cannot match the anonymous callback action; both time out.otel-19produces no correlated X-Ray trace when handler execution fails.
Only 16 of the 19 scenarios produced normalized telemetry artifacts. The callback failures and missing failed-handler trace are functional gaps independent of the exact telemetry schema.
Contract decisions needed
- Define the language-neutral invocation contract: logical span name, span kind, required attributes, and failure status.
- Decide whether additional
durable.*attributes are allowed or exact-key matching remains required. - Define operation/attempt parentage and whether both spans repeat operation subtype, status, and attempt number.
- Standardize lifecycle vocabulary such as
STARTEDversusPENDING. - Standardize failure mapping for invocation, operation, and attempt spans.
- Update the conformance requirements to describe the agreed contract independently of any SDK implementation.
- Track SDK-specific fixes only after intentional differences have been separated from contract violations.
Completion criteria
- The language-neutral span contract is documented.
- Tests distinguish required fields from permitted extensions.
- Intentional SDK differences are represented explicitly in the requirements.
- Remaining Python, Java, and TypeScript deviations are tracked as implementation defects.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
SilanHe commentedon Jul 23, 2026
PluginOperationStatusneeds to be implemented for all operations (durable.operation.status). Javascript is missing the field for certain onOperationEnd calls. Java Otel plugin doesn't need to explicitly update spans withPluginOperationStatus.PENDINGin onInvocationEnd for operations that have not yet completed. We're assuming that they would just bePluginOperationStatus.STARTED. Double check for Wait-for-condition.durable.attempt.numberto denote the total number of attempts that happened. Should only be emitted after the operation is completed.durable.attempt.outcomeattribute. Specifically, we seem to be missing theFAILEDoutcomes.durable.operation.attemptattribute todurable.attempt.numberin attempts span.zhongkechen commentedon Jul 23, 2026
Fix of invocation span kind and attributes in Python SDK: aws/aws-durable-execution-sdk-python#577
zhongkechen commentedon Jul 24, 2026
Detailed community layer + S3 collector comparison
This compares the three community-layer/S3 jobs at the same PR head,
f7c17c7. That revision includes the canonical invocation-counting fix and runs the original 19 OpenTelemetry scenarios.All three jobs built and deployed the same custom OpenTelemetry collector with the S3 exporter. The differing results are therefore comparisons of the SDK/community-layer telemetry carried by the same raw OTLP path.
Python: 18/19
otel-1throughotel-18all pass. This covers successful and failed operations, multiple physical invocations, retries, child/parallel/map contexts, callbacks, wait-for-condition, and chained invokes. The invocation-counting fix also eliminates all count mismatches, including when the community layer does not provide afaas.invocation_id.The sole failure is
otel-19:invocationspan is present withkind=INTERNAL, OpenTelemetry statusERROR, the execution ARN, anddurable.invocation.first=true.durable.invocation.status=FAILED.durable.invocation.status=RETRY, so the otherwise matching span is not selected and is subsequently reported as uncovered.This is a lifecycle-value mismatch between the requirement and the Python plugin, not a collector, correlation, or invocation-count problem.
Java: 0/19, with correlated telemetry for every case
Java produces correlated telemetry for all 19 scenarios and does not have any invocation-count failures. Across the uploaded traces, all 26
invocationspans have unique invocation IDs plusdurable.invocation.firstanddurable.invocation.status.The failures come from a shared telemetry shape that differs from the current Python-derived contract:
SERVER, while the requirements expectINTERNAL.ERROR/FAILED, but successful and pending invocation spans use OpenTelemetry statusUNSETrather thanOK.durable.operation.status=PENDINGrather thanSTARTED.durable.attempt.number.UNSET; 12 reportdurable.attempt.outcome=SUCCEEDED, five omit the outcome, and none report a failed outcome.durable.operation.subtypeanddurable.operation.status.Thus Java's 0/19 result does not mean telemetry is absent. Its invocation lifecycle is complete enough to count correctly, but invocation kind/status, operation/attempt attributes, failure outcomes, hierarchy, and some multi-context correlation differ from the current assertions.
TypeScript: 0/19, with operation telemetry but no canonical invocation view
TypeScript produces normalized telemetry artifacts for 16 scenarios, but none contains either a canonical
invocationspan or an invocation-ID attribute:SERVER, and carries no durable execution or invocation lifecycle attributes.otel-2,otel-3,otel-9,otel-11, andotel-18) consequently report zero recognized invocations.INTERNAL, but all 28 omitdurable.operation.status.durable.operation.attemptrather thandurable.attempt.number.SUCCEEDEDand fiveFAILED. Four failed attempts areERROR; theotel-9condition attempt remainsUNSET.otel-10andotel-17time out because the anonymous callback action cannot be matched.otel-19produces no execution-correlated trace within the 90-second lookup window.TypeScript therefore has a useful operation/attempt model, including correct failed-attempt status and outcome, but it lacks the logical invocation model and cross-invocation correlation required by the current contract.
Cross-SDK comparison
FAILEDversusRETRYlifecycle value inotel-19.The main contract decisions remain invocation kind/status,
FAILEDversusRETRY, operation/attempt parentage, repeated attributes across operation and attempt spans, lifecycle vocabulary, and whether linked physical invocation spans must join the execution-correlated trace.fix(otel): cross sdk behaviour alignment (#756)
SilanHe commentedon Jul 27, 2026
Want to document the following requirement as well:
In execution view, execution/workflow span status can use this mapping from PluginInvocationStatus to span Status:
FAILED -> ERROR
SUCCEEDED -> OK
RETRY/PENDING -> UNSET
which reflects the execution status
For invocation span status, we want to use the following mapping from PluginInvocationStatus to span Status:
SUCCEEDED/PENDING -> OK
RETRY -> UNSET
FAILED -> ERROR
We also want to document that the plugin interface doesn't know if an invocation.workflow was STOPPED or TIMED_OUT, which is also why RETRY maps to UNSET. The workflow span should be INTERNAL as well (span.kind=INTERNAL).
feat(otel): workflow span status (#764)
SilanHe commentedon Jul 28, 2026
Both the InvocationOtelPlugin and the ExecutionOtelPlugin should create a "Workflow" span. Capitalize first letter of the names for the "Invocation" and "Workflow" spans
feat(otel): capitalize InvocationOtelPlugin invocation span name (#769)
SilanHe commentedon Jul 31, 2026
I think we should align on Java behaviour here:
invocation_plugin.py)InvocationOtelPlugin, #572)STARTEDSUCCEEDEDSTARTEDSTARTEDSTARTEDSTARTEDSTARTEDoperation.status=STARTED+attempt.outcomeoperation.status; onlyattempt.outcomeSTARTED+attempt.outcomeSTARTEDon attempt?This will require an update to conformance tests, Python and JS
SilanHe commentedon Aug 4, 2026
Operation start and end time should be constrained within Invocation spans start and end times
SilanHe commentedon Aug 4, 2026
Plugin configuration differences:
ExecutionInvocationtracerProvider(builtTracerProvider)tracer_providertrace_providertracerProviderBuilder(a builder; plugin calls.setIdGenerator().build())useDefaultTracerProvider(bool)use_default_tracer_providerget_tracer_provider()contextExtractorinstrumentationNameinstrument_nameinstrument_name"aws-durable-execution-sdk-java"enableHttpInstrumentationexporterConfigexporter_configpropagatorsworkflowSpanNameworkflowSpanNameenrich_loggerenrich_loggerenableMdcI've also decided to use an enum for the TracerProvider source. Having multiple variables and order determine what TracerProvider the plugin was going to use is too confusing.
feat(otel): add enrichLogger config toggle to gate log context enrich…
SilanHe commentedon Aug 4, 2026
ExecutionotelTraceSampledtraceId/spanIdInvocationotelTraceSampledtraceId/spanIdExecutionotelTraceSampledtraceId/spanIdInvocationotelTraceSampledtraceId/spanIdExecutiontraceSampledtrace_id/span_id"true"/"false")InvocationtraceSampledtrace_id/span_id"true"/"false")SilanHe commentedon Aug 7, 2026
There could be a gap between the behaviour in the ExecutionOtelPlugin when we're not using the ProviderSource.GLOBAL.
I believe in JS, the Invocation span is a child of the Workflow span. In Python, the Invocation spans are root spans. We'll have to double check.
refactor(otel): resolve provider source via enum (#792)