Skip to content
Merged
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
Expand Up @@ -8,7 +8,7 @@ version = "0.0.0"
description = "Cross-SDK conformance test handlers for the AWS Durable Execution SDK for Python, exercised by the aws-durable-execution-conformance-tests runner."
requires-python = ">=3.11"
dependencies = [
"aws-durable-execution-sdk-python==1.7.0",
"aws-durable-execution-sdk-python==1.8.0",
Comment thread
zhongkechen marked this conversation as resolved.
]

[tool.hatch.build.targets.wheel]
Expand Down
15 changes: 14 additions & 1 deletion packages/aws-durable-execution-sdk-python-otel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ pip install aws-durable-execution-sdk-python-otel
3. Pass `InvocationOtelPlugin` to your handler's `plugins` list
4. Add X-Ray write permissions

Alternatively, install this package in the function artifact or a Lambda layer
and select either OTel plugin by entry-point name:

```text
DURABLE_EXECUTION_PLUGINS=otel-invocation
DURABLE_EXECUTION_PLUGINS=otel-execution
```

`otel-invocation` creates `InvocationOtelPlugin`; `otel-execution` creates
`ExecutionOtelPlugin`. The SDK discovers the selected package entry point at
cold start, so the handler does not need to import or explicitly register the
plugin.

### 1. ADOT Lambda Layer

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.
Expand Down Expand Up @@ -293,7 +306,7 @@ setups.
## Requirements

- Python >= 3.11
- `aws-durable-execution-sdk-python` >= 1.5.0
- `aws-durable-execution-sdk-python` >= 1.8.0
- `opentelemetry-api` >= 1.20.0
- `opentelemetry-sdk` >= 1.20.0
- `opentelemetry-exporter-otlp`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ classifiers = [
"Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = [
"aws-durable-execution-sdk-python>=1.5.0",
"aws-durable-execution-sdk-python>=1.8.0",
"opentelemetry-api>=1.20.0",
"opentelemetry-sdk>=1.20.0",
"opentelemetry-exporter-otlp",
"opentelemetry-propagator-aws-xray",
]

[project.entry-points."aws_durable_execution.plugins"]
Comment thread
zhongkechen marked this conversation as resolved.
otel-invocation = "aws_durable_execution_sdk_python_otel.plugin_provider:INVOCATION_OTEL_PLUGIN_PROVIDER"
otel-execution = "aws_durable_execution_sdk_python_otel.plugin_provider:EXECUTION_OTEL_PLUGIN_PROVIDER"
Comment thread
zhongkechen marked this conversation as resolved.

[project.optional-dependencies]
# Instrumentation used by ExecutionOtelPlugin's auto-configured provider path.
# Kept optional so the InvocationOtelPlugin (ADOT / global provider) install
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2025-present Amazon.com, Inc. or its affiliates.
#
# SPDX-License-Identifier: Apache-2.0
__version__ = "0.3.0"
__version__ = "0.4.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from aws_durable_execution_sdk_python.plugin import (
DurableInstrumentationPluginProvider,
)
Comment thread
zhongkechen marked this conversation as resolved.

from aws_durable_execution_sdk_python_otel.execution_plugin import (
ExecutionOtelPlugin,
)
from aws_durable_execution_sdk_python_otel.invocation_plugin import (
InvocationOtelPlugin,
)


INVOCATION_OTEL_PLUGIN_PROVIDER = DurableInstrumentationPluginProvider(
plugin_type=InvocationOtelPlugin,
factory=InvocationOtelPlugin,
plugin_api_version=1,
)

