aws-durable-execution-sdk-python-telemetry-exporter 0.0.1__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.
Files changed (28) hide show
  1. aws_durable_execution_sdk_python_telemetry_exporter/LICENSE +175 -0
  2. aws_durable_execution_sdk_python_telemetry_exporter/NOTICE +1 -0
  3. aws_durable_execution_sdk_python_telemetry_exporter/__about__.py +4 -0
  4. aws_durable_execution_sdk_python_telemetry_exporter/__init__.py +90 -0
  5. aws_durable_execution_sdk_python_telemetry_exporter/_export_scheduler.py +178 -0
  6. aws_durable_execution_sdk_python_telemetry_exporter/exporters/__init__.py +86 -0
  7. aws_durable_execution_sdk_python_telemetry_exporter/exporters/_common.py +109 -0
  8. aws_durable_execution_sdk_python_telemetry_exporter/exporters/aurora_exporter.py +152 -0
  9. aws_durable_execution_sdk_python_telemetry_exporter/exporters/cloudwatch_logs_exporter.py +96 -0
  10. aws_durable_execution_sdk_python_telemetry_exporter/exporters/dynamodb_exporter.py +75 -0
  11. aws_durable_execution_sdk_python_telemetry_exporter/exporters/eventbridge_exporter.py +82 -0
  12. aws_durable_execution_sdk_python_telemetry_exporter/exporters/file_exporter.py +88 -0
  13. aws_durable_execution_sdk_python_telemetry_exporter/exporters/firehose_exporter.py +63 -0
  14. aws_durable_execution_sdk_python_telemetry_exporter/exporters/http_exporter.py +82 -0
  15. aws_durable_execution_sdk_python_telemetry_exporter/exporters/lambda_log_exporter.py +41 -0
  16. aws_durable_execution_sdk_python_telemetry_exporter/exporters/opensearch_exporter.py +109 -0
  17. aws_durable_execution_sdk_python_telemetry_exporter/exporters/otel_exporter.py +151 -0
  18. aws_durable_execution_sdk_python_telemetry_exporter/exporters/redshift_exporter.py +155 -0
  19. aws_durable_execution_sdk_python_telemetry_exporter/exporters/s3_exporter.py +105 -0
  20. aws_durable_execution_sdk_python_telemetry_exporter/exporters/sqs_exporter.py +96 -0
  21. aws_durable_execution_sdk_python_telemetry_exporter/operations_index.py +132 -0
  22. aws_durable_execution_sdk_python_telemetry_exporter/plugin.py +452 -0
  23. aws_durable_execution_sdk_python_telemetry_exporter/py.typed +0 -0
  24. aws_durable_execution_sdk_python_telemetry_exporter/truncation.py +107 -0
  25. aws_durable_execution_sdk_python_telemetry_exporter/types.py +127 -0
  26. aws_durable_execution_sdk_python_telemetry_exporter-0.0.1.dist-info/METADATA +256 -0
  27. aws_durable_execution_sdk_python_telemetry_exporter-0.0.1.dist-info/RECORD +28 -0
  28. aws_durable_execution_sdk_python_telemetry_exporter-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,152 @@
