Skip to content

Commit c21cf27

Browse files
author
Ayushi Ahjolia
committed
feat(otel): parent durable spans to shared execution trace
1 parent d4090f1 commit c21cf27

16 files changed

Lines changed: 1629 additions & 246 deletions

.github/scripts/tests/test_opentelemetry_conformance_workflow.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
WORKFLOW_PATH = (
55
Path(__file__).parents[2] / "workflows" / "opentelemetry-conformance-tests.yml"
66
)
7-
EXAMPLES_DIR = (
8-
".build/durable-sdk/packages/aws-durable-execution-sdk-python-conformance-tests-otel"
9-
)
7+
EXAMPLES_DIR = ".build/durable-sdk/packages/aws-durable-execution-sdk-python-conformance-tests-otel"
108

119

1210
def test_opentelemetry_conformance_caller_uses_current_workflow_contract() -> None:
@@ -66,8 +64,7 @@ def test_opentelemetry_conformance_runs_when_the_handlers_change() -> None:
6664
workflow = WORKFLOW_PATH.read_text()
6765

6866
trigger_path = (
69-
" - "
70-
'"packages/aws-durable-execution-sdk-python-conformance-tests-otel/**"'
67+
' - "packages/aws-durable-execution-sdk-python-conformance-tests-otel/**"'
7168
)
7269
# Once for pull_request, once for push.
7370
assert workflow.count(trigger_path) == 2

packages/aws-durable-execution-sdk-python-otel/README.md

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
# AWS Durable Execution SDK - OpenTelemetry Plugin
22

