Skip to content
Draft
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
55 changes: 55 additions & 0 deletions packages/aws-durable-execution-sdk-python-insight/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# AWS Durable Execution SDK for Python — Workflow Insight plugin

Workflow Insight instrumentation plugin for the AWS Durable Execution SDK for
Python. A port of the JavaScript SDK's `workflowInsight()` plugin: it listens to
the SDK's instrumentation hooks and emits one curated `WorkflowInsight` record
per execution to the configured exporters. The wire record keeps the JS
camelCase field names so records read identically across SDKs.

> **Experimental.** Like its JS counterpart, this plugin is experimental and may
> change or be removed in future releases.

## Install

```bash
pip install aws-durable-execution-sdk-python-insight
# with the S3 exporter's local-dev dependency:
pip install "aws-durable-execution-sdk-python-insight[s3]"
```

## Usage

```python
from aws_durable_execution_sdk_python import durable_execution
from aws_durable_execution_sdk_python_insight import workflow_insight
from aws_durable_execution_sdk_python_insight.exporters import S3Exporter

@durable_execution(
plugins=[
workflow_insight(
exporters=[S3Exporter(bucket="my-bucket", prefix="workflow-insight/")],
)
]
)
def handler(event, context):
...
```

With no exporter configured, records are written to the function's own
CloudWatch log group as single JSON lines (the `LambdaLogExporter` default),
carrying the name-keyed `operationsByName` summary. The `S3Exporter` writes the
lossless per-occurrence `operations` array, one object per execution
(upsert-by-execution-name, so re-emission overwrites rather than appends).

Emission behavior, record schema (`recordType: WorkflowInsight`,
`schemaVersion: "1.0"`), sampling, content configuration (input/output
omission, `include_errors`, per-operation result opt-in), truncation phases,
and `top-level` vs `full-tree` operation detail all mirror the JS plugin.
Behavior is validated cross-SDK by the `insight` conformance suite
(`aws-durable-execution-conformance-tests-insight`).

## Requirements

- `aws-durable-execution-sdk-python` with the plugin invocation hooks that
surface `execution_input` / `execution_result` (included since the version
this package declares as its minimum).
79 changes: 79 additions & 0 deletions packages/aws-durable-execution-sdk-python-insight/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "aws-durable-execution-sdk-python-insight"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review

High: The repository's PyPI workflow still enumerates only core, OTel, and testing packages. This distribution will therefore never be built or uploaded by a release, making the documented pip install unavailable. Add it to both release matrices and distribution legal-file verification.

dynamic = ["version"]
description = 'Workflow Insight instrumentation plugin for the AWS Durable Execution SDK for Python'
readme = "README.md"
requires-python = ">=3.11"
license = "Apache-2.0"
keywords = ["observability", "workflow-insight", "durable-execution"]
authors = [{ name = "AWS durable-execution-dev", email = "durable-execution-dev@amazon.com" }]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: CPython",
]
dependencies = [
# >=1.8.0: first release carrying the plugin invocation-hook fields
# (InvocationInfo.execution_input / InvocationEndInfo.execution_result).
"aws-durable-execution-sdk-python>=1.8.0",
]

[project.optional-dependencies]
# boto3 is provided by the Lambda runtime; declared as an extra for local dev
# (e.g. the S3Exporter) without vendoring it into deployments.
s3 = ["boto3>=1.26.0"]

[project.urls]
Documentation = "https://github.com/aws/aws-durable-execution-sdk-python#readme"
Issues = "https://github.com/aws/aws-durable-execution-sdk-python/issues"
Source = "https://github.com/aws/aws-durable-execution-sdk-python"

[tool.hatch.build.targets.sdist.force-include]
"../../LICENSE" = "LICENSE"
"../../NOTICE" = "NOTICE"

[tool.hatch.build.targets.wheel]
packages = ["src/aws_durable_execution_sdk_python_insight"]

