@aws/durable-execution-sdk-js-otel 0.1.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +523 -175
  2. package/dist/execution-plugin.mjs +539 -0
  3. package/dist/execution-plugin.mjs.map +1 -0
  4. package/dist/global-id-generator.mjs +648 -0
  5. package/dist/global-id-generator.mjs.map +1 -0
  6. package/dist/index.mjs +8 -537
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/invocation-plugin.mjs +661 -0
  9. package/dist/invocation-plugin.mjs.map +1 -0
  10. package/dist/otel-execution.mjs +15 -0
  11. package/dist/otel-execution.mjs.map +1 -0
  12. package/dist/otel-invocation.mjs +16 -0
  13. package/dist/otel-invocation.mjs.map +1 -0
  14. package/dist-cjs/execution-plugin.js +541 -0
  15. package/dist-cjs/execution-plugin.js.map +1 -0
  16. package/dist-cjs/global-id-generator.js +664 -0
  17. package/dist-cjs/global-id-generator.js.map +1 -0
  18. package/dist-cjs/index.js +19 -540
  19. package/dist-cjs/index.js.map +1 -1
  20. package/dist-cjs/invocation-plugin.js +663 -0
  21. package/dist-cjs/invocation-plugin.js.map +1 -0
  22. package/dist-cjs/otel-execution.js +17 -0
  23. package/dist-cjs/otel-execution.js.map +1 -0
  24. package/dist-cjs/otel-invocation.js +18 -0
  25. package/dist-cjs/otel-invocation.js.map +1 -0
  26. package/dist-types/context-extractors.d.ts +70 -2
  27. package/dist-types/context-extractors.d.ts.map +1 -1
  28. package/dist-types/deterministic-id-generator.d.ts +56 -31
  29. package/dist-types/deterministic-id-generator.d.ts.map +1 -1
  30. package/dist-types/execution-plugin-provider.d.ts +7 -0
  31. package/dist-types/execution-plugin-provider.d.ts.map +1 -0
  32. package/dist-types/execution-plugin.d.ts +78 -0
  33. package/dist-types/execution-plugin.d.ts.map +1 -0
  34. package/dist-types/execution-trace-context.d.ts +131 -0
  35. package/dist-types/execution-trace-context.d.ts.map +1 -0
  36. package/dist-types/global-id-generator.d.ts +12 -0
  37. package/dist-types/global-id-generator.d.ts.map +1 -0
  38. package/dist-types/global-sampler.d.ts +25 -0
  39. package/dist-types/global-sampler.d.ts.map +1 -0
  40. package/dist-types/index.d.ts +6 -3
  41. package/dist-types/index.d.ts.map +1 -1
  42. package/dist-types/invocation-plugin-provider.d.ts +7 -0
  43. package/dist-types/invocation-plugin-provider.d.ts.map +1 -0
  44. package/dist-types/invocation-plugin.d.ts +89 -0
  45. package/dist-types/invocation-plugin.d.ts.map +1 -0
  46. package/dist-types/otel-plugin-config.d.ts +66 -0
  47. package/dist-types/otel-plugin-config.d.ts.map +1 -0
  48. package/dist-types/otel-plugin-provider.d.ts +21 -0
  49. package/dist-types/otel-plugin-provider.d.ts.map +1 -0
  50. package/package.json +26 -15
  51. package/dist-types/plugin.d.ts +0 -46
  52. package/dist-types/plugin.d.ts.map +0 -1
  53. package/dist-types/tsconfig.build.tsbuildinfo +0 -1
package/README.md CHANGED
@@ -1,276 +1,624 @@
1
1
  # AWS Durable Execution SDK - OpenTelemetry Plugin
2
2
 
3
- > **⚠️ Experimental Beta:** This plugin is currently in experimental beta. Functionality may change without notice between releases. It is not recommended for production workloads at this time.
3
+ OpenTelemetry instrumentation for the AWS Durable Execution SDK. The package
4
+ provides two plugins that emit Workflow, Invocation, durable operation, and
5
+ operation-attempt spans.
4
6
 
5
- OpenTelemetry instrumentation plugin for AWS Durable Execution SDK. Emits distributed traces that correlate across multiple Lambda invocations of a single durable execution, producing deterministic span and trace IDs so that spans from different invocations are stitched into a single coherent trace.
7
+ The whole durable execution shares **one trace**, anchored at the execution
8
+ ancestor resolved at invocation start. The `Workflow` span and every per-invocation
9
+ `Invocation` span join that trace, so a single trace ID spans all invocations of
10
+ the execution.
6
11
 
7
- ## Features
12
+ They differ in where durable operation spans are parented:
8
13
 
9
- - **Deterministic Trace IDs**: All invocations of the same durable execution share a single trace, derived from the X-Ray trace header or execution ARN
10
- - **Span-per-Operation**: Each durable operation (step, wait, invoke) gets its own span with accurate timing
11
- - **Continuation Spans**: Operations completing in a different invocation are linked back to the original span
12
- - **Log Correlation**: Enrich application logs with trace ID and span ID for end-to-end observability
13
- - **Configurable Sampling**: Control trace volume via environment variable or plugin options
14
- - **Self-Contained Setup**: No manual TracerProvider configuration required
14
+ | Plugin | Operation hierarchy | Cross-link |
15
+ | ---------------------- | ------------------------------------ | --------------------------------------- |
16
+ | `ExecutionOtelPlugin` | `Workflow -> operation -> attempt` | Operations and attempts link Invocation |
17
+ | `InvocationOtelPlugin` | `Invocation -> operation -> attempt` | Operations and attempts link Workflow |
18
+
19
+ The plugins do not create or register an OpenTelemetry provider, exporter,
20
+ sampler, resource, propagator, context manager, or library instrumentation.
21
+ They use the global provider by default, or an application-owned provider
22
+ created through `tracerProviderFactory`.
15
23
 