1
+ # SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """Aurora (RDS Data API) Durable Execution Telemetry exporter."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from enum import StrEnum
9
+ from typing import Any, Literal
10
+
11
+ from aws_durable_execution_sdk_python_telemetry_exporter.exporters._common import (
12
+ compact_dumps,
13
+ sql_identifier,
14
+ )
15
+
16
+
17
+ class AuroraEngine(StrEnum):
18
+ """Database engine; selects the upsert dialect."""
19
+
20
+ POSTGRESQL = "postgresql"
21
+ MYSQL = "mysql"
22
+
23
+
24
+ # Accepted string inputs, kept in lockstep with the enum values above.
25
+ AuroraEngineInput = Literal["postgresql", "mysql"]
26
+
27
+ _COLUMNS = (
28
+ "execution_arn, execution_name, function_name, status, start_time, "
29
+ "end_time, duration_ms, record_json, emitted_at"
30
+ )
31
+
32
+
33
+ def _string_or_null(value: Any) -> dict[str, Any]:
34
+ return {"stringValue": value} if value else {"isNull": True}
35
+
36
+
37
+ class AuroraExporter:
38
+ """Upserts one row per execution through the RDS Data API.
39
+
40
+ Rows are keyed by ``execution_arn``; a later export for the same execution
41
+ overwrites the row. The full record is stored as JSON in ``record_json``.
42
+ The cluster must have the Data API enabled.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ resource_arn: str,
48
+ secret_arn: str,
49
+ database: str,
50
+ engine: AuroraEngine | AuroraEngineInput,
51
+ table: str = "durable_execution_telemetry",
52
+ region: str | None = None,
53
+ max_record_size_bytes: int | None = None,
54
+ client: Any = None,
55
+ ) -> None:
56
+ self.resource_arn = resource_arn
57
+ self.secret_arn = secret_arn
58
+ self.database = database
59
+ self.table = sql_identifier(table)
60
+ self.engine = AuroraEngine(engine)
61
+ self.max_record_size_bytes: int | None = (
62
+ 1_000_000 if max_record_size_bytes is None else max_record_size_bytes
63
+ )
64
+ if client is not None:
65
+ self._client = client
66
+ else:
67
+ import boto3 # deferred: boto3 is provided by the Lambda runtime
68
+
69
+ self._client = (
70
+ boto3.client("rds-data", region_name=region)
71
+ if region
72
+ else boto3.client("rds-data")
73
+ )
74
+
75
+ def render(self, record: dict[str, Any]) -> dict[str, Any]:
76
+ return record
77
+
78
+ def export(self, record: dict[str, Any]) -> None:
79
+ sql = (
80
+ self._postgres_upsert()
81
+ if self.engine == AuroraEngine.POSTGRESQL
82
+ else self._mysql_upsert()
83
+ )
84
+ duration = record.get("durationMs")
85
+ self._client.execute_statement(
86
+ resourceArn=self.resource_arn,
87
+ secretArn=self.secret_arn,
88
+ database=self.database,
89
+ sql=sql,
90
+ parameters=[
91
+ {
92
+ "name": "execution_arn",
93
+ "value": {"stringValue": record["executionArn"]},
94
+ },
95
+ {
96
+ "name": "execution_name",
97
+ "value": _string_or_null(record.get("executionName")),
98
+ },
99
+ {
100
+ "name": "function_name",
101
+ "value": {"stringValue": record["functionName"]},
102
+ },
103
+ {"name": "status", "value": {"stringValue": record["status"]}},
104
+ {"name": "start_time", "value": {"stringValue": record["startTime"]}},
105
+ {"name": "end_time", "value": _string_or_null(record.get("endTime"))},
106
+ {
107
+ "name": "duration_ms",
108
+ "value": {"longValue": duration}
109
+ if duration is not None
110
+ else {"isNull": True},
111
+ },
112
+ {
113
+ "name": "record_json",
114
+ "value": {"stringValue": compact_dumps(record)},
115
+ },
116
+ {"name": "emitted_at", "value": {"stringValue": record["emittedAt"]}},
117
+ ],
118
+ )
119
+
120
+ def flush(self) -> None:
121
+ return None
122
+
123
+ def _postgres_upsert(self) -> str:
124
+ return (
125
+ f"INSERT INTO {self.table}\n"
126
+ f" ({_COLUMNS})\n"
127
+ " VALUES\n"
128
+ " (:execution_arn, :execution_name, :function_name, :status, "
129
+ ":start_time::timestamptz, :end_time::timestamptz, :duration_ms, "
130
+ ":record_json::jsonb, :emitted_at::timestamptz)\n"
131
+ " ON CONFLICT (execution_arn) DO UPDATE SET\n"
132
+ " status = EXCLUDED.status,\n"
133
+ " end_time = EXCLUDED.end_time,\n"
134
+ " duration_ms = EXCLUDED.duration_ms,\n"
135
+ " record_json = EXCLUDED.record_json,\n"
136
+ " emitted_at = EXCLUDED.emitted_at"
137
+ )
138
+
139
+ def _mysql_upsert(self) -> str:
140
+ return (
141
+ f"INSERT INTO {self.table}\n"
142
+ f" ({_COLUMNS})\n"
143
+ " VALUES\n"
144
+ " (:execution_arn, :execution_name, :function_name, :status, "
145
+ ":start_time, :end_time, :duration_ms, :record_json, :emitted_at)\n"
146
+ " ON DUPLICATE KEY UPDATE\n"
147
+ " status = VALUES(status),\n"
148
+ " end_time = VALUES(end_time),\n"
149
+ " duration_ms = VALUES(duration_ms),\n"
150
+ " record_json = VALUES(record_json),\n"
151
+ " emitted_at = VALUES(emitted_at)"
152
+ )
@@ -0,0 +1,96 @@
1
+ # SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """CloudWatch Logs (PutLogEvents) Durable Execution Telemetry exporter."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import datetime
9
+ import time
10
+ from typing import Any
11
+
12
+ from aws_durable_execution_sdk_python_telemetry_exporter.exporters._common import (
13
+ compact_dumps,
14
+ )
15
+ from aws_durable_execution_sdk_python_telemetry_exporter.operations_index import (
16
+ with_operations_by_name,
17
+ )
18
+
19
+
20
+ def _is_already_exists(exc: BaseException) -> bool:
21
+ if type(exc).__name__ == "ResourceAlreadyExistsException":
22
+ return True
23
+ response = getattr(exc, "response", None)
24
+ if isinstance(response, dict):
25
+ code = response.get("Error", {}).get("Code")
26
+ return bool(code == "ResourceAlreadyExistsException")
27
+ return False
28
+
29
+
30
+ class CloudWatchLogsExporter:
31
+ """Writes ``operationsByName`` records to a chosen log group with PutLogEvents.
32
+
33
+ Unlike ``LambdaLogExporter`` this targets any log group. One log stream is
34
+ created per UTC day, named ``{log_stream_prefix}{YYYY}/{MM}/{DD}``.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ log_group_name: str,
40
+ log_stream_prefix: str = "durable-execution-telemetry/",
41
+ region: str | None = None,
42
+ max_record_size_bytes: int | None = None,
43
+ client: Any = None,
44
+ ) -> None:
45
+ self.log_group_name = log_group_name
46
+ self.log_stream_prefix = log_stream_prefix
47
+ self.max_record_size_bytes: int | None = (
48
+ 256_000 if max_record_size_bytes is None else max_record_size_bytes
49
+ )
50
+ self._created_streams: set[str] = set()
51
+ if client is not None:
52
+ self._client = client
53
+ else:
54
+ import boto3 # deferred: boto3 is provided by the Lambda runtime
55
+
56
+ self._client = (
57
+ boto3.client("logs", region_name=region)
58
+ if region
59
+ else boto3.client("logs")
60
+ )
61
+
62
+ def render(self, record: dict[str, Any]) -> dict[str, Any]:
63
+ return with_operations_by_name(record)
64
+
65
+ def export(self, record: dict[str, Any]) -> None:
66
+ stream_name = self._build_stream_name()
67
+ self._ensure_stream(stream_name)
68
+ self._client.put_log_events(
69
+ logGroupName=self.log_group_name,
70
+ logStreamName=stream_name,
71
+ logEvents=[
72
+ {
73
+ "timestamp": int(time.time() * 1000),
74
+ "message": compact_dumps(self.render(record)),
75
+ }
76
+ ],
77
+ )
78
+
79
+ def flush(self) -> None:
80
+ return None
81
+
82
+ def _build_stream_name(self) -> str:
83
+ now = datetime.datetime.now(datetime.UTC)
84
+ return f"{self.log_stream_prefix}{now:%Y/%m/%d}"
85
+
86
+ def _ensure_stream(self, stream_name: str) -> None:
87
+ if stream_name in self._created_streams:
88
+ return
89
+ try:
90
+ self._client.create_log_stream(
91
+ logGroupName=self.log_group_name, logStreamName=stream_name
92
+ )
93
+ except Exception as exc:
94
+ if not _is_already_exists(exc):
95
+ raise
96
+ self._created_streams.add(stream_name)
@@ -0,0 +1,75 @@
1
+ # SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """DynamoDB Durable Execution Telemetry exporter."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from decimal import Decimal
10
+ from typing import Any
11
+
12
+ from aws_durable_execution_sdk_python_telemetry_exporter.exporters._common import (
13
+ compact_dumps,
14
+ )
15
+ from aws_durable_execution_sdk_python_telemetry_exporter.operations_index import (
16
+ with_operations_by_name,
17
+ )
18
+
19
+
20
+ class DynamoDBExporter:
21
+ """Writes ``operationsByName`` records to a DynamoDB table with PutItem.
22
+
23
+ The partition key holds ``executionArn``. With the default sort key
24
+ (``sk`` = ``emittedAt``) every export adds a new item, keeping the full
25
+ history. With ``sort_key=None`` later exports overwrite the item.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ table_name: str,
31
+ partition_key: str = "pk",
32
+ sort_key: str | None = "sk",
33
+ region: str | None = None,
34
+ max_record_size_bytes: int | None = None,
35
+ client: Any = None,
36
+ ) -> None:
37
+ self.table_name = table_name
38
+ self.partition_key = partition_key
39
+ # ``None`` and ``""`` both disable the sort key.
40
+ self.sort_key = sort_key or None
41
+ self.max_record_size_bytes: int | None = (
42
+ 400_000 if max_record_size_bytes is None else max_record_size_bytes
43
+ )
44
+ if client is not None:
45
+ self._client = client
46
+ else:
47
+ import boto3 # deferred: boto3 is provided by the Lambda runtime
48
+
49
+ self._client = (
50
+ boto3.client("dynamodb", region_name=region)
51
+ if region
52
+ else boto3.client("dynamodb")
53
+ )
54
+ from boto3.dynamodb.types import TypeSerializer # deferred, same reason
55
+
56
+ self._serializer = TypeSerializer()
57
+
58
+ def render(self, record: dict[str, Any]) -> dict[str, Any]:
59
+ return with_operations_by_name(record)
60
+
61
+ def export(self, record: dict[str, Any]) -> None:
62
+ item = self.render(record)
63
+ item[self.partition_key] = record["executionArn"]
64
+ if self.sort_key:
65
+ item[self.sort_key] = record["emittedAt"]
66
+ self._client.put_item(TableName=self.table_name, Item=self._marshal(item))
67
+
68
+ def flush(self) -> None:
69
+ return None
70
+
71
+ def _marshal(self, item: dict[str, Any]) -> dict[str, Any]:
72
+ # DynamoDB numbers must be ``Decimal``; a JSON round trip converts every
73
+ # float and leaves the rest of the record untouched.
74
+ plain = json.loads(compact_dumps(item), parse_float=Decimal)
75
+ return {key: self._serializer.serialize(value) for key, value in plain.items()}
@@ -0,0 +1,82 @@
1
+ # SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """EventBridge Durable Execution Telemetry exporter."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from aws_durable_execution_sdk_python_telemetry_exporter.exporters._common import (
11
+ compact_dumps,
12
+ parse_iso_datetime,
13
+ )
14
+ from aws_durable_execution_sdk_python_telemetry_exporter.operations_index import (
15
+ OperationsFormat,
16
+ OperationsFormatInput,
17
+ apply_operations_format,
18
+ )
19
+
20
+
21
+ class EventBridgeExporter:
22
+ """Publishes each record as one EventBridge event with PutEvents.
23
+
24
+ ``Source`` is configurable, ``DetailType`` is the record status
25
+ (``SUCCEEDED``, ``RUNNING``, ``FAILED``), ``Detail`` is the rendered record
26
+ and ``Time`` is the record's ``emittedAt``.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ event_bus_name: str = "default",
32
+ source: str = "software.amazon.lambda.durable.telemetry",
33
+ region: str | None = None,
34
+ operations_format: OperationsFormat | OperationsFormatInput = (
35
+ OperationsFormat.ARRAY
36
+ ),
37
+ max_record_size_bytes: int | None = None,
38
+ client: Any = None,
39
+ ) -> None:
40
+ self.event_bus_name = event_bus_name
41
+ self.source = source
42
+ self.operations_format = OperationsFormat(operations_format)
43
+ self.max_record_size_bytes: int | None = (
44
+ 256_000 if max_record_size_bytes is None else max_record_size_bytes
45
+ )
46
+ if client is not None:
47
+ self._client = client
48
+ else:
49
+ import boto3 # deferred: boto3 is provided by the Lambda runtime
50
+
51
+ self._client = (
52
+ boto3.client("events", region_name=region)
53
+ if region
54
+ else boto3.client("events")
55
+ )
56
+
57
+ def render(self, record: dict[str, Any]) -> dict[str, Any]:
58
+ return apply_operations_format(record, self.operations_format)
59
+
60
+ def export(self, record: dict[str, Any]) -> None:
61
+ result = self._client.put_events(
62
+ Entries=[
63
+ {
64
+ "EventBusName": self.event_bus_name,
65
+ "Source": self.source,
66
+ "DetailType": record["status"],
67
+ "Detail": compact_dumps(self.render(record)),
68
+ "Time": parse_iso_datetime(record["emittedAt"]),
69
+ }
70
+ ]
71
+ )
72
+ if (result or {}).get("FailedEntryCount", 0) > 0:
73
+ entries = result.get("Entries") or [{}]
74
+ entry = entries[0]
75
+ msg = (
76
+ "EventBridge PutEvents failed: "
77
+ f"{entry.get('ErrorCode')} — {entry.get('ErrorMessage')}"
78
+ )
79
+ raise RuntimeError(msg)
80
+
81
+ def flush(self) -> None:
82
+ return None
@@ -0,0 +1,88 @@
1
+ # SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """Filesystem Durable Execution Telemetry exporter."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from enum import StrEnum
10
+ from pathlib import Path
11
+ from typing import Any, Literal
12
+
13
+ from aws_durable_execution_sdk_python_telemetry_exporter.exporters._common import (
14
+ compact_dumps,
15
+ sanitize,
16
+ )
17
+ from aws_durable_execution_sdk_python_telemetry_exporter.operations_index import (
18
+ OperationsFormat,
19
+ OperationsFormatInput,
20
+ apply_operations_format,
21
+ )
22
+
23
+
24
+ class FileMode(StrEnum):
25
+ """File layout."""
26
+
27
+ # append every record to one ``{YYYY-MM-DD}.ndjson`` file per day (default)
28
+ NDJSON = "ndjson"
29
+ # one pretty-printed ``{executionName}.json`` per execution, overwritten
30
+ JSON = "json"
31
+
32
+
33
+ # Accepted string inputs, kept in lockstep with the enum values above.
34
+ FileModeInput = Literal["ndjson", "json"]
35
+
36
+
37
+ class FileExporter:
38
+ """Writes records under a directory (EFS mount, a mounted share, or ``/tmp``).
39
+
40
+ ``ndjson`` appends one compact line per record to ``{directory}/{date}.ndjson``
41
+ where the date comes from ``emittedAt``. ``json`` writes
42
+ ``{directory}/{executionName}.json`` and overwrites it on every export.
43
+ ``max_record_size_bytes`` has no default.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ directory: str | Path,
49
+ mode: FileMode | FileModeInput = FileMode.NDJSON,
50
+ operations_format: OperationsFormat | OperationsFormatInput = (
51
+ OperationsFormat.ARRAY
52
+ ),
53
+ max_record_size_bytes: int | None = None,
54
+ ) -> None:
55
+ self.directory = Path(directory)
56
+ self.mode = FileMode(mode)
57
+ self.operations_format = OperationsFormat(operations_format)
58
+ self.max_record_size_bytes = max_record_size_bytes
59
+ self._dir_created = False
60
+
61
+ def render(self, record: dict[str, Any]) -> dict[str, Any]:
62
+ return apply_operations_format(record, self.operations_format)
63
+
64
+ def export(self, record: dict[str, Any]) -> None:
65
+ self._ensure_dir()
66
+ formatted = self.render(record)
67
+ if self.mode == FileMode.NDJSON:
68
+ date = str(record["emittedAt"])[:10] # YYYY-MM-DD
69
+ path = self.directory / f"{date}.ndjson"
70
+ with path.open("a", encoding="utf-8") as handle:
71
+ handle.write(compact_dumps(formatted) + "\n")
72
+ else:
73
+ file_name = (
74
+ sanitize(record.get("executionName") or record["executionArn"])
75
+ + ".json"
76
+ )
77
+ (self.directory / file_name).write_text(
78
+ json.dumps(formatted, indent=2, ensure_ascii=False), encoding="utf-8"
79
+ )
80
+
81
+ def flush(self) -> None:
82
+ return None
83
+
84
+ def _ensure_dir(self) -> None:
85
+ if self._dir_created:
86
+ return
87
+ self.directory.mkdir(parents=True, exist_ok=True)
88
+ self._dir_created = True
@@ -0,0 +1,63 @@
1
+ # SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """Kinesis Data Firehose Durable Execution Telemetry exporter."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from aws_durable_execution_sdk_python_telemetry_exporter.exporters._common import (
11
+ compact_dumps,
12
+ )
13
+ from aws_durable_execution_sdk_python_telemetry_exporter.operations_index import (
14
+ OperationsFormat,
15
+ OperationsFormatInput,
16
+ apply_operations_format,
17
+ )
18
+
19
+
20
+ class FirehoseExporter:
21
+ """Sends each record to a Firehose delivery stream with PutRecord.
22
+
23
+ The data is one JSON line with a trailing newline, so records that Firehose
24
+ concatenates into a single object stay parseable as NDJSON.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ delivery_stream_name: str,
30
+ region: str | None = None,
31
+ operations_format: OperationsFormat | OperationsFormatInput = (
32
+ OperationsFormat.ARRAY
33
+ ),
34
+ max_record_size_bytes: int | None = None,
35
+ client: Any = None,
36
+ ) -> None:
37
+ self.delivery_stream_name = delivery_stream_name
38
+ self.operations_format = OperationsFormat(operations_format)
39
+ self.max_record_size_bytes: int | None = (
40
+ 1_000_000 if max_record_size_bytes is None else max_record_size_bytes
41
+ )
42
+ if client is not None:
43
+ self._client = client
44
+ else:
45
+ import boto3 # deferred: boto3 is provided by the Lambda runtime
46
+
47
+ self._client = (
48
+ boto3.client("firehose", region_name=region)
49
+ if region
50
+ else boto3.client("firehose")
51
+ )
52
+
53
+ def render(self, record: dict[str, Any]) -> dict[str, Any]:
54
+ return apply_operations_format(record, self.operations_format)
55
+
56
+ def export(self, record: dict[str, Any]) -> None:
57
+ data = (compact_dumps(self.render(record)) + "\n").encode("utf-8")
58
+ self._client.put_record(
59
+ DeliveryStreamName=self.delivery_stream_name, Record={"Data": data}
60
+ )
61
+
62
+ def flush(self) -> None:
63
+ return None
@@ -0,0 +1,82 @@
1
+ # SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """HTTP / webhook Durable Execution Telemetry exporter."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from enum import StrEnum
9
+ from typing import Any, Literal
10
+
11
+ from aws_durable_execution_sdk_python_telemetry_exporter.exporters._common import (
12
+ compact_dumps,
13
+ http_send,
14
+ )
15
+ from aws_durable_execution_sdk_python_telemetry_exporter.operations_index import (
16
+ OperationsFormat,
17
+ OperationsFormatInput,
18
+ apply_operations_format,
19
+ )
20
+
21
+
22
+ class HttpMethod(StrEnum):
23
+ """Request method. ``PUT`` suits endpoints that upsert by URL path."""
24
+
25
+ POST = "POST"
26
+ PUT = "PUT"
27
+
28
+
29
+ # Accepted string inputs, kept in lockstep with the enum values above.
30
+ HttpMethodInput = Literal["POST", "PUT"]
31
+
32
+
33
+ class HttpExporter:
34
+ """Sends each record as a JSON body to any HTTP endpoint.
35
+
36
+ The endpoint must answer 2xx; any other status raises. ``timeout_ms``
37
+ (default 10 seconds) is currently applied to each blocking socket
38
+ operation, not to the request as a whole: the connect, each write while
39
+ sending the request (headers and the record body), and each read of the
40
+ status line and headers, plus of the error body on a non-2xx response. A
41
+ successful response body is never read. An endpoint that stops reading or
42
+ goes silent fails at ``timeout_ms``, but one that keeps consuming or
43
+ sending bytes slowly can hold the request open for longer. A future
44
+ release may enforce ``timeout_ms`` as a deadline for the whole request, so
45
+ do not rely on a request being allowed to exceed it. Size the function
46
+ timeout with this in mind. ``max_record_size_bytes`` has no default because
47
+ a generic endpoint has no known limit.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ url: str,
53
+ headers: dict[str, str] | None = None,
54
+ method: HttpMethod | HttpMethodInput = HttpMethod.POST,
55
+ timeout_ms: int = 10_000,
56
+ operations_format: OperationsFormat | OperationsFormatInput = (
57
+ OperationsFormat.ARRAY
58
+ ),
59
+ max_record_size_bytes: int | None = None,
60
+ ) -> None:
61
+ self.url = url
62
+ self.method = HttpMethod(method)
63
+ self.headers = dict(headers or {})
64
+ self.timeout_ms = timeout_ms
65
+ self.operations_format = OperationsFormat(operations_format)
66
+ self.max_record_size_bytes = max_record_size_bytes
67
+
68
+ def render(self, record: dict[str, Any]) -> dict[str, Any]:
69
+ return apply_operations_format(record, self.operations_format)
70
+
71
+ def export(self, record: dict[str, Any]) -> None:
72
+ body = compact_dumps(self.render(record)).encode("utf-8")
73
+ headers = {"Content-Type": "application/json", **self.headers}
74
+ status, reason, _ = http_send(
75
+ self.method.value, self.url, headers, body, timeout=self.timeout_ms / 1000
76
+ )
77
+ if not 200 <= status < 300:
78
+ msg = f"HttpExporter: endpoint returned {status} {reason}"
79
+ raise RuntimeError(msg)
80
+
81
+ def flush(self) -> None:
82
+ return None
@@ -0,0 +1,41 @@
1
+ # SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """Lambda log (CloudWatch) Durable Execution Telemetry exporter."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from aws_durable_execution_sdk_python_telemetry_exporter.exporters._common import (
11
+ compact_dumps,
12
+ )
13
+ from aws_durable_execution_sdk_python_telemetry_exporter.operations_index import (
14
+ with_operations_by_name,
15
+ )
16
+
17
+
18
+ class LambdaLogExporter:
19
+ """Writes ``operationsByName`` records to the function's own log group via ``print``.
20
+
21
+ Port of the JS ``LambdaLogExporter``: ``console.log(JSON.stringify(
22
+ withOperationsByName(record)))``. Requires no extra IAM. Emits the name-keyed
23
+ summary map (``OPERATIONS_BY_NAME``).
24
+ """
25
+
26
+ def __init__(self, max_record_size_bytes: int | None = None) -> None:
27
+ self.max_record_size_bytes: int | None = (
28
+ 256_000 if max_record_size_bytes is None else max_record_size_bytes
29
+ )
30
+
31
+ def render(self, record: dict[str, Any]) -> dict[str, Any]:
32
+ return with_operations_by_name(record)
33
+
34
+ def export(self, record: dict[str, Any]) -> None:
35
+ # Raw JSON line to stdout -> the function's CloudWatch log group. The
36
+ # conformance CloudWatch sink json.loads each line (and unwraps the
37
+ # Lambda structured-log envelope when present).
38
+ print(compact_dumps(self.render(record)), flush=True) # noqa: T201
39
+
40
+ def flush(self) -> None:
41
+ return None