@aws/durable-execution-sdk-js-otel 0.1.1 → 1.0.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.
- package/README.md +494 -176
- package/dist/execution-plugin.mjs +539 -0
- package/dist/execution-plugin.mjs.map +1 -0
- package/dist/global-id-generator.mjs +622 -0
- package/dist/global-id-generator.mjs.map +1 -0
- package/dist/index.mjs +8 -537
- package/dist/index.mjs.map +1 -1
- package/dist/invocation-plugin.mjs +661 -0
- package/dist/invocation-plugin.mjs.map +1 -0
- package/dist/otel-execution.mjs +15 -0
- package/dist/otel-execution.mjs.map +1 -0
- package/dist/otel-invocation.mjs +16 -0
- package/dist/otel-invocation.mjs.map +1 -0
- package/dist-cjs/execution-plugin.js +541 -0
- package/dist-cjs/execution-plugin.js.map +1 -0
- package/dist-cjs/global-id-generator.js +637 -0
- package/dist-cjs/global-id-generator.js.map +1 -0
- package/dist-cjs/index.js +18 -540
- package/dist-cjs/index.js.map +1 -1
- package/dist-cjs/invocation-plugin.js +663 -0
- package/dist-cjs/invocation-plugin.js.map +1 -0
- package/dist-cjs/otel-execution.js +17 -0
- package/dist-cjs/otel-execution.js.map +1 -0
- package/dist-cjs/otel-invocation.js +18 -0
- package/dist-cjs/otel-invocation.js.map +1 -0
- package/package.json +26 -15
- package/dist-types/context-extractors.d.ts +0 -43
- package/dist-types/context-extractors.d.ts.map +0 -1
- package/dist-types/deterministic-id-generator.d.ts +0 -70
- package/dist-types/deterministic-id-generator.d.ts.map +0 -1
- package/dist-types/index.d.ts +0 -6
- package/dist-types/index.d.ts.map +0 -1
- package/dist-types/plugin.d.ts +0 -46
- package/dist-types/plugin.d.ts.map +0 -1
- package/dist-types/tsconfig.build.tsbuildinfo +0 -1
package/README.md
CHANGED
|
@@ -1,276 +1,594 @@
|
|
|
1
1
|
# AWS Durable Execution SDK - OpenTelemetry Plugin
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
+
They differ in where durable operation spans are parented:
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
+
When an SDK provider is registered globally, no plugin configuration is
|
|
41
|
+
required:
|
|
30
42
|
|
|
31
|
-
|
|
43
|
+
```typescript
|
|
44
|
+
import { withDurableExecution } from "@aws/durable-execution-sdk-js";
|
|
45
|
+
import { ExecutionOtelPlugin } from "@aws/durable-execution-sdk-js-otel";
|
|
32
46
|
|
|
33
|
-
|
|
47
|
+
const plugin = new ExecutionOtelPlugin();
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
export const handler = withDurableExecution(
|
|
50
|
+
async (event, context) => {
|
|
51
|
+
return context.step("process", async () => process(event));
|
|
52
|
+
},
|
|
53
|
+
{ plugins: [plugin] },
|
|
54
|
+
);
|
|
37
55
|
```
|
|
38
56
|
|
|
39
|
-
|
|
57
|
+
Use `InvocationOtelPlugin` instead when operations should appear under each
|
|
58
|
+
Lambda invocation rather than under the durable Workflow.
|
|
40
59
|
|
|
41
|
-
|
|
60
|
+
## Provider Setup
|
|
42
61
|
|
|
43
|
-
|
|
44
|
-
aws lambda update-function-configuration \
|
|
45
|
-
--function-name your-function-name \
|
|
46
|
-
--layers "arn:aws:lambda:<region>:<<awsAccountId>>:layer:AWSOpenTelemetryDistroJs:<version>"
|
|
47
|
-
```
|
|
62
|
+
### Global Provider
|
|
48
63
|
|
|
49
|
-
|
|
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.
|
|
50
67
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
When using ADOT, activate its instrumentation wrapper:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument
|
|
55
72
|
```
|
|
56
73
|
|
|
57
|
-
|
|
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:
|
|
58
77
|
|
|
59
|
-
|
|
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.
|
|
60
82
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
70
|
-
```
|
|
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.
|
|
71
86
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
83
|
-
```
|
|
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:
|
|
84
91
|
|
|
85
|
-
|
|
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.
|
|
86
95
|
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
);
|
|
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.
|
|
95
104
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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;
|
|
103
128
|
},
|
|
104
129
|
});
|
|
105
130
|
```
|
|
106
131
|
|
|
107
|
-
|
|
132
|
+
`createIdGenerator()` delegates non-durable IDs to OpenTelemetry's standard
|
|
133
|
+
random generator. To preserve another generator, pass it as the fallback:
|
|
108
134
|
|
|
109
|
-
|
|
135
|
+
```typescript
|
|
136
|
+
idGenerator: createIdGenerator(applicationIdGenerator);
|
|
137
|
+
```
|
|
110
138
|
|
|
111
|
-
|
|
139
|
+
The deterministic override is active only for the synchronous `startSpan()`
|
|
140
|
+
call made by the plugin. All unrelated IDs use the fallback generator.
|
|
112
141
|
|
|
113
|
-
|
|
142
|
+
The application owns the returned provider and all associated configuration:
|
|
114
143
|
|
|
115
|
-
|
|
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.
|
|
116
150
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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.
|
|
154
|
+
|
|
155
|
+
At the end of each enabled invocation, both plugins call `forceFlush()` when
|
|
156
|
+
the provider exposes it. They never call `shutdown()`.
|
|
122
157
|
|
|
123
|
-
|
|
158
|
+
## Dynamic Loading from a Lambda Layer
|
|
124
159
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
131
166
|
```
|
|
132
167
|
|
|
133
|
-
|
|
168
|
+
or:
|
|
134
169
|
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
tracing: lambda.Tracing.ACTIVE,
|
|
138
|
-
});
|
|
170
|
+
```text
|
|
171
|
+
DURABLE_EXECUTION_PLUGINS=@aws/durable-execution-sdk-js-otel/otel-invocation
|
|
139
172
|
```
|
|
140
173
|
|
|
141
|
-
|
|
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.
|
|
142
178
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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.
|
|
146
183
|
|
|
147
|
-
|
|
148
|
-
async (event, context) => {
|
|
149
|
-
const result = await context.step("fetch-data", async () => {
|
|
150
|
-
return fetchData(event.id);
|
|
151
|
-
});
|
|
184
|
+
## Choosing a Plugin
|
|
152
185
|
|
|
153
|
-
|
|
186
|
+
### `ExecutionOtelPlugin`
|
|
154
187
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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.
|
|
158
191
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
{ plugins: [new OtelPlugin()] },
|
|
162
|
-
);
|
|
163
|
-
```
|
|
192
|
+
```text
|
|
193
|
+
Execution trace
|
|
164
194
|
|
|
165
|
-
|
|
195
|
+
Execution ancestor
|
|
196
|
+
├── Workflow
|
|
197
|
+
│ └── Operation: fetch-data
|
|
198
|
+
│ └── Attempt 1
|
|
199
|
+
└── Invocation
|
|
166
200
|
|
|
167
|
-
|
|
201
|
+
Links:
|
|
202
|
+
Operation -> Invocation
|
|
203
|
+
Attempt -> Invocation
|
|
204
|
+
```
|
|
168
205
|
|
|
169
|
-
The
|
|
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
|
+
```
|
|
170
235
|
|
|
171
|
-
|
|
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
|
|
287
|
+
```
|
|
172
288
|
|
|
173
|
-
|
|
289
|
+
When no complete remote parent can be constructed, the synthetic execution root
|
|
290
|
+
anchors the trace and the `Workflow` span parents onto it:
|
|
174
291
|
|
|
175
|
-
|
|
292
|
+
```text
|
|
293
|
+
Synthetic execution root
|
|
294
|
+
└── Workflow
|
|
295
|
+
```
|
|
176
296
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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:
|
|
183
373
|
|
|
184
|
-
|
|
374
|
+
```typescript
|
|
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;
|
|
385
|
+
```
|
|
185
386
|
|
|
186
|
-
|
|
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.
|
|
187
395
|
|
|
188
|
-
|
|
396
|
+
## Shared Configuration
|
|
397
|
+
|
|
398
|
+
Both plugins accept `OtelPluginConfig`:
|
|
189
399
|
|
|
190
400
|
```typescript
|
|
191
|
-
|
|
401
|
+
interface OtelPluginConfig {
|
|
402
|
+
/** Creates an application-owned provider with a deterministic ID wrapper. */
|
|
403
|
+
tracerProviderFactory?: (
|
|
404
|
+
createIdGenerator: (fallbackIdGenerator?: IdGenerator) => IdGenerator,
|
|
405
|
+
) => TracerProvider;
|
|
192
406
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
contextExtractor?: xRayContextExtractor,
|
|
407
|
+
/** Extracts upstream trace context. Defaults to xRayContextExtractor. */
|
|
408
|
+
contextExtractor?: ContextExtractor;
|
|
196
409
|
|
|
197
|
-
|
|
198
|
-
|
|
410
|
+
/** Instrumentation scope name. */
|
|
411
|
+
instrumentationName?: string;
|
|
199
412
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
413
|
+
/** Custom Workflow span name. Defaults to "Workflow". */
|
|
414
|
+
workflowSpanName?: string;
|
|
415
|
+
|
|
416
|
+
/** Add active OTel IDs to durable log records. Defaults to true. */
|
|
417
|
+
enrichLogger?: boolean;
|
|
418
|
+
}
|
|
203
419
|
```
|
|
204
420
|
|
|
205
|
-
|
|
421
|
+
Provider selection is implicit:
|
|
206
422
|
|
|
207
|
-
|
|
423
|
+
- omit `tracerProviderFactory` to use the global provider;
|
|
424
|
+
- provide `tracerProviderFactory` to use the returned application-owned
|
|
425
|
+
provider.
|
|
208
426
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
OtelPlugin,
|
|
212
|
-
xRayContextExtractor,
|
|
213
|
-
w3cClientContextExtractor,
|
|
214
|
-
} from "@aws/durable-execution-sdk-js-otel";
|
|
427
|
+
The default instrumentation scope name is
|
|
428
|
+
`aws-durable-execution-sdk-js`.
|
|
215
429
|
|
|
216
|
-
|
|
217
|
-
new OtelPlugin({ contextExtractor: xRayContextExtractor });
|
|
430
|
+
## Spans, Attributes, and Status
|
|
218
431
|
|
|
219
|
-
|
|
220
|
-
new OtelPlugin({ contextExtractor: w3cClientContextExtractor });
|
|
221
|
-
```
|
|
432
|
+
All plugin-created spans use `SpanKind.INTERNAL`.
|
|
222
433
|
|
|
223
|
-
|
|
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`.
|
|
224
451
|
|
|
225
|
-
|
|
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.
|
|
226
455
|
|
|
227
|
-
|
|
456
|
+
OpenTelemetry status mapping is:
|
|
228
457
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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` |
|
|
233
464
|
|
|
234
|
-
|
|
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.
|
|
235
468
|
|
|
236
|
-
|
|
469
|
+
## Log Correlation
|
|
237
470
|
|
|
238
|
-
|
|
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.
|
|
474
|
+
|
|
475
|
+
Disable enrichment when another logging integration already injects equivalent
|
|
476
|
+
fields:
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
const plugin = new ExecutionOtelPlugin({
|
|
480
|
+
enrichLogger: false,
|
|
481
|
+
});
|
|
482
|
+
```
|
|
239
483
|
|
|
240
|
-
|
|
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.
|
|
241
488
|
|
|
242
|
-
|
|
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 |
|
|
489
|
+
## Public API
|
|
248
490
|
|
|
249
|
-
|
|
491
|
+
### Plugins
|
|
250
492
|
|
|
251
|
-
|
|
493
|
+
```typescript
|
|
494
|
+
new ExecutionOtelPlugin(config?: OtelPluginConfig);
|
|
495
|
+
new InvocationOtelPlugin(config?: OtelPluginConfig);
|
|
496
|
+
```
|
|
252
497
|
|
|
253
|
-
|
|
498
|
+
### Provider Types
|
|
254
499
|
|
|
255
500
|
```typescript
|
|
256
|
-
|
|
501
|
+
type IdGeneratorFactory = (fallbackIdGenerator?: IdGenerator) => IdGenerator;
|
|
502
|
+
|
|
503
|
+
type TracerProviderFactory = (
|
|
504
|
+
createIdGenerator: IdGeneratorFactory,
|
|
505
|
+
) => TracerProvider;
|
|
257
506
|
```
|
|
258
507
|
|
|
259
508
|
### `DeterministicIdGenerator`
|
|
260
509
|
|
|
261
|
-
|
|
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.
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
const generator = new DeterministicIdGenerator(fallbackIdGenerator);
|
|
517
|
+
|
|
518
|
+
const span = generator.withIds(
|
|
519
|
+
{ traceId: executionTraceId, spanId: deterministicSpanId },
|
|
520
|
+
() => tracer.startSpan("operation"),
|
|
521
|
+
);
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### ID Helpers
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
deriveTraceIdFromArn(
|
|
528
|
+
executionArn: string,
|
|
529
|
+
executionStartTimestamp?: Date,
|
|
530
|
+
): string;
|
|
262
531
|
|
|
263
|
-
|
|
532
|
+
deriveTraceIdFromXRayRoot(xRayRoot: string): string | undefined;
|
|
264
533
|
|
|
265
|
-
|
|
534
|
+
deriveWorkflowSpanId(executionArn: string): string;
|
|
266
535
|
|
|
267
|
-
|
|
536
|
+
deriveExecutionRootSpanId(executionArn: string): string;
|
|
537
|
+
|
|
538
|
+
deriveSpanIdFromOperationId(
|
|
539
|
+
operationId: string,
|
|
540
|
+
executionArn: string,
|
|
541
|
+
): string;
|
|
542
|
+
```
|
|
268
543
|
|
|
269
|
-
|
|
544
|
+
`deriveTraceIdFromArn` returns a 32-character hexadecimal trace ID.
|
|
545
|
+
`deriveTraceIdFromXRayRoot` converts a valid X-Ray `Root` value to an
|
|
546
|
+
OpenTelemetry trace ID and returns `undefined` for invalid input.
|
|
547
|
+
`deriveWorkflowSpanId` hashes `workflow:<execution ARN>`,
|
|
548
|
+
`deriveExecutionRootSpanId` hashes `execution-root:<execution ARN>` (a distinct
|
|
549
|
+
namespace so the synthetic root never collides with the Workflow or operation
|
|
550
|
+
spans on the shared trace), and `deriveSpanIdFromOperationId` hashes
|
|
551
|
+
`<execution ARN>:<operation ID>`. All three span helpers return 16-character
|
|
552
|
+
hexadecimal IDs.
|
|
270
553
|
|
|
271
|
-
###
|
|
554
|
+
### Context Extractors
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
xRayContextExtractor(info: InvocationInfo): ContextExtractorResult;
|
|
558
|
+
w3cClientContextExtractor(info: InvocationInfo): ContextExtractorResult;
|
|
559
|
+
```
|
|
272
560
|
|
|
273
|
-
|
|
561
|
+
The package also exports the `ContextExtractor`, `ContextExtractorResult`,
|
|
562
|
+
`IdGeneratorFactory`, `TracerProviderFactory`, and `OtelPluginConfig` types.
|
|
563
|
+
|
|
564
|
+
## Verification and Troubleshooting
|
|
565
|
+
|
|
566
|
+
After deployment:
|
|
567
|
+
|
|
568
|
+
1. Invoke a durable function with multiple steps or a wait/resume cycle.
|
|
569
|
+
2. Verify Invocation spans and completed operation spans appear after enabled
|
|
570
|
+
invocations.
|
|
571
|
+
3. Verify one Workflow span appears after the execution becomes terminal.
|
|
572
|
+
4. Expect the Workflow span, every Invocation span, and the operation/attempt
|
|
573
|
+
spans to share one execution trace ID.
|
|
574
|
+
5. Confirm the documented cross-links connect the workflow and invocation views.
|
|
575
|
+
6. Confirm unrelated root spans retain provider-generated trace IDs.
|
|
576
|
+
7. Verify durable log records contain correlation fields when
|
|
577
|
+
`enrichLogger` is enabled.
|
|
578
|
+
|
|
579
|
+
If no plugin spans appear:
|
|
580
|
+
|
|
581
|
+
- confirm a compatible SDK provider is registered before invocation start, or
|
|
582
|
+
return one through `tracerProviderFactory`;
|
|
583
|
+
- confirm the provider has a span processor and exporter;
|
|
584
|
+
- check for the plugin warning that says telemetry was disabled because the
|
|
585
|
+
global tracer was incompatible;
|
|
586
|
+
- remember that dynamic loading supports only the global-provider path;
|
|
587
|
+
- remember that Workflow is not exported until `SUCCEEDED` or `FAILED`.
|
|
588
|
+
|
|
589
|
+
One execution trace is expected. The Workflow span and every per-invocation
|
|
590
|
+
Invocation span share it, anchored at the execution ancestor (the propagated
|
|
591
|
+
remote parent, or a synthetic execution root).
|
|
274
592
|
|
|
275
593
|
## License
|
|
276
594
|
|