Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Demonstrates OTel-enriched logging in a durable execution.

The OtelPlugin installs a logging filter on the root logger
The InvocationOtelPlugin installs a logging filter on the root logger
(enrich_logger=True by default) when the plugin is constructed. The filter
stamps the active OpenTelemetry trace context (traceId, spanId,
otelTraceSampled) onto every log record that flows through the root handler.
Expand All @@ -16,7 +16,7 @@

from typing import Any

from aws_durable_execution_sdk_python_otel import OtelPlugin
from aws_durable_execution_sdk_python_otel import InvocationOtelPlugin

from aws_durable_execution_sdk_python import StepContext
from aws_durable_execution_sdk_python.context import (
Expand Down Expand Up @@ -44,7 +44,7 @@ def greet_in_child(child_context: DurableContext, name: str) -> str:
return result


@durable_execution(plugins=[OtelPlugin()])
@durable_execution(plugins=[InvocationOtelPlugin()])
def handler(_event: Any, context: DurableContext) -> str:
# Logged at the top level: enriched with the invocation span_id.
context.logger.info("Workflow started")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import Any

from aws_durable_execution_sdk_python_otel import OtelPlugin
from aws_durable_execution_sdk_python_otel import InvocationOtelPlugin

from aws_durable_execution_sdk_python import StepContext
from aws_durable_execution_sdk_python.config import Duration
Expand Down Expand Up @@ -32,7 +32,7 @@ def add_numbers_in_child(child_context: DurableContext, a: int, b: int):
return result


@durable_execution(plugins=[OtelPlugin()])
@durable_execution(plugins=[InvocationOtelPlugin()])
def handler(_event: Any, context: DurableContext) -> int:
result = 0
for i in range(3):
Expand Down
20 changes: 10 additions & 10 deletions packages/aws-durable-execution-sdk-python-otel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pip install aws-durable-execution-sdk-python-otel

1. Add the [ADOT Lambda Layer](#1-adot-lambda-layer) to your function and set `AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument`
2. Enable [X-Ray Active Tracing](#2-aws-x-ray-active-tracing) on the function
3. Pass `OtelPlugin` to your handler's `plugins` list
3. Pass `InvocationOtelPlugin` to your handler's `plugins` list
4. Add X-Ray write permissions

### 1. ADOT Lambda Layer
Expand Down Expand Up @@ -130,10 +130,10 @@ lambda_.Function(
```python
from aws_durable_execution_sdk_python import DurableContext
from aws_durable_execution_sdk_python.execution import durable_execution
from aws_durable_execution_sdk_python_otel import OtelPlugin
from aws_durable_execution_sdk_python_otel import InvocationOtelPlugin


@durable_execution(plugins=[OtelPlugin()])
@durable_execution(plugins=[InvocationOtelPlugin()])
def handler(event: dict, context: DurableContext) -> dict:
result = context.step(lambda _: fetch_data(event["id"]), name="fetch-data")

Expand Down Expand Up @@ -167,11 +167,11 @@ See the [ADOT sampling configuration](https://aws-otel.github.io/docs/getting-st

```python
from aws_durable_execution_sdk_python_otel import (
OtelPlugin,
InvocationOtelPlugin,
xray_context_extractor,
)

plugin = OtelPlugin(
plugin = InvocationOtelPlugin(
# Provide your own TracerProvider if you already have one configured.
# Defaults to the globally configured tracer provider.
trace_provider=None,
Expand All @@ -192,16 +192,16 @@ The plugin supports multiple strategies for extracting upstream trace context:

```python
from aws_durable_execution_sdk_python_otel import (
OtelPlugin,
InvocationOtelPlugin,
w3c_client_context_extractor,
xray_context_extractor,
)

# Default: X-Ray trace header (recommended for most Lambda deployments)
OtelPlugin(context_extractor=xray_context_extractor)
InvocationOtelPlugin(context_extractor=xray_context_extractor)

# W3C Trace Context via clientContext (requires backend propagation support)
OtelPlugin(context_extractor=w3c_client_context_extractor)
InvocationOtelPlugin(context_extractor=w3c_client_context_extractor)
```

### Log Correlation
Expand Down Expand Up @@ -245,12 +245,12 @@ After deploying your function with the plugin configured:

## API Reference

### `OtelPlugin`
### `InvocationOtelPlugin`

The main plugin class. Implements `DurableInstrumentationPlugin` from `aws_durable_execution_sdk_python`.

```python
OtelPlugin(
InvocationOtelPlugin(
trace_provider=None,
context_extractor=None,
instrument_name="aws-durable-execution-sdk-python",
Expand Down
9 changes: 9 additions & 0 deletions packages/aws-durable-execution-sdk-python-otel/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ dependencies = [
"opentelemetry-propagator-aws-xray",
]

[project.optional-dependencies]
# Instrumentation used by ExecutionOtelPlugin's auto-configured provider path.
# Kept optional so the InvocationOtelPlugin (ADOT / global provider) install
# stays lean; the instrumentations module degrades gracefully when absent.
instrumentation = [
"opentelemetry-instrumentation-botocore",
"opentelemetry-instrumentation-urllib3",
]

[project.urls]
Documentation = "https://github.com/aws/aws-durable-execution-sdk-python#readme"
Issues = "https://github.com/aws/aws-durable-execution-sdk-python/issues"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,47 @@
)
from aws_durable_execution_sdk_python_otel.deterministic_id_generator import (
DeterministicIdGenerator,
derive_workflow_span_id,
operation_id_to_span_id,
)
from aws_durable_execution_sdk_python_otel.execution_plugin import (
ExecutionOtelPlugin,
)
from aws_durable_execution_sdk_python_otel.otel_plugin_config import (
OtelPluginConfig,
ExporterConfig,
)
from aws_durable_execution_sdk_python_otel.instrumentations import (
register_standalone_instrumentations,
)
from aws_durable_execution_sdk_python_otel.log_filter import (
OtelContextLogFilter,
install_log_filter,
)
from aws_durable_execution_sdk_python_otel.plugin import (
OtelPlugin,
from aws_durable_execution_sdk_python_otel.invocation_plugin import (
InvocationOtelPlugin,
)
from aws_durable_execution_sdk_python_otel.provider import (
ProviderResult,
create_tracer_provider,
)


__all__ = [
"__version__",
"ContextExtractor",
"DeterministicIdGenerator",
"ExecutionOtelPlugin",
"OtelPluginConfig",
"ExporterConfig",
"InvocationOtelPlugin",
"OtelContextLogFilter",
"OtelPlugin",
"ProviderResult",
"create_tracer_provider",
"derive_workflow_span_id",
"install_log_filter",
"operation_id_to_span_id",
"register_standalone_instrumentations",
"w3c_client_context_extractor",
"xray_context_extractor",
]
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,39 @@ def operation_id_to_span_id(durable_execution_arn: str, operation_id: str) -> in
return int(hashed_operation_id, 16)


def derive_workflow_span_id(durable_execution_arn: str) -> int:
"""Derive the deterministic Workflow root span ID (64 bits) from an execution ARN.

Mirrors the JS ``deriveWorkflowSpanId``: hash ``"workflow:" + arn`` and take
the first 16 hex characters. All invocations of the same durable execution
therefore share one Workflow root span ID, allowing the Workflow span to be
(re-)created and exported exactly once per execution regardless of how many
Lambda invocations back it.

The hash algorithm is blake2b to stay consistent with
:func:`operation_id_to_span_id`; it does not need to match the JS SDK's
SHA-256 byte-for-byte because span IDs only need to be stable within a single
execution's trace, not across language runtimes.

Args:
durable_execution_arn: The durable execution ARN. Must be non-empty.

Returns:
A 64-bit integer span ID. If the derived value is all-zero (invalid per
the OTel spec) it is bumped to ``1``.

Raises:
ValueError: If ``durable_execution_arn`` is empty. Only emptiness is
validated; ARN format is not checked.
"""
if not durable_execution_arn:
raise ValueError("execution ARN is required to derive a workflow span ID")
plain_value = f"workflow:{durable_execution_arn}"
hashed = hashlib.blake2b(plain_value.encode()).hexdigest()[:16]
span_id = int(hashed, 16)
return span_id or 1


class DeterministicIdGenerator(RandomIdGenerator):
"""An ID generator that produces deterministic span IDs when a pending
operation ID is set, and falls back to the provided generator otherwise.
Expand Down
Loading