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.
- aws_durable_execution_sdk_python_telemetry_exporter/LICENSE +175 -0
- aws_durable_execution_sdk_python_telemetry_exporter/NOTICE +1 -0
- aws_durable_execution_sdk_python_telemetry_exporter/__about__.py +4 -0
- aws_durable_execution_sdk_python_telemetry_exporter/__init__.py +90 -0
- aws_durable_execution_sdk_python_telemetry_exporter/_export_scheduler.py +178 -0
- aws_durable_execution_sdk_python_telemetry_exporter/exporters/__init__.py +86 -0
- aws_durable_execution_sdk_python_telemetry_exporter/exporters/_common.py +109 -0
- aws_durable_execution_sdk_python_telemetry_exporter/exporters/aurora_exporter.py +152 -0
- aws_durable_execution_sdk_python_telemetry_exporter/exporters/cloudwatch_logs_exporter.py +96 -0
- aws_durable_execution_sdk_python_telemetry_exporter/exporters/dynamodb_exporter.py +75 -0
- aws_durable_execution_sdk_python_telemetry_exporter/exporters/eventbridge_exporter.py +82 -0
- aws_durable_execution_sdk_python_telemetry_exporter/exporters/file_exporter.py +88 -0
- aws_durable_execution_sdk_python_telemetry_exporter/exporters/firehose_exporter.py +63 -0
- aws_durable_execution_sdk_python_telemetry_exporter/exporters/http_exporter.py +82 -0
- aws_durable_execution_sdk_python_telemetry_exporter/exporters/lambda_log_exporter.py +41 -0
- aws_durable_execution_sdk_python_telemetry_exporter/exporters/opensearch_exporter.py +109 -0
- aws_durable_execution_sdk_python_telemetry_exporter/exporters/otel_exporter.py +151 -0
- aws_durable_execution_sdk_python_telemetry_exporter/exporters/redshift_exporter.py +155 -0
- aws_durable_execution_sdk_python_telemetry_exporter/exporters/s3_exporter.py +105 -0
- aws_durable_execution_sdk_python_telemetry_exporter/exporters/sqs_exporter.py +96 -0
- aws_durable_execution_sdk_python_telemetry_exporter/operations_index.py +132 -0
- aws_durable_execution_sdk_python_telemetry_exporter/plugin.py +452 -0
- aws_durable_execution_sdk_python_telemetry_exporter/py.typed +0 -0
- aws_durable_execution_sdk_python_telemetry_exporter/truncation.py +107 -0
- aws_durable_execution_sdk_python_telemetry_exporter/types.py +127 -0
- aws_durable_execution_sdk_python_telemetry_exporter-0.0.1.dist-info/METADATA +256 -0
- aws_durable_execution_sdk_python_telemetry_exporter-0.0.1.dist-info/RECORD +28 -0
- 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
|