aws-durable-execution-sdk-python-otel 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

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.
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2025-present Amazon.com, Inc. or its affiliates.
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
- __version__ = "0.2.0"
4
+ __version__ = "0.3.0"
@@ -14,7 +14,7 @@ from aws_durable_execution_sdk_python_otel.log_filter import (
14
14
  install_log_filter,
15
15
  )
16
16
  from aws_durable_execution_sdk_python_otel.plugin import (
17
- DurableExecutionOtelPlugin,
17
+ OtelPlugin,
18
18
  )
19
19
 
20
20
 
@@ -22,8 +22,8 @@ __all__ = [
22
22
  "__version__",
23
23
  "ContextExtractor",
24
24
  "DeterministicIdGenerator",
25
- "DurableExecutionOtelPlugin",
26
25
  "OtelContextLogFilter",
26
+ "OtelPlugin",
27
27
  "install_log_filter",
28
28
  "w3c_client_context_extractor",
29
29
  "xray_context_extractor",
@@ -66,9 +66,10 @@ def _to_otel_trace_id(execution_arn: str, start_timestamp: datetime | None) -> i
66
66
  return int(f"{time_part}{hash_part}", 16)
67
67
 
68
68
 
69
- def operation_id_to_span_id(operation_id: str) -> int:
70
- """Derive a deterministic span ID (64 bits) from an operation ID."""
71
- hashed_operation_id = hashlib.blake2b(operation_id.encode()).hexdigest()[:16]
69
+ def operation_id_to_span_id(durable_execution_arn: str, operation_id: str) -> int:
70
+ """Derive a deterministic span ID (64 bits) from an execution and operation."""
71
+ plain_value = f"{durable_execution_arn}:{operation_id}"
72
+ hashed_operation_id = hashlib.blake2b(plain_value.encode()).hexdigest()[:16]
72
73
  return int(hashed_operation_id, 16)
73
74
 
74
75
 
@@ -4,12 +4,11 @@ The filter attaches to a stdlib logging handler and enriches *every* record
4
4
  that flows through it: direct ``logging.getLogger().info(...)`` calls,
5
5
  child-logger records that propagate to root, and third-party library logs.
6
6
 
7
- The span/trace identifiers are added as ``LogRecord`` attributes (using
8
- underscore names, since dotted names are not valid record attributes):
7
+ The span/trace identifiers are added as ``LogRecord`` attributes:
9
8
 
10
- - ``otel_trace_id``: 32-char hex trace identifier
11
- - ``otel_span_id``: 16-char hex span identifier
12
- - ``otel_trace_sampled``: boolean indicating if the trace is sampled
9
+ - ``traceId``: 32-char hex trace identifier
10
+ - ``spanId``: 16-char hex span identifier
11
+ - ``otelTraceSampled``: boolean indicating if the trace is sampled
13
12
 
14
13
  These attributes are only set when a valid span context is active. Records
15
14
  emitted outside an active invocation (e.g. during Lambda teardown) pass through
@@ -25,7 +24,7 @@ from opentelemetry.trace import TraceFlags
25
24
 
26
25
 
27
26
  if TYPE_CHECKING:
28
- from aws_durable_execution_sdk_python_otel.plugin import DurableExecutionOtelPlugin
27
+ from aws_durable_execution_sdk_python_otel.plugin import OtelPlugin
29
28
 
30
29
 
31
30
  class OtelContextLogFilter(logging.Filter):
@@ -45,7 +44,7 @@ class OtelContextLogFilter(logging.Filter):
45
44
  plugin: The OTel plugin instance that resolves the current span context.
46
45
  """
47
46
 
48
- def __init__(self, plugin: DurableExecutionOtelPlugin) -> None:
47
+ def __init__(self, plugin: OtelPlugin) -> None:
49
48
  super().__init__()
50
49
  self._plugin = plugin
51
50
 
@@ -53,16 +52,16 @@ class OtelContextLogFilter(logging.Filter):
53
52
  """Stamp the active span context onto the record, then allow it through."""
54
53
  span_context = self._plugin.get_current_span_context()
55
54
  if span_context and span_context.is_valid:
56
- record.otel_trace_id = format(span_context.trace_id, "032x")
57
- record.otel_span_id = format(span_context.span_id, "016x")
58
- record.otel_trace_sampled = bool(
55
+ record.traceId = format(span_context.trace_id, "032x")
56
+ record.spanId = format(span_context.span_id, "016x")
57
+ record.otelTraceSampled = bool(
59
58
  span_context.trace_flags & TraceFlags.SAMPLED
60
59
  )
61
60
  return True
62
61
 
63
62
 
64
63
  def install_log_filter(
65
- plugin: DurableExecutionOtelPlugin,
64
+ plugin: OtelPlugin,
66
65
  target_logger: logging.Logger | None = None,
67
66
  ) -> OtelContextLogFilter | None:
68
67
  """Attach an OtelContextLogFilter to a logger's handlers, idempotently.
@@ -21,7 +21,6 @@ from aws_durable_execution_sdk_python.plugin import (
21
21
  from opentelemetry import context, trace
22
22
  from opentelemetry.context import Context
23
23
  from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider
24
- from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
25
24
  from opentelemetry.trace import (
26
25
  Link,
27
26
  Span,
@@ -52,7 +51,7 @@ def _to_otel_timestamp(dt: datetime.datetime | None) -> int | None:
52
51
  return int(dt.timestamp() * 1_000_000_000)
53
52
 
54
53
 
55
- class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
54
+ class OtelPlugin(DurableInstrumentationPlugin):
56
55
  """OpenTelemetry instrumentation plugin for durable executions.
57
56
 
58
57
  The plugin creates spans for Lambda invocations, durable operations, and
@@ -68,9 +67,10 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
68
67
 
69
68
  Args:
70
69
  trace_provider: OpenTelemetry tracer provider used to create spans.
70
+ Optional; when omitted, the globally configured tracer provider
71
+ (``opentelemetry.trace.get_tracer_provider()``) is used.
71
72
  context_extractor: Optional extractor for upstream context. Defaults to
72
73
  AWS X-Ray header extraction.
73
- sampling_rate: Ratio used by ``TraceIdRatioBased`` sampling.
74
74
  instrument_name: Instrumentation scope name registered with the tracer.
75
75
  """
76
76
 
@@ -78,17 +78,17 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
78
78
 
79
79
  def __init__(
80
80
  self,
81
- trace_provider: SdkTracerProvider,
81
+ trace_provider: SdkTracerProvider | None = None,
82
82
  context_extractor: ContextExtractor | None = None,
83
- sampling_rate: float = 1.0,
84
83
  instrument_name: str = DEFAULT_INSTRUMENT_NAME,
85
84
  enrich_logger: bool = True,
86
85
  ) -> None:
87
86
  """Initialize the plugin with an OpenTelemetry tracer provider.
88
87
 
89
- The provided tracer provider is configured with this plugin's
90
- deterministic ID generator and sampling strategy so spans for a durable
91
- execution share stable trace and logical operation identifiers.
88
+ The tracer provider is configured with this plugin's deterministic ID
89
+ generator so spans for a durable execution share stable trace and
90
+ logical operation identifiers. When no provider is supplied, the
91
+ globally configured tracer provider is used.
92
92
 
93
93
  When enrich_logger is enabled (default), the plugin installs a logging
94
94
  filter on the root logger at invocation start that stamps the active
@@ -99,15 +99,28 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
99
99
  context_extractor or xray_context_extractor
100
100
  )
101
101
 
102
- self._provider = trace_provider
103
- # A ProxyTracerProvider (the API default from trace.get_tracer_provider()
104
- # before an SDK provider is configured) has no id_generator; fall back to
105
- # None so DeterministicIdGenerator uses its own default generator.
102
+ self._provider = trace_provider or trace.get_tracer_provider()
106
103
  self._id_generator: DeterministicIdGenerator = DeterministicIdGenerator(
107
104
  fallback_id_generator=getattr(self._provider, "id_generator", None)
108
105
  )
109
- self._provider.id_generator = self._id_generator
110
- self._provider.sampler = TraceIdRatioBased(sampling_rate)
106
+ # Deterministic trace stitching requires the SDK TracerProvider, which
107
+ # exposes id_generator/sampler. The API's default ProxyTracerProvider
108
+ # (returned before an SDK provider is configured) does not. Rather than
109
+ # fail the invocation over an observability concern, warn and continue:
110
+ # the proxy's tracer is effectively a no-op (and auto-delegates if an SDK
111
+ # provider is configured later). In a Lambda OTel/ADOT deployment the
112
+ # layer configures a real SDK provider before the handler imports.
113
+ if isinstance(self._provider, SdkTracerProvider):
114
+ self._provider.id_generator = self._id_generator
115
+ else:
116
+ logger.warning(
117
+ "OtelPlugin expected an SDK TracerProvider "
118
+ "(opentelemetry.sdk.trace.TracerProvider) but got %s. Spans will "
119
+ "not use deterministic IDs. "
120
+ "Ensure the OpenTelemetry SDK is configured (e.g. via the ADOT "
121
+ "Lambda layer) or pass an explicit trace_provider.",
122
+ type(self._provider).__name__,
123
+ )
111
124
  self._tracer: Tracer = self._provider.get_tracer(instrument_name)
112
125
 
113
126
  # per invocation status:
@@ -139,12 +152,18 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
139
152
  with self._operation_spans_lock:
140
153
  return self._operation_spans.get(operation_id)
141
154
 
155
+ @staticmethod
156
+ def _attempt_span_key(info: UserFunctionStartInfo | UserFunctionEndInfo) -> str:
157
+ """Return the registry key for a STEP attempt span."""
158
+ return f"{info.operation_id}:attempt:{info.attempt or 1}"
159
+
142
160
  def get_current_span_context(self) -> SpanContext | None:
143
161
  """Return the span context to use for log correlation.
144
162
 
145
163
  Resolution order:
146
- 1. The span attached to the OTel thread-local context. Inside a step or
147
- child context this is the active operation span (attached in
164
+ 1. The span attached to the OTel thread-local context. Inside a step
165
+ this is the active attempt span, and inside a child context this is
166
+ the active context span (attached in
148
167
  on_user_function_start), and between operations it is the enclosing
149
168
  operation span (restored in on_user_function_end).
150
169
  2. The invocation span from the plugin registry. This is the path used
@@ -196,6 +215,8 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
196
215
  start_time: datetime.datetime | None = None,
197
216
  parent_span: Span | None = None,
198
217
  existed: bool = False,
218
+ span_key: str | None = None,
219
+ deterministic_span_id: bool = True,
199
220
  ) -> Span:
200
221
  """Start and store a span for an invocation or durable operation.
201
222
 
@@ -210,6 +231,10 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
210
231
  existed: Whether the logical operation already had a previous span.
211
232
  Continuation spans link back to the deterministic span ID for
212
233
  the operation while using a fresh generated span ID.
234
+ span_key: Optional registry key. Defaults to ``operation_id``.
235
+ deterministic_span_id: Whether to use the deterministic operation
236
+ span ID. Attempt spans set this to ``False`` so they can be
237
+ separate children of the logical operation span.
213
238
 
214
239
  Returns:
215
240
  The started OpenTelemetry span.
@@ -220,11 +245,15 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
220
245
  name,
221
246
  parent_span,
222
247
  )
248
+ registry_key = span_key if span_key is not None else operation_id
223
249
  with self._operation_spans_lock:
224
- if existed:
250
+ if not deterministic_span_id:
251
+ links = []
252
+ self._id_generator.set_next_span_id(None)
253
+ elif existed:
225
254
  if not operation_id:
226
255
  raise ValueError("operation id is required")
227
- span_id = operation_id_to_span_id(operation_id)
256
+ span_id = operation_id_to_span_id(self._execution_arn, operation_id)
228
257
  links = [
229
258
  Link(
230
259
  context=SpanContext(
@@ -240,7 +269,9 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
240
269
  links = []
241
270
 
242
271
  self._id_generator.set_next_span_id(
243
- operation_id_to_span_id(operation_id) if operation_id else None
272
+ operation_id_to_span_id(self._execution_arn, operation_id)
273
+ if operation_id
274
+ else None
244
275
  )
245
276
  if parent_span is None:
246
277
  # root span
@@ -256,7 +287,7 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
256
287
  context=parent_context,
257
288
  links=links,
258
289
  )
259
- self._operation_spans[operation_id] = span
290
+ self._operation_spans[registry_key] = span
260
291
 
261
292
  logger.debug("Started OTel span: %s", span)
262
293
  return span
@@ -324,8 +355,8 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
324
355
  def on_operation_start(self, info: OperationStartInfo) -> None:
325
356
  """Called when an operation begins. Creates a span for the operation."""
326
357
  logger.debug("Durable operation started: %s", info)
327
- if info.operation_type in [OperationType.CONTEXT, OperationType.STEP]:
328
- # Context and Step operations are tracked using on_user_function_start
358
+ if info.operation_type is OperationType.CONTEXT:
359
+ # Context operations are tracked using on_user_function_start.
329
360
  return
330
361
  parent_span = self._resolve_parent_span(info.parent_id)
331
362
  attributes = self._extract_attributes(info)
@@ -336,6 +367,7 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
336
367
  attributes=attributes,
337
368
  start_time=info.start_time,
338
369
  parent_span=parent_span,
370
+ existed=info.is_replayed,
339
371
  )
340
372
 
341
373
  def on_operation_end(self, info: OperationEndInfo) -> None:
@@ -348,8 +380,8 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
348
380
  operation span before being ended.
349
381
  """
350
382
  logger.debug("Durable operation ended: %s", info)
351
- if info.operation_type in [OperationType.CONTEXT, OperationType.STEP]:
352
- # Context and Step operations are tracked using on_user_function_end
383
+ if info.operation_type is OperationType.CONTEXT:
384
+ # Context operations are tracked using on_user_function_end.
353
385
  return
354
386
  span = self._get_span(info.operation_id)
355
387
  if not span:
@@ -365,6 +397,8 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
365
397
  parent_span=parent_span,
366
398
  existed=True,
367
399
  )
400
+ else:
401
+ span.set_attributes(self._extract_attributes(info))
368
402
 
369
403
  if info.error:
370
404
  span.set_status(StatusCode.ERROR, info.error.message or "")
@@ -384,8 +418,9 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
384
418
 
385
419
  This callback runs inside the thread that executes user code so the
386
420
  started span can be attached to the OpenTelemetry context for any
387
- instrumentation used by that code. Attempts after the first are emitted
388
- as continuation spans linked to the logical operation span.
421
+ instrumentation used by that code. STEP attempts are emitted as child
422
+ spans beneath the logical STEP operation span created by
423
+ ``on_operation_start``.
389
424
 
390
425
  Args:
391
426
  info: Information about the operation attempt.
@@ -396,15 +431,30 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
396
431
  raise RuntimeError(
397
432
  "on_user_function_start should only be called for CONTEXT and STEP operations"
398
433
  )
399
- parent_span = self._resolve_parent_span(info.parent_id)
434
+ if info.operation_type is OperationType.STEP:
435
+ parent_span = self._get_span(
436
+ info.operation_id
437
+ ) or self._resolve_parent_span(info.parent_id)
438
+ else:
439
+ parent_span = self._resolve_parent_span(info.parent_id)
400
440
  attributes = self._extract_attributes(info)
441
+ span_name = info.name or info.operation_id
442
+ if info.operation_type is OperationType.STEP:
443
+ span_name = f"{span_name} attempt {info.attempt or 1}"
444
+ span_key = (
445
+ self._attempt_span_key(info)
446
+ if info.operation_type is OperationType.STEP
447
+ else info.operation_id
448
+ )
401
449
  span = self._start_span(
402
450
  operation_id=info.operation_id,
403
- name=info.name or info.operation_id,
451
+ name=span_name,
404
452
  attributes=attributes,
405
453
  start_time=info.start_time,
406
454
  parent_span=parent_span,
407
- existed=info.attempt != 1,
455
+ existed=info.attempt != 1 and info.operation_type is not OperationType.STEP,
456
+ span_key=span_key,
457
+ deterministic_span_id=info.operation_type is not OperationType.STEP,
408
458
  )
409
459
  context.attach(trace.set_span_in_context(span, self._extracted_context))
410
460
 
@@ -424,7 +474,12 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
424
474
  "on_user_function_end should only be called for CONTEXT and STEP operations"
425
475
  )
426
476
  # key = f"{info.operation_id}-{int(info.start_time.timestamp())}"
427
- span = self._get_span(info.operation_id)
477
+ span_key = (
478
+ self._attempt_span_key(info)
479
+ if info.operation_type is OperationType.STEP
480
+ else info.operation_id
481
+ )
482
+ span = self._get_span(span_key)
428
483
  if not span:
429
484
  raise RuntimeError(
430
485
  "on_user_function_end called without matching on_user_function_start"
@@ -440,16 +495,13 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
440
495
  else "Unknown error"
441
496
  )
442
497
  )