3-
OpenTelemetry instrumentation plugin for the [AWS Durable Execution SDK for Python](https://github.com/aws/aws-durable-execution-sdk-python). Emits durable execution spans with deterministic workflow and operation IDs while keeping invocation spans in the ambient Lambda trace.
3+
OpenTelemetry instrumentation plugin for the [AWS Durable Execution SDK for Python](https://github.com/aws/aws-durable-execution-sdk-python). Emits durable execution spans on one execution trace, with deterministic workflow, synthetic-root, and operation span IDs.
44

55
## Features
66

7-
- **Deterministic Workflow Traces**: Durable operations use an execution-derived trace that is independent of the ambient Lambda/X-Ray trace
8-
- **Ambient Invocation Traces**: Invocation spans inherit the active Lambda or extracted upstream context
7+
- **Shared Execution Trace**: Workflow and Invocation spans share one trace, anchored to a propagated backend parent when available or a deterministic synthetic execution root otherwise
8+
- **Same-Trace Ambient Parenting**: Invocation spans use the active ambient span only when it already belongs to the execution trace
99
- **Span-per-Operation**: Each durable operation (step, wait, invoke) gets its own span with accurate timing
1010
- **Continuation Spans**: Operations completing in another invocation produce a new correlated span without fabricating an unobserved prior span context
1111
- **Log Correlation**: Enrich application logs with trace ID and span ID for end-to-end observability
1212
- **Provider Integration**: Use the global ADOT provider or supply an explicit SDK `TracerProvider`
13-
- **Provider-Managed Sampling**: Use standard OpenTelemetry or ADOT sampling configuration
13+
- **Execution Sampling**: Resolve sampling once per invocation and apply it consistently to Workflow, Invocation, operation, and attempt spans
1414

1515
## Installation
1616

@@ -124,7 +124,7 @@ fn = lambda_.Function(
124124
125125
### 2. AWS X-Ray Active Tracing
126126

127-
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.
127+
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 anchor the execution trace on the propagated X-Ray `Root`/`Parent` when both are valid, and preserves `Sampled=1` or `Sampled=0` as the backend sampling decision.
128128

129129
**AWS Console:** Lambda → Configuration → Monitoring and operations tools → Active tracing → Enable
130130

@@ -191,7 +191,7 @@ The function's execution role needs the `AWSXRayDaemonWriteAccess` managed polic
191191
| `OTEL_TRACES_SAMPLER` | Sampler to use (e.g., `traceidratio` for ratio-based sampling) | `always_on` |
192192
| `OTEL_TRACES_SAMPLER_ARG` | Argument for the sampler (e.g., `0.3` to sample 30% of traces) ||
193193

194-
See the [ADOT sampling configuration](https://aws-otel.github.io/docs/getting-started/lambda#sampling-configuration) for more details.
194+
See the [ADOT sampling configuration](https://aws-otel.github.io/docs/getting-started/lambda#sampling-configuration) for more details. When the backend header contains an explicit `Sampled` value, that backend decision takes precedence over local sampler configuration for durable spans.
195195

196196
## Configuration
197197

@@ -220,7 +220,15 @@ plugin = InvocationOtelPlugin(
220220

221221
### Context Extractors
222222

223-
The plugin supports multiple strategies for extracting upstream trace context:
223+
Context extractors return an `ExtractedContext` object, or `None` when no
224+
durable execution trace context is available. The object carries:
225+
226+
- `trace_id`: 128-bit OpenTelemetry trace ID
227+
- `parent_span_id`: 64-bit OpenTelemetry parent span ID
228+
- `sampling`: `Sampling.SAMPLED`, `Sampling.NOT_SAMPLED`, or `Sampling.UNDECIDED`
229+
230+
The plugin supports multiple strategies for extracting durable execution trace
231+
context:
224232

225233
```python
226234
from aws_durable_execution_sdk_python_otel import (
@@ -230,13 +238,65 @@ from aws_durable_execution_sdk_python_otel import (
230238
xray_context_extractor,
231239
)
232240

233-
# Default: X-Ray trace header (recommended for most Lambda deployments)
241+
# Default: X-Ray trace header (recommended for most Lambda deployments).
234242
InvocationOtelPlugin(OtelPluginConfig(context_extractor=xray_context_extractor))
235243

236-
# W3C Trace Context via clientContext (requires backend propagation support)
244+
# W3C Trace Context via clientContext (placeholder for backend propagation support).
237245
InvocationOtelPlugin(OtelPluginConfig(context_extractor=w3c_client_context_extractor))
238246
```
239247

248+
Custom extractors should return `ExtractedContext`, not an OpenTelemetry
249+
`Context`.
250+
251+
### Trace Structure
252+
253+
Both bundled plugins use the same execution ancestor:
254+
255+
- a propagated backend parent when `_X_AMZN_TRACE_ID` contains a valid `Root`
256+
and `Parent`
257+
- otherwise a deterministic, non-recording synthetic root derived from the
258+
durable execution ARN
259+
260+
`InvocationOtelPlugin` keeps durable operation spans under the Invocation span
261+
and links operations to Workflow:
262+
263+
```text
264+
Execution ancestor
265+
├── Workflow
266+
└── Invocation
267+
└── operation
268+
└── operation attempt 1
269+
```
270+
271+
`ExecutionOtelPlugin` keeps operation spans under Workflow and links operations
272+
to the current Invocation span:
273+
274+
```text
275+
Execution ancestor
276+
├── Workflow
277+
│ └── operation
278+
│ └── operation attempt 1
279+
└── Invocation
280+
```
281+
282+
If an ambient Lambda span is active and already has the execution trace ID, the
283+
Invocation span uses that ambient span as its parent. Ambient spans on a
284+
different trace are ignored for durable parenting so Invocation remains on the
285+
execution trace.
286+
287+
### Sampling
288+
289+
Sampling is resolved once per invocation and carried to every durable span in
290+
that invocation. Precedence is:
291+
292+
1. `Sampled=1` or `Sampled=0` from `_X_AMZN_TRACE_ID`
293+
2. a same-trace ambient span's recording/sampled state
294+
3. the configured OpenTelemetry sampler
295+
296+
The resolved decision is applied to Workflow, Invocation, operation, and attempt
297+
spans. This avoids independently querying stateful or ratio-based samplers for
298+
each durable span in the same invocation.
299+
240300
### Log Correlation
241301

242302
When `enrich_logger=True` (the default), the plugin installs a logging filter on
@@ -256,8 +316,9 @@ After deploying your function with the plugin configured:
256316

257317
1. **Invoke your durable function** — trigger at least one execution that includes multiple steps or a wait/resume cycle.
258318

259-
2. **Check the CloudWatch console** — Navigate to CloudWatch → Traces in the AWS Console. You should see a trace with:
260-
- An "invocation" span per invocation
319+
2. **Check the CloudWatch console** — Navigate to CloudWatch → Traces in the AWS Console. You should see an execution trace with:
320+
- A "Workflow" span exported on the terminal invocation
321+
- An "Invocation" span per invocation
261322
- Child spans for each durable operation (named after your step names)
262323
- All invocations of the same execution grouped under one trace ID
263324

@@ -272,15 +333,15 @@ After deploying your function with the plugin configured:
272333
| Symptom | Likely Cause |
273334
| --------------------------------- | --------------------------------------------------------------- |
274335
| No traces appear | ADOT layer not configured, or `AWS_LAMBDA_EXEC_WRAPPER` not set |
275-
| Traces appear but are fragmented | X-Ray active tracing not enabled on the Lambda function |
336+
| Traces appear but are fragmented | Backend trace context is not propagated to every invocation |
276337
| Missing spans for some operations | `OTEL_TRACES_SAMPLER_ARG` set below 1.0 |
277338
| `_X_AMZN_TRACE_ID` not populated | X-Ray active tracing not enabled |
278339

279340
## API Reference
280341

281342
### `InvocationOtelPlugin`
282343

283-
The main plugin class. Implements `DurableInstrumentationPlugin` from `aws_durable_execution_sdk_python`.
344+
Invocation-rooted view. Implements `DurableInstrumentationPlugin` from `aws_durable_execution_sdk_python`.
284345

285346
```python
286347
InvocationOtelPlugin(
@@ -297,21 +358,35 @@ InvocationOtelPlugin(
297358
Pass `tracer_provider=...` when the application owns the OpenTelemetry SDK
298359
provider. When omitted, the globally configured provider is used.
299360

361+
### `ExecutionOtelPlugin`
362+
363+
Execution-rooted view. Uses the same execution ancestor and sampling behavior as
364+
`InvocationOtelPlugin`, but parents operation spans under Workflow and links
365+
them to Invocation.
366+
300367
### `DeterministicIdGenerator`
301368

302369
A custom OpenTelemetry `IdGenerator` that produces reproducible trace and span IDs from execution metadata. Exported for advanced use cases.
303370

304371
### `xray_context_extractor`
305372

306-
Default context extractor. Reads the `_X_AMZN_TRACE_ID` environment variable to derive trace context.
373+
Default context extractor. Reads the `_X_AMZN_TRACE_ID` environment variable and
374+
returns `ExtractedContext` containing parsed `Root`, `Parent`, and `Sampled`
375+
fields when present.
307376

308377
### `w3c_client_context_extractor`
309378

310-
Alternative context extractor. Reads W3C `traceparent` from `context.clientContext.custom.traceparent`. Requires backend `clientContext` propagation to be enabled.
379+
Alternative context extractor placeholder. Returns `None` until backend W3C
380+
`traceparent` propagation is supported.
311381

312382
### `ContextExtractor`
313383

314-
Type alias for custom context extractor functions.
384+
Type alias for custom context extractor functions:
385+
`Callable[[InvocationStartInfo], ExtractedContext | None]`.
386+
387+
### `ExtractedContext` / `Sampling`
388+
389+
Structured trace context and sampling decision returned by context extractors.
315390

316391
### `OtelContextLogFilter` / `install_log_filter`
317392

packages/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
from aws_durable_execution_sdk_python_otel.__about__ import __version__
44
from aws_durable_execution_sdk_python_otel.context_extractors import (
55
ContextExtractor,
6+
ExtractedContext,
7+
Sampling,
68
w3c_client_context_extractor,
79
xray_context_extractor,
810
)
911
from aws_durable_execution_sdk_python_otel.deterministic_id_generator import (
1012
DeterministicIdGenerator,
13+
derive_execution_root_span_id,
1114
derive_workflow_span_id,
1215
operation_id_to_span_id,
1316
)
@@ -35,11 +38,14 @@
3538
"ContextExtractor",
3639
"DeterministicIdGenerator",
3740
"ExecutionOtelPlugin",
41+
"ExtractedContext",
3842
"OtelPluginConfig",
3943
"InvocationOtelPlugin",
4044
"OtelContextLogFilter",
45+
"Sampling",
4146
"ProviderResult",
4247
"create_tracer_provider",
48+
"derive_execution_root_span_id",
4349
"derive_workflow_span_id",
4450
"install_log_filter",
4551
"operation_id_to_span_id",

0 commit comments

Comments
 (0)