16
24
  ## Installation
17
25
 
18
26
  ```bash
19
- npm install @aws/durable-execution-sdk-js-otel
27
+ npm install @aws/durable-execution-sdk-js-otel \
28
+ @aws/durable-execution-sdk-js \
29
+ @opentelemetry/api \
30
+ @opentelemetry/core \
31
+ @opentelemetry/sdk-trace-node
20
32
  ```
21
33
 
22
- ## Quick Start using Xray/CloudWatch Tracing
34
+ The package requires Node.js 22 or later. Exporters, span processors,
35
+ propagators, resources, and library instrumentation are application or ADOT
36
+ responsibilities.
23
37
 
24
- 1. Add the ADOT Lambda Layer to your function and set `AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument`
25
- 2. Enable X-Ray Active Tracing on the function
26
- 3. Pass `OtelPlugin` to your handler's `plugins` array
27
- 4. Add Xray Write Permissions
38
+ ## Quick Start
28
39
 
29
- ### 1. ADOT Lambda Layer
40
+ When an SDK provider is registered globally, no plugin configuration is
41
+ required:
30
42
 
31
- This plugin requires the [AWS Distro for OpenTelemetry (ADOT) Lambda layer](https://aws-otel.github.io/docs/getting-started/lambda) to export traces from your Lambda function.
43
+ ```typescript
44
+ import { withDurableExecution } from "@aws/durable-execution-sdk-js";
45
+ import { ExecutionOtelPlugin } from "@aws/durable-execution-sdk-js-otel";
32
46
 
33
- The layer ARN follows the format:
47
+ const plugin = new ExecutionOtelPlugin();
34
48
 
49
+ export const handler = withDurableExecution(
50
+ async (event, context) => {
51
+ return context.step("process", async () => process(event));
52
+ },
53
+ { plugins: [plugin] },
54
+ );
35
55
  ```
36
- arn:aws:lambda:<region>:<awsAccountId>:layer:AWSOpenTelemetryDistroJs:<version>
56
+
57
+ Use `InvocationOtelPlugin` instead when operations should appear under each
58
+ Lambda invocation rather than under the durable Workflow.
59
+
60
+ ## Provider Setup
61
+
62
+ ### Global Provider
63
+
64
+ Omitting `tracerProviderFactory` uses `trace.getTracerProvider()`. This is the
65
+ normal setup for the ADOT Lambda layer, OpenTelemetry zero-code
66
+ instrumentation, or an application that registers its own provider globally.
67
+
68
+ When using ADOT, activate its instrumentation wrapper:
69
+
70
+ ```text
71
+ AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument
37
72
  ```
38
73
 
39
- Refer to the [ADOT Lambda Layer ARNs](https://aws-otel.github.io/docs/getting-started/lambda#aws-lambda-layer-for-opentelemetry-arns) page for the latest version number and supported regions.
74
+ The OpenTelemetry JavaScript API does not provide a public way to retrieve or
75
+ replace a provider's configured ID generator. For a compatible SDK tracer, the
76
+ plugin therefore:
40
77
 
41
- **AWS CLI:**
78
+ 1. reads the tracer's runtime `_idGenerator` field;
79
+ 2. replaces it with a guarded deterministic wrapper;
80
+ 3. delegates all ID generation outside plugin span creation to the original
81
+ generator.
42
82
 
43
- ```bash
44
- aws lambda update-function-configuration \
45
- --function-name your-function-name \
46
- --layers "arn:aws:lambda:<region>:<<awsAccountId>>:layer:AWSOpenTelemetryDistroJs:<version>"
83
+ This private-field integration is isolated behind runtime shape and assignment
84
+ checks. It does not change IDs for unrelated spans, including spans created
85
+ concurrently or with the same instrumentation scope.
86
+
87
+ If the plugin is constructed before the SDK provider is globally registered,
88
+ its initial tracer may be a proxy without `_idGenerator`. At each invocation
89
+ start, the plugin re-resolves the global provider until a compatible SDK tracer
90
+ is available. When installation still fails:
91
+
92
+ - plugin telemetry and log enrichment are disabled for that invocation;
93
+ - a warning is emitted;
94
+ - provider resolution is retried on the next invocation.
95
+
96
+ Providers or future SDK tracers that do not expose the compatible runtime field
97
+ cannot use this global-provider path. Use `tracerProviderFactory` to install the
98
+ generator through the supported provider constructor API.
99
+
100
+ ### Application-Owned Provider
101
+
102
+ `tracerProviderFactory` receives a function that creates the plugin's
103
+ deterministic ID wrapper. The factory is called during plugin construction.
104
+
105
+ ```typescript
106
+ import { InvocationOtelPlugin } from "@aws/durable-execution-sdk-js-otel";
107
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
108
+ import {
109
+ NodeTracerProvider,
110
+ SimpleSpanProcessor,
111
+ } from "@opentelemetry/sdk-trace-node";
112
+
113
+ const plugin = new InvocationOtelPlugin({
114
+ tracerProviderFactory: (createIdGenerator) => {
115
+ const provider = new NodeTracerProvider({
116
+ idGenerator: createIdGenerator(),
117
+ spanProcessors: [
118
+ new SimpleSpanProcessor(
119
+ new OTLPTraceExporter({
120
+ url: "http://localhost:4318/v1/traces",
121
+ }),
122
+ ),
123
+ ],
124
+ });
125
+
126
+ provider.register();
127
+ return provider;
128
+ },
129
+ });
47
130
  ```
48
131
 
49
- You must also set the `AWS_LAMBDA_EXEC_WRAPPER` environment variable:
132
+ `createIdGenerator()` delegates non-durable IDs to OpenTelemetry's standard
133
+ random generator. To preserve another generator, pass it as the fallback:
50
134
 
51
- ```bash
52
- aws lambda update-function-configuration \
53
- --function-name your-function-name \
54
- --environment "Variables={AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument}"
135
+ ```typescript
136
+ idGenerator: createIdGenerator(applicationIdGenerator);
55
137
  ```
56
138
 
57
- > **Note:** Replace `<region>`, `<awsAccountId>` `<version>` with the latest layer information from the ADOT docs.
139
+ The deterministic override is active only for the synchronous `startSpan()`
140
+ call made by the plugin. All unrelated IDs use the fallback generator.
141
+
142
+ The application owns the returned provider and all associated configuration:
143
+
144
+ - exporters and span processors;
145
+ - sampling and resources;
146
+ - context management and propagation;
147
+ - HTTP, AWS SDK, Lambda, and other library instrumentation;
148
+ - global registration;
149
+ - shutdown.
58
150
 
59
- **CloudFormation / SAM:**
151
+ The provider does not have to be the global provider for the plugin to obtain a
152
+ tracer from it. Register it globally when the application also needs its
153
+ context manager, propagator, or library instrumentation.
60
154
 
61
- ```yaml
62
- MyFunction:
63
- Type: AWS::Serverless::Function
64
- Properties:
65
- Layers:
66
- - !Sub arn:aws:lambda:${AWS::Region}:<awsAccountId>:layer:AWSOpenTelemetryDistroJs:<version>
67
- Environment:
68
- Variables:
69
- AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-instrument
155
+ At the end of each enabled invocation, both plugins call `forceFlush()` when
156
+ the provider exposes it. They never call `shutdown()`.
157
+
158
+ ## Dynamic Loading from a Lambda Layer
159
+
160
+ The SDK can load either plugin without importing it in function code. Package
161
+ this module and its OpenTelemetry peer dependencies in a Lambda layer under
162
+ `nodejs/node_modules`, then configure one entry point:
163
+
164
+ ```text
165
+ DURABLE_EXECUTION_PLUGINS=@aws/durable-execution-sdk-js-otel/otel-execution
70
166
  ```
71
167
 
72
- For raw CloudFormation (`AWS::Lambda::Function`):
73
-
74
- ```yaml
75
- MyFunction:
76
- Type: AWS::Lambda::Function
77
- Properties:
78
- Layers:
79
- - !Sub arn:aws:lambda:${AWS::Region}:<awsAccountId>:layer:AWSOpenTelemetryDistroJs:<version>
80
- Environment:
81
- Variables:
82
- AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-instrument
168
+ or:
169
+
170
+ ```text
171
+ DURABLE_EXECUTION_PLUGINS=@aws/durable-execution-sdk-js-otel/otel-invocation
83
172
  ```
84
173
 
85
- **CDK:**
174
+ Do not package `@aws/durable-execution-sdk-js` in the layer. The provider entry
175
+ points use SDK types only, and the SDK peer dependency is optional so package
176
+ installation does not add a second copy. At runtime, the plugin is loaded and
177
+ driven by the SDK instance bundled with the function.
86
178
 
87
- ```typescript
88
- import * as lambda from "aws-cdk-lib/aws-lambda";
179
+ Dynamic providers construct plugins with default configuration, so they
180
+ require a compatible globally registered SDK provider. Use code-based
181
+ registration when `tracerProviderFactory` or other custom configuration is
182
+ needed.
89
183
 
90
- const adotLayer = lambda.LayerVersion.fromLayerVersionArn(
91
- this,
92
- "AdotLayer",
93
- `arn:aws:lambda:${this.region}:<awsAccountId>:layer:AWSOpenTelemetryDistroJs:<version>`,
94
- );
184
+ ## Choosing a Plugin
95
185
 
96
- const fn = new lambda.Function(this, "MyFunction", {
97
- runtime: lambda.Runtime.NODEJS_22_X,
98
- handler: "index.handler",
99
- code: lambda.Code.fromAsset("lambda"),
100
- layers: [adotLayer],
101
- environment: {
102
- AWS_LAMBDA_EXEC_WRAPPER: "/opt/otel-instrument",
103
- },
104
- });
105
- ```
186
+ ### `ExecutionOtelPlugin`
106
187
 
107
- > **Tip:** Pin the layer version to a specific number in production deployments to avoid unexpected behavior from automatic version changes.
188
+ Use this plugin for a workflow-centered view. Operations and attempts are
189
+ children of the `Workflow` span. Each also links to the `Invocation` span that
190
+ observed it. The whole execution shares one trace.
108
191
 
109
- ### 2. AWS X-Ray Active Tracing
192
+ ```text
193
+ Execution trace
110
194
 
111
- Enable active tracing on your Lambda function so the `_X_AMZN_TRACE_ID` environment variable is populated at invocation time. The plugin uses this header to derive deterministic trace IDs that remain consistent across all invocations of the same durable execution.
195
+ Execution ancestor
196
+ ├── Workflow
197
+ │ └── Operation: fetch-data
198
+ │ └── Attempt 1
199
+ └── Invocation
112
200
 
113
- **AWS Console:** Lambda → Configuration → Monitoring and operations tools → Active tracing → Enable
201
+ Links:
202
+ Operation -> Invocation
203
+ Attempt -> Invocation
204
+ ```
114
205
 
115
- **AWS CLI:**
206
+ The plugin makes Workflow the active span while durable handler code runs.
207
+ Completed operation spans use durable operation start/end timestamps and
208
+ deterministic span IDs. While an operation spans multiple invocations its
209
+ identity is carried as a non-recording context; nothing is exported until it
210
+ completes. The single recording operation span is then created and ended once,
211
+ in the invocation where the operation terminates, under a **deterministic span
212
+ ID on the execution trace**. Because there is one span per logical operation,
213
+ no cross-invocation link is needed to stitch it together — unlike
214
+ `InvocationOtelPlugin` below.
215
+
216
+ ### `InvocationOtelPlugin`
217
+
218
+ Use this plugin for an invocation-centered view. Operations and attempts are
219
+ children of the current `Invocation` span. Each links to the `Workflow` span.
220
+ The whole execution shares one trace.
221
+
222
+ ```text
223
+ Execution trace
224
+
225
+ Execution ancestor
226
+ ├── Workflow
227
+ └── Invocation
228
+ └── Operation: fetch-data
229
+ └── Attempt 1
230
+
231
+ Links:
232
+ Operation -> Workflow
233
+ Attempt -> Workflow
234
+ ```
116
235
 
117
- ```bash
118
- aws lambda update-function-configuration \
119
- --function-name your-function-name \
120
- --tracing-config Mode=Active
236
+ The plugin makes Invocation the active span while durable handler code runs.
237
+ Open operation spans are ended at the invocation boundary and retain
238
+ `durable.operation.status=STARTED`. When an operation completes in a later
239
+ invocation, the plugin emits a continuation span in that invocation.
240
+
241
+ Because the original span context is not checkpointed, replayed `STEP` and
242
+ `CONTEXT` spans and cross-invocation continuation spans use new provider IDs.
243
+ They correlate through two links: the real exported `Workflow` span, and the
244
+ **initial logical operation span** — whose ID is deterministic on
245
+ `(operationId, execution ARN)` and lives on the execution trace, so the segments
246
+ of one logical operation stay stitched together across invocations. Replay-only
247
+ `WAIT`, `INVOKE`, `CHAINED_INVOKE`, and `CALLBACK` operations are not emitted
248
+ again.
249
+
250
+ ## Execution Trace and the Execution Ancestor
251
+
252
+ Both plugins resolve one **execution ancestor** at invocation start — the common
253
+ parent the `Workflow` and `Invocation` spans join — so the whole execution
254
+ shares a single trace. The ancestor is chosen by precedence:
255
+
256
+ 1. a **complete propagated remote parent** (a valid `Root` trace ID and a valid
257
+ `Parent` span ID): used directly, whether or not `Sampled` is present;
258
+ 2. otherwise a **synthetic execution root** with a deterministic span ID derived
259
+ from the execution ARN.
260
+
261
+ The canonical trace ID follows the same precedence: the propagated remote trace
262
+ ID when valid, else one derived from the ARN and start time.
263
+
264
+ A live ambient span (a `context.active()` span created by an auto-instrumentation
265
+ layer) is deliberately **never** used as the execution ancestor. Its trace ID is
266
+ not guaranteed stable across Lambda reinvocations, so anchoring the
267
+ multi-invocation execution on it could change the execution trace ID on replay
268
+ and break the cross-invocation continuation and replay links (which target a
269
+ deterministic operation span ID on the canonical trace). Both anchors used above
270
+ are stable: the durable backend keeps the propagated `Root` identical for every
271
+ invocation, and the ARN-derived synthetic root is a pure hash of the ARN.
272
+
273
+ ### Valid Workflow parent shapes
274
+
275
+ `ADOT` and community Node.js auto-instrumentation layers can create the ambient
276
+ handler span before the durable plugin runs, on the trace the durable backend
277
+ propagated. OpenTelemetry `SpanContext` has no ancestor pointer, so the plugin
278
+ cannot walk from that local span to the durable backend span. The `Workflow`
279
+ span therefore joins the canonical trace and parents onto whichever ancestor the
280
+ precedence above resolves. When a complete remote parent is propagated, that is
281
+ the ancestor:
282
+
283
+ ```text
284
+ Propagated remote parent
285
+ ├── Workflow
286
+ └── Invocation
121
287
  ```
122
288
 
123
- **CloudFormation / SAM:**
289
+ When no complete remote parent can be constructed, the synthetic execution root
290
+ anchors the trace and the `Workflow` span parents onto it:
124
291
 
125
- ```yaml
126
- MyFunction:
127
- Type: AWS::Lambda::Function
128
- Properties:
129
- TracingConfig:
130
- Mode: Active
292
+ ```text
293
+ Synthetic execution root
294
+ └── Workflow
131
295
  ```
132
296
 
133
- **CDK:**
297
+ The `Invocation` span may still nest under a same-trace ambient handler span —
298
+ see [Invocation parent](#invocation-parent) — without changing the execution
299
+ ancestor the `Workflow` span joins.
300
+
301
+ ### Workflow identity and lifecycle
302
+
303
+ `Workflow` is an `INTERNAL` span that **joins the execution trace** by parenting
304
+ onto the execution ancestor (its span ID is forced; the trace ID comes from the
305
+ parent). Its span ID is reproducible across replays:
306
+
307
+ - span ID: the first 16 hexadecimal characters of SHA-256 over
308
+ `workflow:<execution ARN>`.
309
+
310
+ The synthetic execution root, when used, gets a distinct deterministic span ID
311
+ from SHA-256 over `execution-root:<execution ARN>`, and the ARN-derived trace ID
312
+ is the first 32 hexadecimal characters of SHA-256 over
313
+ `<execution ARN>:<execution start timestamp in ISO 8601 format>` (falling back
314
+ to the ARN alone when the timestamp is unavailable).
315
+
316
+ The plugins carry the same `Workflow` span identity on each invocation as a
317
+ non-recording span context, and create the single recording `Workflow` span only
318
+ when the execution reaches `SUCCEEDED` or `FAILED`, backdated to the execution
319
+ start. `PENDING` and `RETRYING` invocations create no recording `Workflow` span,
320
+ so none is ever left unended, and exactly one `Workflow` root is exported for the
321
+ durable execution while intermediate invocation spans export normally. The
322
+ non-recording context carries the execution's sampling decision, so operations
323
+ under it are sampled consistently with the eventual root. In-flight attempt spans
324
+ are ended at invocation cleanup, so a non-terminal invocation leaves no recording
325
+ span unended. Suspended operations differ by plugin: `InvocationOtelPlugin` ends
326
+ the open operation span at the boundary and continues it later, while
327
+ `ExecutionOtelPlugin` holds the operation identity as a non-recording context and
328
+ exports one recording span only when the operation completes.
329
+
330
+ When a chained parent and target execution share a propagated remote parent,
331
+ both join that one trace, each keeping its own deterministic `Workflow` span ID.
332
+
333
+ ### Invocation parent
334
+
335
+ The `Invocation` span parents onto the **same-trace ambient span** when one is
336
+ active on the canonical trace (preserving the Lambda/X-Ray linkage), otherwise
337
+ onto the execution ancestor so it stays on the execution trace. Provider
338
+ ownership does not change this topology.
339
+
340
+ ### Sampling
341
+
342
+ Sampling follows this precedence, highest first:
343
+
344
+ 1. **Explicit upstream decision** — an explicit `Sampled=1` / `Sampled=0` in a
345
+ valid propagated header is authoritative and preserved. It is applied only
346
+ when the extracted trace ID is itself valid, so an unusable trace never
347
+ carries its sampling bit into the derived execution trace.
348
+ 2. **Configured sampler** — when the header carries no usable decision, the
349
+ provider sampler decides. It is evaluated at its **root policy**
350
+ (`ROOT_CONTEXT`, no parent) with the real trace ID, span name, and attributes,
351
+ so a `ParentBasedSampler` applies its `root` sampler rather than inheriting an
352
+ unrelated ambient span's decision.
353
+ 3. **Default to sampled** — used only when no sampler decision can be obtained.
354
+
355
+ The resolved execution decision is enforced for all SDK-created spans through a
356
+ durable sampler wrapper around the provider sampler. This makes explicit backend
357
+ sampling authoritative even for direct/custom samplers that ignore parent
358
+ `traceFlags`. Unrelated application spans still use the original configured
359
+ sampler. If the resolved tracer does not expose a compatible writable sampler,
360
+ the plugin preserves normal tracing through the configured provider; in that
361
+ case direct samplers may make their normal span-level decisions.
362
+
363
+ ### Context extractors
364
+
365
+ The default `xRayContextExtractor` parses `Root`, `Parent`, and `Sampled`
366
+ independently from `_X_AMZN_TRACE_ID`, rejecting an all-zero (invalid) `Root` or
367
+ `Parent`. The durable backend keeps the X-Ray `Root` stable for every invocation
368
+ of one execution, so its trace ID anchors the whole execution. The package also
369
+ exports `w3cClientContextExtractor`, which reads W3C `traceparent` data from
370
+ `context.clientContext.custom.traceparent`.
371
+
372
+ A custom extractor can return:
134
373
 
135
374
  ```typescript
136
- new lambda.Function(this, "MyFunction", {
137
- tracing: lambda.Tracing.ACTIVE,
138
- });
375
+ type Sampling = "SAMPLED" | "NOT_SAMPLED" | "UNDECIDED";
376
+
377
+ type ContextExtractor = (info: InvocationInfo) =>
378
+ | {
379
+ traceId: string;
380
+ parentSpanId?: string;
381
+ traceFlags?: number;
382
+ sampling?: Sampling;
383
+ }
384
+ | undefined;
139
385
  ```
140
386
 
141
- ### 3. In your Lambda handler (index.js)
387
+ A complete remote parent needs a valid `traceId` and a valid `parentSpanId`.
388
+ `sampling` is the tri-state upstream decision; when omitted it is derived from
389
+ `traceFlags` (sampled bit), and when neither is present the decision is
390
+ `UNDECIDED` and the sampling rules above decide. A custom extractor must return
391
+ the durable execution's own trace context: a valid trace ID anchors the
392
+ execution across replays, so returning stale or per-invocation context (for
393
+ example an ambient span that changes each invocation) would split one durable
394
+ execution across traces.
395
+
396
+ ## Shared Configuration
397
+
398
+ Both plugins accept `OtelPluginConfig`:
142
399
 
143
400
  ```typescript
144
- import { withDurableExecution } from "@aws/durable-execution-sdk-js";
145
- import { OtelPlugin } from "@aws/durable-execution-sdk-js-otel";
401
+ interface OtelPluginConfig {
402
+ /** Creates an application-owned provider with a deterministic ID wrapper. */
403
+ tracerProviderFactory?: (
404
+ createIdGenerator: (fallbackIdGenerator?: IdGenerator) => IdGenerator,
405
+ ) => TracerProvider;
146
406
 
147
- export const handler = withDurableExecution(
148
- async (event, context) => {
149
- const result = await context.step("fetch-data", async () => {
150
- return fetchData(event.id);
151
- });
407
+ /** Extracts upstream trace context. Defaults to xRayContextExtractor. */
408
+ contextExtractor?: ContextExtractor;
152
409
 
153
- await context.wait({ seconds: 5 });
410
+ /** Instrumentation scope name. */
411
+ instrumentationName?: string;
154
412
 
155
- await context.step("process", async () => {
156
- return process(result);
157
- });
413
+ /** Custom Workflow span name. Defaults to "Workflow". */
414
+ workflowSpanName?: string;
158
415
 
159
- return result;
160
- },
161
- { plugins: [new OtelPlugin()] },
162
- );
416
+ /** Add active OTel IDs to durable log records. Defaults to true. */
417
+ enrichLogger?: boolean;
418
+ }
163
419
  ```
164
420
 
165
- That's it. The plugin handles TracerProvider setup, deterministic ID generation, and span lifecycle internally.
421
+ Provider selection is implicit:
166
422
 
167
- ### 4. Grant Permissions
423
+ - omit `tracerProviderFactory` to use the global provider;
424
+ - provide `tracerProviderFactory` to use the returned application-owned
425
+ provider.
168
426
 
169
- The function's execution role needs the `AWSXRayDaemonWriteAccess` managed policy (or equivalent permissions) if using X-Ray as the tracing backend.
427
+ The default instrumentation scope name is
428
+ `aws-durable-execution-sdk-js`.
170
429
 
171
- ### AWS Console
430
+ ## Spans, Attributes, and Status
172
431
 
173
- See https://aws-otel.github.io/docs/getting-started/lambda#use-the-lambda-console.
432
+ All plugin-created spans use `SpanKind.INTERNAL`.
174
433
 
175
- ### Environment Variables for ADOT layer
434
+ - **Workflow:** Named by `workflowSpanName`, which defaults to `Workflow`.
435
+ Carries `durable.execution.arn` and, when terminal,
436
+ `durable.execution.status`.
437
+ - **Invocation:** Named `Invocation`. Carries `durable.execution.arn`,
438
+ `durable.invocation.first`, and `durable.invocation.status`.
439
+ - **Operation:** Named from the configured operation name, or the operation
440
+ type when unnamed. Carries `durable.execution.arn`,
441
+ `durable.operation.id`, `durable.operation.type`, optional
442
+ `durable.operation.name`, and optional `durable.operation.subtype`.
443
+ `InvocationOtelPlugin` sets `durable.operation.status=STARTED` at span
444
+ creation; both plugins apply a supplied terminal operation status at
445
+ completion. `durable.attempt.number` is added to `STEP` and
446
+ `WAIT_FOR_CONDITION` operation spans when supplied.
447
+ - **Attempt:** Named `<operation name or type> attempt <number>`. Carries the
448
+ operation identity attributes, `durable.attempt.number`, and terminal
449
+ `durable.attempt.outcome`. Attempt spans do not carry
450
+ `durable.operation.status`.
176
451
 
177
- | Variable | Description | Default |
178
- | ----------------------------- | --------------------------------------------------------------------------------------------- | ----------------- |
179
- | `OTEL_EXPORTER_OTLP_ENDPOINT` | Endpoint for the OTLP exporter (e.g., `http://localhost:4318` for the ADOT collector sidecar) | Set by ADOT layer |
180
- | `AWS_LAMBDA_EXEC_WRAPPER` | Set to `/opt/otel-instrument` for the ADOT layer to instrument your function | — |
181
- | `OTEL_TRACES_SAMPLER` | Sampler to use (e.g., `traceidratio` for ratio-based sampling) | `always_on` |
182
- | `OTEL_TRACES_SAMPLER_ARG` | Argument for the sampler (e.g., `0.3` to sample 30% of traces) | — |
452
+ For `ExecutionOtelPlugin` with an application-owned provider, Invocation also
453
+ adds `cloud.resource_id` and `faas.max_memory` when the corresponding Lambda
454
+ environment values are available.
183
455
 
184
- See the [ADOT sampling configuration](https://aws-otel.github.io/docs/getting-started/lambda#sampling-configuration) for more details.
456
+ OpenTelemetry status mapping is:
185
457
 
186
- ## Configuration
458
+ | Span | Durable result | OTel status |
459
+ | ---------- | ------------------------------------------------------------------ | ------------------------ |
460
+ | Workflow | `SUCCEEDED` / `FAILED` | `OK` / `ERROR` |
461
+ | Invocation | `SUCCEEDED` or `PENDING` / `FAILED` / `RETRYING` | `OK` / `ERROR` / `UNSET` |
462
+ | Operation | `SUCCEEDED` / error object / other failure without an error object | `OK` / `ERROR` / `UNSET` |
463
+ | Attempt | `FAILED` / any other outcome | `ERROR` / `OK` |
187
464
 
188
- ### Plugin Options
465
+ Operation and attempt errors are recorded as exception events when an error
466
+ object is available. Workflow and Invocation failures set an `ERROR` status and
467
+ status message without recording an exception event.
189
468
 
190
- ```typescript
191
- import { OtelPlugin } from "@aws/durable-execution-sdk-js-otel";
469
+ ## Log Correlation
192
470
 
193
- const plugin = new OtelPlugin({
194
- // Use a custom context extractor (default: xRayContextExtractor)
195
- contextExtractor?: xRayContextExtractor,
471
+ When `enrichLogger` is enabled, durable log records receive the currently
472
+ active OpenTelemetry `traceId`, `spanId`, and `otelTraceSampled` values.
473
+ Outside an active span, no fields are added.
196
474
 
197
- // Provide your own TracerProvider if you already have one configured
198
- tracerProvider?: myTracerProvider,
475
+ Disable enrichment when another logging integration already injects equivalent
476
+ fields:
199
477
 
200
- // Custom instrumentation scope name (default: "aws-durable-execution-sdk-js")
201
- instrumentationName?: "my-service",
478
+ ```typescript
479
+ const plugin = new ExecutionOtelPlugin({
480
+ enrichLogger: false,
202
481
  });
203
482
  ```
204
483
 
205
- ### Context Extractors
206
-
207
- The plugin supports multiple strategies for extracting upstream trace context:
484
+ For `ExecutionOtelPlugin`, handler-level logs correlate with Workflow. For
485
+ `InvocationOtelPlugin`, handler-level logs correlate with Invocation. Logs
486
+ inside child contexts and attempts correlate with their active operation or
487
+ attempt span.
208
488
 
209
- ```typescript
210
- import {
211
- OtelPlugin,
212
- xRayContextExtractor,
213
- w3cClientContextExtractor,
214
- } from "@aws/durable-execution-sdk-js-otel";
489
+ ## Public API
215
490
 
216
- // Default: X-Ray trace header (recommended for most Lambda deployments)
217
- new OtelPlugin({ contextExtractor: xRayContextExtractor });
491
+ ### Plugins
218
492
 
219
- // W3C Trace Context via clientContext (requires backend propagation support (TODO))
220
- new OtelPlugin({ contextExtractor: w3cClientContextExtractor });
493
+ ```typescript
494
+ new ExecutionOtelPlugin(config?: OtelPluginConfig);
495
+ new InvocationOtelPlugin(config?: OtelPluginConfig);
221
496
  ```
222
497
 
223
- ## Verification
498
+ ### Provider Types
224
499
 
225
- After deploying your function with the plugin configured:
226
-
227
- 1. **Invoke your durable function** — trigger at least one execution that includes multiple steps or a wait/resume cycle.
500
+ ```typescript
501
+ type IdGeneratorFactory = (fallbackIdGenerator?: IdGenerator) => IdGenerator;
228
502
 
229
- 2. **Check Cloudwatch console** — Navigate to Cloudwatch → Traces in the AWS Console. You should see a trace with:
230
- - An "invocation" span per invocation
231
- - Child spans for each durable operation (named after your step names)
232
- - All invocations of the same execution grouped under one trace ID
503
+ type TracerProviderFactory = (
504
+ createIdGenerator: IdGeneratorFactory,
505
+ ) => TracerProvider;
506
+ ```
233
507
 
234
- 3. **Check log correlation** — If you use `enrichLogContext()`, verify that your logs include `traceId` and `spanId` fields matching the spans in X-Ray.
508
+ ### `DeterministicIdGenerator`
235
509
 
236
- 4. **Confirm sampling** — If you set `OTEL_TRACES_SAMPLER=traceidratio` and `OTEL_TRACES_SAMPLER_ARG` to a value less than 1.0, verify that only the expected proportion of traces appear.
510
+ An OpenTelemetry `IdGenerator` with `AsyncLocalStorage`-scoped deterministic
511
+ overrides. `withIds()` applies optional trace and span IDs only while its
512
+ synchronous callback starts a span. The span ID override is consumed after its
513
+ first use. All other ID generation delegates to the fallback generator.
237
514
 
238
- 5. **span links** — For operations that span multiple invocations (e.g., after a wait resumes), though span links are set, they are not visualized within CloudWatch console.
515
+ ```typescript
516
+ const generator = new DeterministicIdGenerator(fallbackIdGenerator);
239
517
 
240
- ### Troubleshooting
518
+ const span = generator.withIds(
519
+ { traceId: executionTraceId, spanId: deterministicSpanId },
520
+ () => tracer.startSpan("operation"),
521
+ );
522
+ ```
241
523
 
242
- | Symptom | Likely Cause |
243
- | --------------------------------- | --------------------------------------------------------------- |
244
- | No traces appear | ADOT layer not configured, or `AWS_LAMBDA_EXEC_WRAPPER` not set |
245
- | Traces appear but are fragmented | X-Ray active tracing not enabled on the Lambda function |
246
- | Missing spans for some operations | `OTEL_TRACES_SAMPLER_ARG` set below 1.0 |
247
- | `_X_AMZN_TRACE_ID` not populated | X-Ray active tracing not enabled |
524
+ ### ID Helpers
248
525
 
249
- ## API Reference
526
+ ```typescript
527
+ deriveTraceIdFromArn(
528
+ executionArn: string,
529
+ executionStartTimestamp?: Date,
530
+ ): string;
250
531
 
251
- ### `OtelPlugin`
532
+ deriveTraceIdFromXRayRoot(xRayRoot: string): string | undefined;
252
533
 
253
- The main plugin class. Implements `DurableInstrumentationPlugin` from `@aws/durable-execution-sdk-js`.
534
+ deriveExecutionTraceId(
535
+ environment: ExecutionTraceEnvironment,
536
+ executionArn: string,
537
+ executionStartTimestamp?: Date,
538
+ ): string;
254
539
 
255
- ```typescript
256
- new OtelPlugin(config?: OtelPluginConfig)
257
- ```
540
+ deriveWorkflowSpanId(executionArn: string): string;
258
541
 
259
- ### `DeterministicIdGenerator`
542
+ deriveExecutionRootSpanId(executionArn: string): string;
260
543
 
261
- A custom OpenTelemetry `IdGenerator` that produces reproducible trace and span IDs from execution metadata. Exported for advanced use cases.
544
+ deriveSpanIdFromOperationId(
545
+ operationId: string,
546
+ executionArn: string,
547
+ ): string;
548
+ ```
262
549
 
263
- ### `xRayContextExtractor`
550
+ `deriveTraceIdFromArn` returns a 32-character hexadecimal trace ID.
551
+ `deriveTraceIdFromXRayRoot` converts a valid X-Ray `Root` value to an
552
+ OpenTelemetry trace ID and returns `undefined` for invalid input.
553
+ `deriveExecutionTraceId` applies the default plugin precedence to an explicit
554
+ environment: a valid `_X_AMZN_TRACE_ID` `Root` wins, otherwise it uses the same
555
+ ARN-and-start-time fallback as the plugins. Pass `process.env` in Lambda or a
556
+ plain object in tests. When no valid X-Ray Root is available, pass the same
557
+ execution start timestamp supplied to the plugin; omit it only when it is
558
+ unavailable to both callers.
559
+ `deriveWorkflowSpanId` hashes `workflow:<execution ARN>`,
560
+ `deriveExecutionRootSpanId` hashes `execution-root:<execution ARN>` (a distinct
561
+ namespace so the synthetic root never collides with the Workflow or operation
562
+ spans on the shared trace), and `deriveSpanIdFromOperationId` hashes
563
+ `<execution ARN>:<operation ID>`. All three span helpers return 16-character
564
+ hexadecimal IDs.
565
+
566
+ Wrapper-style instrumentation can create spans on the same execution trace and
567
+ refer to the same Workflow span without copying the plugin's derivations:
264
568
 
265
- Default context extractor. Reads the `_X_AMZN_TRACE_ID` environment variable to derive trace context.
569
+ ```typescript
570
+ import {
571
+ deriveExecutionTraceId,
572
+ deriveWorkflowSpanId,
573
+ } from "@aws/durable-execution-sdk-js-otel";
266
574
 
267
- ### `w3cClientContextExtractor`
575
+ const executionTraceId = deriveExecutionTraceId(
576
+ process.env,
577
+ executionArn,
578
+ executionStartTimestamp,
579
+ );
580
+ const workflowSpanId = deriveWorkflowSpanId(executionArn);
581
+ ```
268
582
 
269
- Alternative context extractor. Reads `traceparent` from `context.clientContext.custom.traceparent` (W3C Trace Context format).
583
+ ### Context Extractors
270
584
 
271
- ### `ContextExtractor`
585
+ ```typescript
586
+ xRayContextExtractor(info: InvocationInfo): ContextExtractorResult;
587
+ w3cClientContextExtractor(info: InvocationInfo): ContextExtractorResult;
588
+ ```
272
589
 
273
- Type definition for custom context extractor functions.
590
+ The package also exports the `ContextExtractor`, `ContextExtractorResult`,
591
+ `ExecutionTraceEnvironment`, `IdGeneratorFactory`, `TracerProviderFactory`, and
592
+ `OtelPluginConfig` types.
593
+
594
+ ## Verification and Troubleshooting
595
+
596
+ After deployment:
597
+
598
+ 1. Invoke a durable function with multiple steps or a wait/resume cycle.
599
+ 2. Verify Invocation spans and completed operation spans appear after enabled
600
+ invocations.
601
+ 3. Verify one Workflow span appears after the execution becomes terminal.
602
+ 4. Expect the Workflow span, every Invocation span, and the operation/attempt
603
+ spans to share one execution trace ID.
604
+ 5. Confirm the documented cross-links connect the workflow and invocation views.
605
+ 6. Confirm unrelated root spans retain provider-generated trace IDs.
606
+ 7. Verify durable log records contain correlation fields when
607
+ `enrichLogger` is enabled.
608
+
609
+ If no plugin spans appear:
610
+
611
+ - confirm a compatible SDK provider is registered before invocation start, or
612
+ return one through `tracerProviderFactory`;
613
+ - confirm the provider has a span processor and exporter;
614
+ - check for the plugin warning that says telemetry was disabled because the
615
+ global tracer was incompatible;
616
+ - remember that dynamic loading supports only the global-provider path;
617
+ - remember that Workflow is not exported until `SUCCEEDED` or `FAILED`.
618
+
619
+ One execution trace is expected. The Workflow span and every per-invocation
620
+ Invocation span share it, anchored at the execution ancestor (the propagated
621
+ remote parent, or a synthetic execution root).
274
622
 
275
623
  ## License
276
624