443
- elif info.outcome is UserFunctionOutcome.SUCCEEDED:
444
- span.set_status(StatusCode.OK)
445
498
  else:
446
- # PENDING
447
- span.set_status(StatusCode.UNSET, "PENDING")
499
+ span.set_status(StatusCode.OK)
448
500
 
449
501
  end_timestamp = info.end_time
450
502
  if end_timestamp is not None and end_timestamp == info.start_time:
451
503
  end_timestamp += datetime.timedelta(microseconds=1)
452
- self._end_span(info.operation_id, end_timestamp)
504
+ self._end_span(span_key, end_timestamp)
453
505
  # Restore the enclosing operation span as current so code that runs
454
506
  # after this operation (e.g. between steps in a child context)
455
507
  # correlates to its enclosing operation, not the operation that just
@@ -478,11 +530,19 @@ class DurableExecutionOtelPlugin(DurableInstrumentationPlugin):
478
530
  attributes["durable.operation.id"] = info.operation_id
479
531
  if hasattr(info, "operation_type") and info.operation_type is not None:
480
532
  attributes["durable.operation.type"] = info.operation_type.value
533
+ if hasattr(info, "sub_type") and info.sub_type is not None:
534
+ attributes["durable.operation.subtype"] = info.sub_type.value
535
+ if hasattr(info, "status") and info.status is not None:
536
+ attributes["durable.operation.status"] = info.status.value
481
537
  if hasattr(info, "name") and info.name is not None:
482
538
  attributes["durable.operation.name"] = info.name
483
- if hasattr(info, "attempt") and info.attempt is not None:
484
- attributes["durable.attempt.number"] = info.attempt
485
- if hasattr(info, "outcome") and info.outcome is not None:
486
- attributes["durable.attempt.outcome"] = info.outcome.value
539
+ # Per-attempt fields are meaningful for STEP (each attempt is retried)
540
+ # but not for CONTEXT (a context is entered once per invocation, not
541
+ # retried). Omit them on CONTEXT spans for cross-SDK consistency.
542
+ if getattr(info, "operation_type", None) is not OperationType.CONTEXT:
543
+ if hasattr(info, "attempt") and info.attempt is not None:
544
+ attributes["durable.attempt.number"] = info.attempt
545
+ if hasattr(info, "outcome") and info.outcome is not None:
546
+ attributes["durable.attempt.outcome"] = info.outcome.value
487
547
 
488
548
  return attributes
@@ -0,0 +1,318 @@
1
+ Metadata-Version: 2.4
2
+ Name: aws-durable-execution-sdk-python-otel
3
+ Version: 0.3.0
4
+ Summary: OpenTelemetry instrumentation for AWS Durable Execution SDK for Python
5
+ Project-URL: Documentation, https://github.com/aws/aws-durable-execution-sdk-python#readme
6
+ Project-URL: Issues, https://github.com/aws/aws-durable-execution-sdk-python/issues
7
+ Project-URL: Source, https://github.com/aws/aws-durable-execution-sdk-python
8
+ Author-email: AWS durable-execution-dev <durable-execution-dev@amazon.com>
9
+ License-Expression: Apache-2.0
10
+ Keywords: durable-execution,observability,opentelemetry,tracing
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: Implementation :: CPython
18
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: aws-durable-execution-sdk-python>=1.5.0
21
+ Requires-Dist: opentelemetry-api>=1.20.0
22
+ Requires-Dist: opentelemetry-exporter-otlp
23
+ Requires-Dist: opentelemetry-propagator-aws-xray
24
+ Requires-Dist: opentelemetry-sdk>=1.20.0
25
+ Description-Content-Type: text/markdown
26
+
27
+ # AWS Durable Execution SDK - OpenTelemetry Plugin
28
+
29
+ OpenTelemetry instrumentation plugin for the [AWS Durable Execution SDK for Python](https://github.com/aws/aws-durable-execution-sdk-python). 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.
30
+
31
+ ## Features
32
+
33
+ - **Deterministic Trace IDs**: All invocations of the same durable execution share a single trace, derived from the X-Ray trace header or execution ARN
34
+ - **Span-per-Operation**: Each durable operation (step, wait, invoke) gets its own span with accurate timing
35
+ - **Continuation Spans**: Operations completing in a different invocation are linked back to the original span
36
+ - **Log Correlation**: Enrich application logs with trace ID and span ID for end-to-end observability
37
+ - **Configurable Sampling**: Control trace volume via plugin options
38
+ - **Self-Contained Setup**: No manual TracerProvider configuration required
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install aws-durable-execution-sdk-python-otel
44
+ ```
45
+
46
+ ## Quick Start using X-Ray/CloudWatch Tracing
47
+
48
+ 1. Add the [ADOT Lambda Layer](#1-adot-lambda-layer) to your function and set `AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument`
49
+ 2. Enable [X-Ray Active Tracing](#2-aws-x-ray-active-tracing) on the function
50
+ 3. Pass `OtelPlugin` to your handler's `plugins` list
51
+ 4. Add X-Ray write permissions
52
+
53
+ ### 1. ADOT Lambda Layer
54
+
55
+ 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.
56
+
57
+ The layer ARN follows the format:
58
+
59
+ ```
60
+ arn:aws:lambda:<region>:<awsAccountId>:layer:aws-otel-python-<arch>-ver-<version>
61
+ ```
62
+
63
+ Refer to the [ADOT Lambda Layer ARNs](https://aws-otel.github.io/docs/getting-started/lambda/lambda-python) page for the latest version number, architecture, and supported regions.
64
+
65
+ **AWS CLI:**
66
+
67
+ ```bash
68
+ aws lambda update-function-configuration \
69
+ --function-name your-function-name \
70
+ --layers "arn:aws:lambda:<region>:<awsAccountId>:layer:aws-otel-python-amd64-ver-<version>"
71
+ ```
72
+
73
+ You must also set the `AWS_LAMBDA_EXEC_WRAPPER` environment variable:
74
+
75
+ ```bash
76
+ aws lambda update-function-configuration \
77
+ --function-name your-function-name \
78
+ --environment "Variables={AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument}"
79
+ ```
80
+
81
+ > **Note:** Replace `<region>` with your function's region and `<version>`/`<arch>` with the latest layer version and architecture from the ADOT docs.
82
+
83
+ **CloudFormation / SAM:**
84
+
85
+ ```yaml
86
+ MyFunction:
87
+ Type: AWS::Serverless::Function
88
+ Properties:
89
+ Layers:
90
+ - !Sub arn:aws:lambda:${AWS::Region}:<awsAccountId>:layer:aws-otel-python-amd64-ver-<version>
91
+ Environment:
92
+ Variables:
93
+ AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-instrument
94
+ ```
95
+
96
+ **CDK:**
97
+
98
+ ```python
99
+ from aws_cdk import aws_lambda as lambda_
100
+
101
+ adot_layer = lambda_.LayerVersion.from_layer_version_arn(
102
+ self,
103
+ "AdotLayer",
104
+ f"arn:aws:lambda:<region>:<awsAccountId>:layer:aws-otel-python-amd64-ver-<version>",
105
+ )
106
+
107
+ fn = lambda_.Function(
108
+ self,
109
+ "MyFunction",
110
+ runtime=lambda_.Runtime.PYTHON_3_12,
111
+ handler="index.handler",
112
+ code=lambda_.Code.from_asset("lambda"),
113
+ layers=[adot_layer],
114
+ environment={"AWS_LAMBDA_EXEC_WRAPPER": "/opt/otel-instrument"},
115
+ )
116
+ ```
117
+
118
+ > **Tip:** Pin the layer version to a specific number in production deployments to avoid unexpected behavior from automatic version changes.
119
+
120
+ ### 2. AWS X-Ray Active Tracing
121
+
122
+ 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.
123
+
124
+ **AWS Console:** Lambda → Configuration → Monitoring and operations tools → Active tracing → Enable
125
+
126
+ **AWS CLI:**
127
+
128
+ ```bash
129
+ aws lambda update-function-configuration \
130
+ --function-name your-function-name \
131
+ --tracing-config Mode=Active
132
+ ```
133
+
134
+ **CloudFormation / SAM:**
135
+
136
+ ```yaml
137
+ MyFunction:
138
+ Type: AWS::Lambda::Function
139
+ Properties:
140
+ TracingConfig:
141
+ Mode: Active
142
+ ```
143
+
144
+ **CDK:**
145
+
146
+ ```python
147
+ lambda_.Function(
148
+ self,
149
+ "MyFunction",
150
+ tracing=lambda_.Tracing.ACTIVE,
151
+ )
152
+ ```
153
+
154
+ ### 3. In your Lambda handler (index.py)
155
+
156
+ ```python
157
+ from aws_durable_execution_sdk_python import DurableContext
158
+ from aws_durable_execution_sdk_python.execution import durable_execution
159
+ from aws_durable_execution_sdk_python_otel import OtelPlugin
160
+
161
+
162
+ @durable_execution(plugins=[OtelPlugin()])
163
+ def handler(event: dict, context: DurableContext) -> dict:
164
+ result = context.step(lambda _: fetch_data(event["id"]), name="fetch-data")
165
+
166
+ context.wait(duration=Duration.from_seconds(5))
167
+
168
+ context.step(lambda _: process(result), name="process")
169
+
170
+ return result
171
+ ```
172
+
173
+ That's it. The plugin handles TracerProvider setup, deterministic ID generation, and span lifecycle internally.
174
+
175
+ ### 4. Grant Permissions
176
+
177
+ The function's execution role needs the `AWSXRayDaemonWriteAccess` managed policy (or equivalent permissions) if using X-Ray as the tracing backend.
178
+
179
+ ### Environment Variables for ADOT layer
180
+
181
+ | Variable | Description | Default |
182
+ | ----------------------------- | --------------------------------------------------------------------------------------------- | ----------------- |
183
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | Endpoint for the OTLP exporter (e.g., `http://localhost:4318` for the ADOT collector sidecar) | Set by ADOT layer |
184
+ | `AWS_LAMBDA_EXEC_WRAPPER` | Set to `/opt/otel-instrument` for the ADOT layer to instrument your function | — |
185
+ | `OTEL_TRACES_SAMPLER` | Sampler to use (e.g., `traceidratio` for ratio-based sampling) | `always_on` |
186
+ | `OTEL_TRACES_SAMPLER_ARG` | Argument for the sampler (e.g., `0.3` to sample 30% of traces) | — |
187
+
188
+ See the [ADOT sampling configuration](https://aws-otel.github.io/docs/getting-started/lambda#sampling-configuration) for more details.
189
+
190
+ ## Configuration
191
+
192
+ ### Plugin Options
193
+
194
+ ```python
195
+ from aws_durable_execution_sdk_python_otel import (
196
+ OtelPlugin,
197
+ xray_context_extractor,
198
+ )
199
+
200
+ plugin = OtelPlugin(
201
+ # Provide your own TracerProvider if you already have one configured.
202
+ # Defaults to the globally configured tracer provider.
203
+ trace_provider=None,
204
+ # Use a custom context extractor (default: xray_context_extractor).
205
+ context_extractor=xray_context_extractor,
206
+ # Custom instrumentation scope name
207
+ # (default: "aws-durable-execution-sdk-python").
208
+ instrument_name="my-service",
209
+ # Install a root-logger filter that stamps trace context onto every
210
+ # log record (default: True).
211
+ enrich_logger=True,
212
+ )
213
+ ```
214
+
215
+ ### Context Extractors
216
+
217
+ The plugin supports multiple strategies for extracting upstream trace context:
218
+
219
+ ```python
220
+ from aws_durable_execution_sdk_python_otel import (
221
+ OtelPlugin,
222
+ w3c_client_context_extractor,
223
+ xray_context_extractor,
224
+ )
225
+
226
+ # Default: X-Ray trace header (recommended for most Lambda deployments)
227
+ OtelPlugin(context_extractor=xray_context_extractor)
228
+
229
+ # W3C Trace Context via clientContext (requires backend propagation support)
230
+ OtelPlugin(context_extractor=w3c_client_context_extractor)
231
+ ```
232
+
233
+ ### Log Correlation
234
+
235
+ When `enrich_logger=True` (the default), the plugin installs a logging filter on
236
+ the root logger at invocation start. The filter stamps the active OTel trace
237
+ context onto every emitted log record using these attributes:
238
+
239
+ - `traceId`: 32-char hex trace identifier
240
+ - `spanId`: 16-char hex span identifier
241
+ - `otelTraceSampled`: boolean indicating if the trace is sampled
242
+
243
+ These attributes are only set when a valid span context is active, so any log
244
+ formatter or schema must treat the fields as optional.
245
+
246
+ ## Verification
247
+
248
+ After deploying your function with the plugin configured:
249
+
250
+ 1. **Invoke your durable function** — trigger at least one execution that includes multiple steps or a wait/resume cycle.
251
+
252
+ 2. **Check the CloudWatch console** — Navigate to CloudWatch → Traces in the AWS Console. You should see a trace with:
253
+ - An "invocation" span per invocation
254
+ - Child spans for each durable operation (named after your step names)
255
+ - All invocations of the same execution grouped under one trace ID
256
+
257
+ 3. **Check log correlation** — verify that your logs include `traceId` and `spanId` fields matching the spans in X-Ray.
258
+
259
+ 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.
260
+
261
+ 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 the CloudWatch console.
262
+
263
+ ### Troubleshooting
264
+
265
+ | Symptom | Likely Cause |
266
+ | --------------------------------- | --------------------------------------------------------------- |
267
+ | No traces appear | ADOT layer not configured, or `AWS_LAMBDA_EXEC_WRAPPER` not set |
268
+ | Traces appear but are fragmented | X-Ray active tracing not enabled on the Lambda function |
269
+ | Missing spans for some operations | `OTEL_TRACES_SAMPLER_ARG` set below 1.0 |
270
+ | `_X_AMZN_TRACE_ID` not populated | X-Ray active tracing not enabled |
271
+
272
+ ## API Reference
273
+
274
+ ### `OtelPlugin`
275
+
276
+ The main plugin class. Implements `DurableInstrumentationPlugin` from `aws_durable_execution_sdk_python`.
277
+
278
+ ```python
279
+ OtelPlugin(
280
+ trace_provider=None,
281
+ context_extractor=None,
282
+ instrument_name="aws-durable-execution-sdk-python",
283
+ enrich_logger=True,
284
+ )
285
+ ```
286
+
287
+ ### `DeterministicIdGenerator`
288
+
289
+ A custom OpenTelemetry `IdGenerator` that produces reproducible trace and span IDs from execution metadata. Exported for advanced use cases.
290
+
291
+ ### `xray_context_extractor`
292
+
293
+ Default context extractor. Reads the `_X_AMZN_TRACE_ID` environment variable to derive trace context.
294
+
295
+ ### `w3c_client_context_extractor`
296
+
297
+ Alternative context extractor. Reads W3C `traceparent` from `context.clientContext.custom.traceparent`. Requires backend `clientContext` propagation to be enabled.
298
+
299
+ ### `ContextExtractor`
300
+
301
+ Type alias for custom context extractor functions.
302
+
303
+ ### `OtelContextLogFilter` / `install_log_filter`
304
+
305
+ The logging filter (and its installer) used to stamp trace context onto log
306
+ records. Installed automatically when `enrich_logger=True`; exported for manual
307
+ setups.
308
+
309
+ ## Requirements
310
+
311
+ - Python >= 3.11
312
+ - `aws-durable-execution-sdk-python` >= 1.5.0
313
+ - `opentelemetry-api` >= 1.20.0
314
+ - `opentelemetry-sdk` >= 1.20.0
315
+
316
+ ## License
317
+
318
+ Apache-2.0
@@ -0,0 +1,12 @@
1
+ aws_durable_execution_sdk_python_otel/__about__.py,sha256=eMP6nYgoHQWcCgV4SvwAgcs8UrO25qIlEa33am5Bfpw,137
2
+ aws_durable_execution_sdk_python_otel/__init__.py,sha256=8MbvOj7Bde-BHrZzvFqphYvGghUoXG5dN0hGhmSHMLw,844
3
+ aws_durable_execution_sdk_python_otel/context_extractors.py,sha256=hI3ZeVsNZHYyw1eOgvDJv8Y5NQY2VDpzTr_VyNJMtPU,1332
4
+ aws_durable_execution_sdk_python_otel/deterministic_id_generator.py,sha256=5lV3pyu0krZaoPwscOQG0lwV4caz5XXnPsa-FPFWQKs,5061
5
+ aws_durable_execution_sdk_python_otel/log_filter.py,sha256=ypfbFWDbE35sxOzOZ32yIHbOIOR7ySMVymfsMfyr46U,4128
6
+ aws_durable_execution_sdk_python_otel/plugin.py,sha256=VSyo160gyro4xv1sRM0JPiOYhSQPI2NYEUKz5zU5aQs,23926
7
+ aws_durable_execution_sdk_python_otel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ aws_durable_execution_sdk_python_otel/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
9
+ aws_durable_execution_sdk_python_otel/NOTICE,sha256=1CkO1kwu3Q_OHYTj-d-yiBJA_lNN73a4zSntavaD4oc,67
10
+ aws_durable_execution_sdk_python_otel-0.3.0.dist-info/METADATA,sha256=F8Pc8geychW3Pq2hFnRUM6ny07bptbjPnBXps8qgJiQ,12240
11
+ aws_durable_execution_sdk_python_otel-0.3.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
12
+ aws_durable_execution_sdk_python_otel-0.3.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.30.1
2
+ Generator: hatchling 1.31.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,67 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: aws-durable-execution-sdk-python-otel
3
- Version: 0.2.0
4
- Summary: OpenTelemetry instrumentation for AWS Durable Execution SDK for Python
5
- Project-URL: Documentation, https://github.com/aws/aws-durable-execution-sdk-python#readme
6
- Project-URL: Issues, https://github.com/aws/aws-durable-execution-sdk-python/issues
7
- Project-URL: Source, https://github.com/aws/aws-durable-execution-sdk-python
8
- Author-email: AWS durable-execution-dev <durable-execution-dev@amazon.com>
9
- License-Expression: Apache-2.0
10
- Keywords: durable-execution,observability,opentelemetry,tracing
11
- Classifier: Development Status :: 4 - Beta
12
- Classifier: Programming Language :: Python
13
- Classifier: Programming Language :: Python :: 3.11
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Programming Language :: Python :: 3.13
16
- Classifier: Programming Language :: Python :: 3.14
17
- Classifier: Programming Language :: Python :: Implementation :: CPython
18
- Classifier: Programming Language :: Python :: Implementation :: PyPy
19
- Requires-Python: >=3.11
20
- Requires-Dist: aws-durable-execution-sdk-python>=1.5.0
21
- Requires-Dist: opentelemetry-api>=1.20.0
22
- Requires-Dist: opentelemetry-exporter-otlp
23
- Requires-Dist: opentelemetry-propagator-aws-xray
24
- Requires-Dist: opentelemetry-sdk>=1.20.0
25
- Description-Content-Type: text/markdown
26
-
27
- # aws-durable-execution-sdk-python-otel
28
-
29
- OpenTelemetry instrumentation for the [AWS Durable Execution SDK for Python](https://github.com/aws/aws-durable-execution-sdk-python).
30
-
31
- > **Note:** v0.1.0 reserves the package name. Instrumentation lands in v0.2.0.
32
-
33
- ## Overview
34
-
35
- This package will provide automatic OpenTelemetry tracing for durable execution workflows, giving you visibility into step execution, waits, retries, and overall workflow performance.
36
-
37
- ## Installation
38
-
39
- ```bash
40
- pip install aws-durable-execution-sdk-python-otel
41
- ```
42
-
43
- ## Quick Start
44
-
45
- ```python
46
- from aws_durable_execution_sdk_python_otel import __version__
47
-
48
- print(__version__)
49
- ```
50
-
51
- ## Planned Features (v0.2.0)
52
-
53
- - Automatic span creation for steps, waits, invokes, and child contexts
54
- - Replay-aware tracing (distinguishes fresh executions from replays)
55
- - Error recording with proper OTel status codes
56
- - Configurable span attributes and naming
57
-
58
- ## Requirements
59
-
60
- - Python >= 3.11
61
- - `aws-durable-execution-sdk-python` >= 1.5.0
62
- - `opentelemetry-api` >= 1.20.0
63
- - `opentelemetry-sdk` >= 1.20.0
64
-
65
- ## License
66
-
67
- Apache-2.0
@@ -1,12 +0,0 @@
1
- aws_durable_execution_sdk_python_otel/__about__.py,sha256=fN6OMpnwGW4dbezf3aG7wuWrAhfRPpRGifGrGbfb5QI,137
2
- aws_durable_execution_sdk_python_otel/__init__.py,sha256=3ssbxJTadtJWAv8TZVfWL0a6-d5kpmxj_pJCtpKT56w,876
3
- aws_durable_execution_sdk_python_otel/context_extractors.py,sha256=hI3ZeVsNZHYyw1eOgvDJv8Y5NQY2VDpzTr_VyNJMtPU,1332
4
- aws_durable_execution_sdk_python_otel/deterministic_id_generator.py,sha256=yVMJAvuzqOgFrw7vuHkUlrdAu53FtKtPf6jHLjK_AhQ,4963
5
- aws_durable_execution_sdk_python_otel/log_filter.py,sha256=FUDiNJ1cj4p-SRZ09YEZDpvZgWOA8BMw0MKTcoWAIrg,4281
6
- aws_durable_execution_sdk_python_otel/plugin.py,sha256=7OBNlpWmDxunvuXQvsMaHiKR-ZkofN0w1CUXxyWuXUE,20791
7
- aws_durable_execution_sdk_python_otel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- aws_durable_execution_sdk_python_otel/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
9
- aws_durable_execution_sdk_python_otel/NOTICE,sha256=1CkO1kwu3Q_OHYTj-d-yiBJA_lNN73a4zSntavaD4oc,67
10
- aws_durable_execution_sdk_python_otel-0.2.0.dist-info/METADATA,sha256=O1AdqEFUVG-gQbsBNbBvB-HjuqPwxVVnyYZOpTbWD3k,2386
11
- aws_durable_execution_sdk_python_otel-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
12
- aws_durable_execution_sdk_python_otel-0.2.0.dist-info/RECORD,,