[tool.hatch.build.targets.wheel.force-include]
"../../LICENSE" = "aws_durable_execution_sdk_python_insight/LICENSE"
"../../NOTICE" = "aws_durable_execution_sdk_python_insight/NOTICE"

[tool.hatch.version]
path = "src/aws_durable_execution_sdk_python_insight/__about__.py"

[tool.hatch.publish.index]
disable = true

[tool.coverage.run]
source_pkgs = ["aws_durable_execution_sdk_python_insight"]
branch = true
parallel = true
omit = ["src/aws_durable_execution_sdk_python_insight/__about__.py"]

[tool.coverage.report]
exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]

[tool.ruff]
line-length = 88
target-version = "py311"

[tool.ruff.lint]
preview = true
select = ["E4", "E7", "E9", "F", "TID252"]

[tool.ruff.lint.isort]
known-first-party = ["aws_durable_execution_sdk_python_insight"]
force-single-line = false
lines-after-imports = 2

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["ARG001", "ARG002", "ARG005", "S101", "PLR2004", "PLR6301", "SIM117", "TRY301"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates.
#
# SPDX-License-Identifier: Apache-2.0
__version__ = "0.0.1"
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates.
#
# SPDX-License-Identifier: Apache-2.0
"""Workflow Insight instrumentation plugin for the AWS Durable Execution Python SDK."""

from aws_durable_execution_sdk_python_insight.__about__ import __version__
from aws_durable_execution_sdk_python_insight.exporters import (
LambdaLogExporter,
S3Exporter,
)
from aws_durable_execution_sdk_python_insight.operations_index import (
build_operations_by_name,
with_operations_by_name,
)
from aws_durable_execution_sdk_python_insight.plugin import (
WorkflowInsightPlugin,
workflow_insight,
)
from aws_durable_execution_sdk_python_insight.truncation import truncate_record
from aws_durable_execution_sdk_python_insight.types import (
ContentConfig,
ContentOperations,
InsightExporter,
OperationOverride,
WorkflowInsightConfig,
)


__all__ = [
"__version__",
"ContentConfig",
"ContentOperations",
"InsightExporter",
"LambdaLogExporter",
"OperationOverride",
"S3Exporter",
"WorkflowInsightConfig",
"WorkflowInsightPlugin",
"build_operations_by_name",
"truncate_record",
"with_operations_by_name",
"workflow_insight",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates.
#
# SPDX-License-Identifier: Apache-2.0
"""First-party Workflow Insight exporters.

One module per exporter, mirroring the JS package's ``src/exporters/`` layout
(``aws-durable-execution-sdk-js-insight``). Each destination lives in its own
module so the set can grow to the full JS parity surface (S3, CloudWatch Logs,
DynamoDB, Firehose, EventBridge, SQS, OpenSearch, Redshift, Aurora, HTTP, OTel,
file, ...) without any single file accreting every backend's imports and
optional dependencies.

Concrete exporters are re-exported here so the public import path is stable:
``from aws_durable_execution_sdk_python_insight.exporters import S3Exporter``
keeps working exactly as before this package was split out of a single module.
Shared serialization helpers live in the private ``_common`` module.

Both shipped exporters serialize the curated record with JS-compatible compact
JSON (no whitespace) so the wire bytes match across SDKs. Records are written
verbatim -- no synthetic emission.
"""

from __future__ import annotations

from aws_durable_execution_sdk_python_insight.exporters.lambda_log_exporter import (
LambdaLogExporter,
)
from aws_durable_execution_sdk_python_insight.exporters.s3_exporter import (
S3Exporter,
)


__all__ = [
"LambdaLogExporter",
"S3Exporter",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates.
#
# SPDX-License-Identifier: Apache-2.0
"""Shared serialization helpers for the Workflow Insight exporters.

Kept private to the ``exporters`` package: every backend needs the same
JS-compatible compact JSON encoding and the same key/file-name sanitizer, so
they live here rather than being duplicated per exporter module.
"""

from __future__ import annotations

import json
import re
from typing import Any


def compact_dumps(value: Any) -> str:
"""Serialize ``value`` as compact JSON (no whitespace, non-ASCII preserved).

Matches the JS exporters' ``JSON.stringify`` output so the wire bytes are
identical across SDKs.
"""
return json.dumps(value, separators=(",", ":"), ensure_ascii=False)


def sanitize(value: str) -> str:
"""Replace characters unsafe for object keys / file names with ``_``."""
return re.sub(r"[^a-zA-Z0-9._-]", "_", value)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates.
#
# SPDX-License-Identifier: Apache-2.0
"""Lambda log (CloudWatch) Workflow Insight exporter."""

from __future__ import annotations

from typing import Any

from aws_durable_execution_sdk_python_insight.exporters._common import compact_dumps
from aws_durable_execution_sdk_python_insight.operations_index import (
with_operations_by_name,
)


class LambdaLogExporter:
"""Writes ``operationsByName`` records to the function's own log group via ``print``.

Port of the JS ``LambdaLogExporter``: ``console.log(JSON.stringify(
withOperationsByName(record)))``. Requires no extra IAM. Emits the name-keyed
summary map (``OPERATIONS_BY_NAME``).
"""

def __init__(self, max_record_size_bytes: int | None = None) -> None:
self.max_record_size_bytes = (
256_000 if max_record_size_bytes is None else max_record_size_bytes
)

def render(self, record: dict[str, Any]) -> dict[str, Any]:
return with_operations_by_name(record)

def export(self, record: dict[str, Any]) -> None:
# Raw JSON line to stdout -> the function's CloudWatch log group. The
# conformance CloudWatch sink json.loads each line (and unwraps the
# Lambda structured-log envelope when present).
print(compact_dumps(self.render(record)), flush=True) # noqa: T201

def flush(self) -> None:
return None
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates.
#
# SPDX-License-Identifier: Apache-2.0
"""S3 Workflow Insight exporter."""

from __future__ import annotations

from typing import Any

from aws_durable_execution_sdk_python_insight.exporters._common import (
compact_dumps,
sanitize,
)


class S3Exporter:
"""Writes canonical ``operations``-array records to S3.

Port of the JS ``S3Exporter``. Each record is a JSON object keyed by
execution name, so updates to the same execution overwrite the same object.
Emits the lossless ``operations`` array (``OPERATIONS_ARRAY``).
"""

def __init__(
self,
bucket: str,
prefix: str = "workflow-insight/",
partitioning: str = "date",
region: str | None = None,
max_record_size_bytes: int | None = None,
client: Any = None,
) -> None:
self.bucket = bucket
self.prefix = prefix
self.partitioning = partitioning
self.max_record_size_bytes = (
5_000_000 if max_record_size_bytes is None else max_record_size_bytes
)
if client is not None:
self._client = client
else:
import boto3 # deferred: boto3 is provided by the Lambda runtime

self._client = (
boto3.client("s3", region_name=region) if region else boto3.client("s3")
)

def render(self, record: dict[str, Any]) -> dict[str, Any]:
return record

def export(self, record: dict[str, Any]) -> None:
key = self._build_key(record)
self._client.put_object(
Bucket=self.bucket,
Key=key,
Body=compact_dumps(record).encode("utf-8"),
ContentType="application/json",
)

def flush(self) -> None:
return None

def _build_key(self, record: dict[str, Any]) -> str:
file_name = (
sanitize(
record.get("executionName") or record.get("executionArn") or "record"
)
+ ".json"
)
return f"{self.prefix}{self._partition(record)}{file_name}"

def _partition(self, record: dict[str, Any]) -> str:
if self.partitioning == "function-name":
return f"function={sanitize(record.get('functionName', ''))}/"
if self.partitioning == "date":
start = str(record.get("startTime", ""))
# YYYY-MM-DD... -> year=YYYY/month=MM/day=DD/
if len(start) >= 10 and start[4] == "-" and start[7] == "-":
return f"year={start[0:4]}/month={start[5:7]}/day={start[8:10]}/"
return ""
return ""
Loading
Loading