EXECUTION_OTEL_PLUGIN_PROVIDER = DurableInstrumentationPluginProvider(
plugin_type=ExecutionOtelPlugin,
factory=ExecutionOtelPlugin,
plugin_api_version=1,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from aws_durable_execution_sdk_python.plugin import (
DURABLE_INSTRUMENTATION_PLUGIN_API_VERSION,
)
from aws_durable_execution_sdk_python.plugin_discovery import (
PLUGIN_ENVIRONMENT_VARIABLE,
load_configured_plugins,
)

from aws_durable_execution_sdk_python_otel.execution_plugin import (
ExecutionOtelPlugin,
)
from aws_durable_execution_sdk_python_otel.invocation_plugin import (
InvocationOtelPlugin,
)
from aws_durable_execution_sdk_python_otel.plugin_provider import (
EXECUTION_OTEL_PLUGIN_PROVIDER,
INVOCATION_OTEL_PLUGIN_PROVIDER,
)
Comment thread
zhongkechen marked this conversation as resolved.


def test_invocation_otel_plugin_provider_uses_current_plugin_api() -> None:
assert INVOCATION_OTEL_PLUGIN_PROVIDER.plugin_type is InvocationOtelPlugin
assert (
INVOCATION_OTEL_PLUGIN_PROVIDER.plugin_api_version
== DURABLE_INSTRUMENTATION_PLUGIN_API_VERSION
)


def test_invocation_otel_plugin_provider_creates_invocation_plugin() -> None:
assert isinstance(INVOCATION_OTEL_PLUGIN_PROVIDER.factory(), InvocationOtelPlugin)


def test_execution_otel_plugin_provider_uses_current_plugin_api() -> None:
assert EXECUTION_OTEL_PLUGIN_PROVIDER.plugin_type is ExecutionOtelPlugin
assert (
EXECUTION_OTEL_PLUGIN_PROVIDER.plugin_api_version
== DURABLE_INSTRUMENTATION_PLUGIN_API_VERSION
)


def test_execution_otel_plugin_provider_creates_execution_plugin() -> None:
assert isinstance(EXECUTION_OTEL_PLUGIN_PROVIDER.factory(), ExecutionOtelPlugin)


def test_installed_otel_entry_points_load_both_plugin_types() -> None:
Comment thread
zhongkechen marked this conversation as resolved.
plugins = load_configured_plugins(
None,
environment={
PLUGIN_ENVIRONMENT_VARIABLE: "otel-invocation,otel-execution",
},
)

assert [type(plugin) for plugin in plugins] == [
InvocationOtelPlugin,
ExecutionOtelPlugin,
]
50 changes: 50 additions & 0 deletions packages/aws-durable-execution-sdk-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,56 @@ Build reliable, long-running AWS Lambda workflows with checkpointed steps, waits
| `aws-durable-execution-sdk-python` | Execution SDK for Lambda durable functions | [![PyPI - Version](https://img.shields.io/pypi/v/aws-durable-execution-sdk-python.svg)](https://pypi.org/project/aws-durable-execution-sdk-python) |
| `aws-durable-execution-sdk-python-testing` | Local/cloud test runner and pytest helpers | [![PyPI - Version](https://img.shields.io/pypi/v/aws-durable-execution-sdk-python-testing.svg)](https://pypi.org/project/aws-durable-execution-sdk-python-testing) |

## Dynamic instrumentation plugins

Instrumentation plugins can be selected at Lambda cold start without importing
them in the function artifact. Install a provider package in the function or a
Lambda layer, then set an ordered allow-list:

```text
DURABLE_EXECUTION_PLUGINS=otel-invocation,example_audit
```

The SDK resolves those names from the `aws_durable_execution.plugins` Python
entry-point group when the decorated handler is initialized. An unset or blank
variable preserves the existing behavior. The decorator's `plugins` argument
remains supported; explicit plugins run first and take precedence when a
dynamic provider creates the same concrete plugin type.

Provider packages expose a versioned factory:

```python
from aws_durable_execution_sdk_python.plugin import (
DurableInstrumentationPlugin,
DurableInstrumentationPluginProvider,
)


class AuditPlugin(DurableInstrumentationPlugin):
pass


AUDIT_PLUGIN_PROVIDER = DurableInstrumentationPluginProvider(
plugin_type=AuditPlugin,
factory=AuditPlugin,
plugin_api_version=1,
)
```

Register the provider in the package's `pyproject.toml`:

```toml
[project.entry-points."aws_durable_execution.plugins"]
example_audit = "example_audit:AUDIT_PLUGIN_PROVIDER"
```

Set `plugin_api_version` to the literal API version the provider implements.
Update it only after verifying the provider against that API version.

Provider names must be unique across installed distributions. Missing,
ambiguous, incompatible, or invalid providers raise `PluginLoadError` during
handler initialization with the provider and distribution details.

## 🚀 Quick Start

Install the execution SDK:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2025-present Amazon.com, Inc. or its affiliates.
#
# SPDX-License-Identifier: Apache-2.0
__version__ = "1.7.0"
__version__ = "1.8.0"
Comment thread
zhongkechen marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
ExecutionError,
InvocationError,
InvokeError,
PluginLoadError,
StepError,
ValidationError,
WaitForConditionError,
Expand Down Expand Up @@ -55,6 +56,7 @@
"InvocationError",
"InvokeError",
"ParallelBranch",
"PluginLoadError",
"StepContext",
"StepError",
"ValidationError",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ class DurableExecutionsError(Exception):
"""Base class for Durable Executions exceptions"""


class PluginLoadError(DurableExecutionsError):
"""A dynamically configured instrumentation plugin could not be loaded."""


class UnrecoverableError(DurableExecutionsError):
"""Base class for errors that terminate execution."""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
DurableInstrumentationPlugin,
PluginExecutor,
)
from aws_durable_execution_sdk_python.plugin_discovery import (
load_configured_plugins,
)
from aws_durable_execution_sdk_python.state import ExecutionState, ReplayStatus


Expand Down Expand Up @@ -194,7 +197,7 @@ def durable_execution(
stacklevel=2, # point the warning to the caller of durable_execution
)

plugin_executor = PluginExecutor(plugins)
plugin_executor = PluginExecutor(load_configured_plugins(plugins))

@plugin_executor.handle_durable_output
def wrapper(event: Any, context: LambdaContext) -> MutableMapping[str, Any]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

logger = logging.getLogger(__name__)

DURABLE_INSTRUMENTATION_PLUGIN_API_VERSION = 1


def _extract_result(operation: Operation) -> str | None:
if operation.step_details and operation.step_details.result is not None:
Expand Down Expand Up @@ -261,6 +263,15 @@ def on_user_function_end(self, info: UserFunctionEndInfo) -> None:
pass


@dataclass(frozen=True)
class DurableInstrumentationPluginProvider:
"""Versioned factory exposed through the plugin entry-point group."""

plugin_type: type[DurableInstrumentationPlugin]
factory: Callable[[], DurableInstrumentationPlugin]
plugin_api_version: int


class PluginExecutor:
def __init__(self, plugins: list[DurableInstrumentationPlugin] | None):
self._plugins = plugins or []
Expand Down
Loading
Loading