aws-durable-execution-sdk-python-otel 0.1.0__tar.gz → 0.2.0__tar.gz
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_otel-0.1.0 → aws_durable_execution_sdk_python_otel-0.2.0}/PKG-INFO +40 -3
- aws_durable_execution_sdk_python_otel-0.2.0/README.md +41 -0
- {aws_durable_execution_sdk_python_otel-0.1.0 → aws_durable_execution_sdk_python_otel-0.2.0}/pyproject.toml +51 -2
- {aws_durable_execution_sdk_python_otel-0.1.0 → aws_durable_execution_sdk_python_otel-0.2.0}/src/aws_durable_execution_sdk_python_otel/__about__.py +1 -1
- aws_durable_execution_sdk_python_otel-0.2.0/src/aws_durable_execution_sdk_python_otel/__init__.py +30 -0
- aws_durable_execution_sdk_python_otel-0.2.0/src/aws_durable_execution_sdk_python_otel/context_extractors.py +40 -0
- aws_durable_execution_sdk_python_otel-0.2.0/src/aws_durable_execution_sdk_python_otel/deterministic_id_generator.py +126 -0
- aws_durable_execution_sdk_python_otel-0.2.0/src/aws_durable_execution_sdk_python_otel/log_filter.py +105 -0
- aws_durable_execution_sdk_python_otel-0.2.0/src/aws_durable_execution_sdk_python_otel/plugin.py +488 -0
- aws_durable_execution_sdk_python_otel-0.2.0/src/aws_durable_execution_sdk_python_otel/py.typed +0 -0
- aws_durable_execution_sdk_python_otel-0.2.0/tests/__init__.py +0 -0
- aws_durable_execution_sdk_python_otel-0.2.0/tests/test_context_extractors.py +66 -0
- aws_durable_execution_sdk_python_otel-0.2.0/tests/test_deterministic_id_generator.py +282 -0
- aws_durable_execution_sdk_python_otel-0.2.0/tests/test_init.py +18 -0
- aws_durable_execution_sdk_python_otel-0.2.0/tests/test_log_filter.py +230 -0
- aws_durable_execution_sdk_python_otel-0.2.0/tests/test_plugin.py +493 -0
- aws_durable_execution_sdk_python_otel-0.1.0/README.md +0 -9
- aws_durable_execution_sdk_python_otel-0.1.0/src/aws_durable_execution_sdk_python_otel/__init__.py +0 -6
- {aws_durable_execution_sdk_python_otel-0.1.0 → aws_durable_execution_sdk_python_otel-0.2.0}/.gitignore +0 -0
- {aws_durable_execution_sdk_python_otel-0.1.0 → aws_durable_execution_sdk_python_otel-0.2.0}/LICENSE +0 -0
- {aws_durable_execution_sdk_python_otel-0.1.0 → aws_durable_execution_sdk_python_otel-0.2.0}/NOTICE +0 -0
{aws_durable_execution_sdk_python_otel-0.1.0 → aws_durable_execution_sdk_python_otel-0.2.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-durable-execution-sdk-python-otel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: OpenTelemetry instrumentation for AWS Durable Execution SDK for Python
|
|
5
5
|
Project-URL: Documentation, https://github.com/aws/aws-durable-execution-sdk-python#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/aws/aws-durable-execution-sdk-python/issues
|
|
@@ -8,7 +8,7 @@ Project-URL: Source, https://github.com/aws/aws-durable-execution-sdk-python
|
|
|
8
8
|
Author-email: AWS durable-execution-dev <durable-execution-dev@amazon.com>
|
|
9
9
|
License-Expression: Apache-2.0
|
|
10
10
|
Keywords: durable-execution,observability,opentelemetry,tracing
|
|
11
|
-
Classifier: Development Status ::
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Programming Language :: Python
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -17,13 +17,50 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
17
17
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
18
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: aws-durable-execution-sdk-python>=1.5.0
|
|
21
|
+
Requires-Dist: opentelemetry-api>=1.20.0
|
|
22
|
+
Requires-Dist: opentelemetry-exporter-otlp
|
|
23
|
+
Requires-Dist: opentelemetry-propagator-aws-xray
|
|
24
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0
|
|
20
25
|
Description-Content-Type: text/markdown
|
|
21
26
|
|
|
22
27
|
# aws-durable-execution-sdk-python-otel
|
|
23
28
|
|
|
24
29
|
OpenTelemetry instrumentation for the [AWS Durable Execution SDK for Python](https://github.com/aws/aws-durable-execution-sdk-python).
|
|
25
30
|
|
|
26
|
-
> **Note:**
|
|
31
|
+
> **Note:** v0.1.0 reserves the package name. Instrumentation lands in v0.2.0.
|
|
32
|
+
|
|
33
|
+
## Overview
|
|
34
|
+
|
|
35
|
+
This package will provide automatic OpenTelemetry tracing for durable execution workflows, giving you visibility into step execution, waits, retries, and overall workflow performance.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install aws-durable-execution-sdk-python-otel
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from aws_durable_execution_sdk_python_otel import __version__
|
|
47
|
+
|
|
48
|
+
print(__version__)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Planned Features (v0.2.0)
|
|
52
|
+
|
|
53
|
+
- Automatic span creation for steps, waits, invokes, and child contexts
|
|
54
|
+
- Replay-aware tracing (distinguishes fresh executions from replays)
|
|
55
|
+
- Error recording with proper OTel status codes
|
|
56
|
+
- Configurable span attributes and naming
|
|
57
|
+
|
|
58
|
+
## Requirements
|
|
59
|
+
|
|
60
|
+
- Python >= 3.11
|
|
61
|
+
- `aws-durable-execution-sdk-python` >= 1.5.0
|
|
62
|
+
- `opentelemetry-api` >= 1.20.0
|
|
63
|
+
- `opentelemetry-sdk` >= 1.20.0
|
|
27
64
|
|
|
28
65
|
## License
|
|
29
66
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# aws-durable-execution-sdk-python-otel
|
|
2
|
+
|
|
3
|
+
OpenTelemetry instrumentation for the [AWS Durable Execution SDK for Python](https://github.com/aws/aws-durable-execution-sdk-python).
|
|
4
|
+
|
|
5
|
+
> **Note:** v0.1.0 reserves the package name. Instrumentation lands in v0.2.0.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
This package will provide automatic OpenTelemetry tracing for durable execution workflows, giving you visibility into step execution, waits, retries, and overall workflow performance.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install aws-durable-execution-sdk-python-otel
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from aws_durable_execution_sdk_python_otel import __version__
|
|
21
|
+
|
|
22
|
+
print(__version__)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Planned Features (v0.2.0)
|
|
26
|
+
|
|
27
|
+
- Automatic span creation for steps, waits, invokes, and child contexts
|
|
28
|
+
- Replay-aware tracing (distinguishes fresh executions from replays)
|
|
29
|
+
- Error recording with proper OTel status codes
|
|
30
|
+
- Configurable span attributes and naming
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Python >= 3.11
|
|
35
|
+
- `aws-durable-execution-sdk-python` >= 1.5.0
|
|
36
|
+
- `opentelemetry-api` >= 1.20.0
|
|
37
|
+
- `opentelemetry-sdk` >= 1.20.0
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
Apache-2.0
|
|
@@ -12,7 +12,7 @@ license = "Apache-2.0"
|
|
|
12
12
|
keywords = ["opentelemetry", "tracing", "observability", "durable-execution"]
|
|
13
13
|
authors = [{ name = "AWS durable-execution-dev", email = "durable-execution-dev@amazon.com" }]
|
|
14
14
|
classifiers = [
|
|
15
|
-
"Development Status ::
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
16
|
"Programming Language :: Python",
|
|
17
17
|
"Programming Language :: Python :: 3.11",
|
|
18
18
|
"Programming Language :: Python :: 3.12",
|
|
@@ -21,7 +21,13 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
22
22
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
23
23
|
]
|
|
24
|
-
dependencies = [
|
|
24
|
+
dependencies = [
|
|
25
|
+
"aws-durable-execution-sdk-python>=1.5.0",
|
|
26
|
+
"opentelemetry-api>=1.20.0",
|
|
27
|
+
"opentelemetry-sdk>=1.20.0",
|
|
28
|
+
"opentelemetry-exporter-otlp",
|
|
29
|
+
"opentelemetry-propagator-aws-xray",
|
|
30
|
+
]
|
|
25
31
|
|
|
26
32
|
[project.urls]
|
|
27
33
|
Documentation = "https://github.com/aws/aws-durable-execution-sdk-python#readme"
|
|
@@ -41,3 +47,46 @@ packages = ["src/aws_durable_execution_sdk_python_otel"]
|
|
|
41
47
|
|
|
42
48
|
[tool.hatch.version]
|
|
43
49
|
path = "src/aws_durable_execution_sdk_python_otel/__about__.py"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.publish.index]
|
|
52
|
+
disable = true
|
|
53
|
+
|
|
54
|
+
[tool.coverage.run]
|
|
55
|
+
source_pkgs = ["aws_durable_execution_sdk_python_otel"]
|
|
56
|
+
branch = true
|
|
57
|
+
parallel = true
|
|
58
|
+
omit = ["src/aws_durable_execution_sdk_python_otel/__about__.py"]
|
|
59
|
+
|
|
60
|
+
[tool.coverage.paths]
|
|
61
|
+
aws_durable_execution_sdk_python_otel = [
|
|
62
|
+
"src/aws_durable_execution_sdk_python_otel",
|
|
63
|
+
"*/aws-durable-execution-sdk-python-otel/src/aws_durable_execution_sdk_python_otel",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[tool.coverage.report]
|
|
67
|
+
exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
|
|
68
|
+
|
|
69
|
+
[tool.ruff]
|
|
70
|
+
line-length = 88
|
|
71
|
+
target-version = "py311"
|
|
72
|
+
|
|
73
|
+
[tool.ruff.lint]
|
|
74
|
+
preview = true
|
|
75
|
+
select = ["TID252"]
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint.isort]
|
|
78
|
+
known-first-party = ["aws_durable_execution_sdk_python_otel"]
|
|
79
|
+
force-single-line = false
|
|
80
|
+
lines-after-imports = 2
|
|
81
|
+
|
|
82
|
+
[tool.ruff.lint.per-file-ignores]
|
|
83
|
+
"tests/**" = [
|
|
84
|
+
"ARG001",
|
|
85
|
+
"ARG002",
|
|
86
|
+
"ARG005",
|
|
87
|
+
"S101",
|
|
88
|
+
"PLR2004",
|
|
89
|
+
"PLR6301",
|
|
90
|
+
"SIM117",
|
|
91
|
+
"TRY301",
|
|
92
|
+
]
|
aws_durable_execution_sdk_python_otel-0.2.0/src/aws_durable_execution_sdk_python_otel/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""OpenTelemetry instrumentation for AWS Lambda Durable Executions Python SDK."""
|
|
2
|
+
|
|
3
|
+
from aws_durable_execution_sdk_python_otel.__about__ import __version__
|
|
4
|
+
from aws_durable_execution_sdk_python_otel.context_extractors import (
|
|
5
|
+
ContextExtractor,
|
|
6
|
+
w3c_client_context_extractor,
|
|
7
|
+
xray_context_extractor,
|
|
8
|
+
)
|
|
9
|
+
from aws_durable_execution_sdk_python_otel.deterministic_id_generator import (
|
|
10
|
+
DeterministicIdGenerator,
|
|
11
|
+
)
|
|
12
|
+
from aws_durable_execution_sdk_python_otel.log_filter import (
|
|
13
|
+
OtelContextLogFilter,
|
|
14
|
+
install_log_filter,
|
|
15
|
+
)
|
|
16
|
+
from aws_durable_execution_sdk_python_otel.plugin import (
|
|
17
|
+
DurableExecutionOtelPlugin,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"__version__",
|
|
23
|
+
"ContextExtractor",
|
|
24
|
+
"DeterministicIdGenerator",
|
|
25
|
+
"DurableExecutionOtelPlugin",
|
|
26
|
+
"OtelContextLogFilter",
|
|
27
|
+
"install_log_filter",
|
|
28
|
+
"w3c_client_context_extractor",
|
|
29
|
+
"xray_context_extractor",
|
|
30
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Context extractors for propagating trace context into durable executions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import TYPE_CHECKING, Callable
|
|
7
|
+
|
|
8
|
+
from opentelemetry import context as otel_context, propagate
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from opentelemetry.context import Context
|
|
13
|
+
|
|
14
|
+
from aws_durable_execution_sdk_python.plugin import InvocationStartInfo
|
|
15
|
+
|
|
16
|
+
ContextExtractor = Callable[["InvocationStartInfo"], "Context"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def xray_context_extractor(info: "InvocationStartInfo") -> "Context":
|
|
20
|
+
"""Read the X-Ray trace header from the _X_AMZN_TRACE_ID environment variable.
|
|
21
|
+
|
|
22
|
+
The durable execution backend propagates the same Root trace ID to every
|
|
23
|
+
invocation, so all invocations share one traceId.
|
|
24
|
+
"""
|
|
25
|
+
trace_header = os.environ.get("_X_AMZN_TRACE_ID")
|
|
26
|
+
if not trace_header:
|
|
27
|
+
return otel_context.get_current()
|
|
28
|
+
return propagate.extract(
|
|
29
|
+
carrier={"X-Amzn-Trace-Id": trace_header},
|
|
30
|
+
context=otel_context.get_current(),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def w3c_client_context_extractor(info: "InvocationStartInfo") -> "Context":
|
|
35
|
+
"""Read W3C traceparent from context.clientContext.custom.traceparent.
|
|
36
|
+
|
|
37
|
+
Requires the backend clientContext propagation to be enabled.
|
|
38
|
+
This extractor is a placeholder for when backend propagation is supported.
|
|
39
|
+
"""
|
|
40
|
+
return otel_context.get_current()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Deterministic ID generator for OpenTelemetry spans in durable executions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextvars
|
|
6
|
+
import hashlib
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
|
|
11
|
+
from opentelemetry.sdk.trace import IdGenerator, RandomIdGenerator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
HASHED_ID_PATTERN = re.compile(r"^[0-9a-f]{16}$")
|
|
15
|
+
|
|
16
|
+
# Scoping the pending span ID to the execution context ensures concurrent
|
|
17
|
+
# operations cannot consume each other's deterministic span ID.
|
|
18
|
+
_next_span_id: contextvars.ContextVar[int | None] = contextvars.ContextVar(
|
|
19
|
+
"next_span_id", default=None
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _parse_xray_root_trace_id(trace_header: str | None) -> str | None:
|
|
24
|
+
"""Parse the Root trace ID from an X-Ray trace header string.
|
|
25
|
+
|
|
26
|
+
The header format is:
|
|
27
|
+
Root=1-<8 hex>-<24 hex>;Parent=<16 hex>;Sampled=0|1
|
|
28
|
+
|
|
29
|
+
Returns the root value (e.g. "1-5759e988-bd862e3fe1be46a994272793")
|
|
30
|
+
or None if the header is missing or malformed.
|
|
31
|
+
"""
|
|
32
|
+
if not trace_header:
|
|
33
|
+
return None
|
|
34
|
+
match = re.search(r"Root=(1-[0-9a-fA-F]{8}-[0-9a-fA-F]{24})", trace_header)
|
|
35
|
+
return match.group(1) if match else None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _xray_trace_id_to_otel(xray_trace_id: str) -> int:
|
|
39
|
+
"""Convert an X-Ray trace ID to the W3C/OpenTelemetry 32-char hex format.
|
|
40
|
+
|
|
41
|
+
X-Ray format: "1-<8hex>-<24hex>" (36 chars with prefix and dashes)
|
|
42
|
+
OTel format: "<8hex><24hex>" (32 lowercase hex chars)
|
|
43
|
+
"""
|
|
44
|
+
otel_id = xray_trace_id.replace("1-", "", 1).replace("-", "").lower()
|
|
45
|
+
return int(otel_id, 16)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _to_otel_trace_id(execution_arn: str, start_timestamp: datetime | None) -> int:
|
|
49
|
+
"""Build an OTel-compatible trace ID (128 bits)
|
|
50
|
+
|
|
51
|
+
First attempts to read the trace ID from the _X_AMZN_TRACE_ID environment
|
|
52
|
+
variable that Lambda populates on each invocation. This ties the durable
|
|
53
|
+
execution spans to the same trace that X-Ray is already tracking.
|
|
54
|
+
|
|
55
|
+
Falls back to generating a deterministic trace ID from the execution ARN
|
|
56
|
+
and timestamp when the environment variable is not set (e.g. in tests or
|
|
57
|
+
non-Lambda environments).
|
|
58
|
+
"""
|
|
59
|
+
env_trace_id = _parse_xray_root_trace_id(os.environ.get("_X_AMZN_TRACE_ID"))
|
|
60
|
+
if env_trace_id:
|
|
61
|
+
return _xray_trace_id_to_otel(env_trace_id)
|
|
62
|
+
|
|
63
|
+
# Fallback: deterministic ID from execution ARN + timestamp
|
|
64
|
+
time_part = format(int((start_timestamp or datetime.now(UTC)).timestamp()), "08x")
|
|
65
|
+
hash_part = hashlib.blake2b(execution_arn.encode()).hexdigest()[:24] # noqa: S324
|
|
66
|
+
return int(f"{time_part}{hash_part}", 16)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def operation_id_to_span_id(operation_id: str) -> int:
|
|
70
|
+
"""Derive a deterministic span ID (64 bits) from an operation ID."""
|
|
71
|
+
hashed_operation_id = hashlib.blake2b(operation_id.encode()).hexdigest()[:16]
|
|
72
|
+
return int(hashed_operation_id, 16)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class DeterministicIdGenerator(RandomIdGenerator):
|
|
76
|
+
"""An ID generator that produces deterministic span IDs when a pending
|
|
77
|
+
operation ID is set, and falls back to the provided generator otherwise.
|
|
78
|
+
|
|
79
|
+
Trace IDs are deterministic when an execution ARN is set, ensuring all
|
|
80
|
+
invocations of the same durable execution share a single trace. When no
|
|
81
|
+
deterministic ID is available, generation is delegated to the fallback
|
|
82
|
+
generator (the tracer provider's original ID generator by default).
|
|
83
|
+
|
|
84
|
+
Trace IDs embed a real timestamp so they satisfy the X-Ray format
|
|
85
|
+
requirement (first 8 hex chars = Unix epoch seconds).
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
fallback_id_generator: Generator used when no deterministic ID is
|
|
89
|
+
available. Defaults to a new ``RandomIdGenerator``.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, fallback_id_generator: IdGenerator | None = None) -> None:
|
|
93
|
+
self._execution_trace_id: int | None = None
|
|
94
|
+
self._fallback_id_generator = fallback_id_generator or RandomIdGenerator()
|
|
95
|
+
|
|
96
|
+
def set_next_span_id(self, span_id: int | None) -> None:
|
|
97
|
+
"""Set the operation ID to use for the next span's ID.
|
|
98
|
+
|
|
99
|
+
After one span is created, it resets to random.
|
|
100
|
+
"""
|
|
101
|
+
_next_span_id.set(span_id)
|
|
102
|
+
|
|
103
|
+
def set_trace_id(
|
|
104
|
+
self, execution_arn: str, start_timestamp: datetime | None
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Compute and cache the deterministic trace ID for this execution.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
execution_arn: The durable execution ARN (used for the hash portion).
|
|
110
|
+
start_timestamp: start time of invocation
|
|
111
|
+
"""
|
|
112
|
+
self._execution_trace_id = _to_otel_trace_id(execution_arn, start_timestamp)
|
|
113
|
+
|
|
114
|
+
def generate_trace_id(self) -> int:
|
|
115
|
+
"""Generate a 128-bit trace ID."""
|
|
116
|
+
return (
|
|
117
|
+
self._execution_trace_id or self._fallback_id_generator.generate_trace_id()
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def generate_span_id(self) -> int:
|
|
121
|
+
"""Generate a 64-bit span ID."""
|
|
122
|
+
span_id = _next_span_id.get()
|
|
123
|
+
# Consume once: the deterministic ID applies only to the next span
|
|
124
|
+
# created in this context; subsequent spans fall back to random.
|
|
125
|
+
_next_span_id.set(None)
|
|
126
|
+
return span_id or self._fallback_id_generator.generate_span_id()
|
aws_durable_execution_sdk_python_otel-0.2.0/src/aws_durable_execution_sdk_python_otel/log_filter.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Root-logger filter that stamps OTel trace context onto every log record.
|
|
2
|
+
|
|
3
|
+
The filter attaches to a stdlib logging handler and enriches *every* record
|
|
4
|
+
that flows through it: direct ``logging.getLogger().info(...)`` calls,
|
|
5
|
+
child-logger records that propagate to root, and third-party library logs.
|
|
6
|
+
|
|
7
|
+
The span/trace identifiers are added as ``LogRecord`` attributes (using
|
|
8
|
+
underscore names, since dotted names are not valid record attributes):
|
|
9
|
+
|
|
10
|
+
- ``otel_trace_id``: 32-char hex trace identifier
|
|
11
|
+
- ``otel_span_id``: 16-char hex span identifier
|
|
12
|
+
- ``otel_trace_sampled``: boolean indicating if the trace is sampled
|
|
13
|
+
|
|
14
|
+
These attributes are only set when a valid span context is active. Records
|
|
15
|
+
emitted outside an active invocation (e.g. during Lambda teardown) pass through
|
|
16
|
+
unmodified, so any log formatter or schema must treat the fields as optional.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
from opentelemetry.trace import TraceFlags
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from aws_durable_execution_sdk_python_otel.plugin import DurableExecutionOtelPlugin
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OtelContextLogFilter(logging.Filter):
|
|
32
|
+
"""Logging filter that injects the active OTel span context onto records.
|
|
33
|
+
|
|
34
|
+
The filter is a pure reader of the plugin's current span context. It
|
|
35
|
+
resolves the span at emit time, on the thread that emits the record, via
|
|
36
|
+
``plugin.get_current_span_context()``. That method returns the active
|
|
37
|
+
operation span inside steps and child contexts (attached to the worker
|
|
38
|
+
thread's OTel context) and falls back to the invocation span for top-level
|
|
39
|
+
handler code.
|
|
40
|
+
|
|
41
|
+
The filter never caches identifiers and always returns ``True`` so it never
|
|
42
|
+
drops a record.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
plugin: The OTel plugin instance that resolves the current span context.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, plugin: DurableExecutionOtelPlugin) -> None:
|
|
49
|
+
super().__init__()
|
|
50
|
+
self._plugin = plugin
|
|
51
|
+
|
|
52
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
53
|
+
"""Stamp the active span context onto the record, then allow it through."""
|
|
54
|
+
span_context = self._plugin.get_current_span_context()
|
|
55
|
+
if span_context and span_context.is_valid:
|
|
56
|
+
record.otel_trace_id = format(span_context.trace_id, "032x")
|
|
57
|
+
record.otel_span_id = format(span_context.span_id, "016x")
|
|
58
|
+
record.otel_trace_sampled = bool(
|
|
59
|
+
span_context.trace_flags & TraceFlags.SAMPLED
|
|
60
|
+
)
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def install_log_filter(
|
|
65
|
+
plugin: DurableExecutionOtelPlugin,
|
|
66
|
+
target_logger: logging.Logger | None = None,
|
|
67
|
+
) -> OtelContextLogFilter | None:
|
|
68
|
+
"""Attach an OtelContextLogFilter to a logger's handlers, idempotently.
|
|
69
|
+
|
|
70
|
+
The filter is attached to each handler on ``target_logger`` (the root logger
|
|
71
|
+
by default). Attaching to handlers rather than the logger itself ensures
|
|
72
|
+
records propagated from child loggers are also enriched, since handler
|
|
73
|
+
filters run for every record reaching the handler.
|
|
74
|
+
|
|
75
|
+
This is safe to call on every invocation: if a handler already has an
|
|
76
|
+
OtelContextLogFilter, it is left as-is, so warm Lambda reuse will not stack
|
|
77
|
+
duplicate filters. A single shared filter instance is reused across all
|
|
78
|
+
handlers.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
plugin: The OTel plugin that resolves the current span context.
|
|
82
|
+
target_logger: Logger whose handlers receive the filter. Defaults to the
|
|
83
|
+
root logger, which in AWS Lambda is where runtime log handlers live.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The filter instance attached to the handlers, or ``None`` if the target
|
|
87
|
+
logger has no handlers to attach to.
|
|
88
|
+
"""
|
|
89
|
+
logger = target_logger if target_logger is not None else logging.getLogger()
|
|
90
|
+
|
|
91
|
+
context_filter: OtelContextLogFilter | None = None
|
|
92
|
+
for handler in logger.handlers:
|
|
93
|
+
existing = next(
|
|
94
|
+
(f for f in handler.filters if isinstance(f, OtelContextLogFilter)),
|
|
95
|
+
None,
|
|
96
|
+
)
|
|
97
|
+
if existing is not None:
|
|
98
|
+
# Reuse the already-installed filter so a single instance is shared.
|
|
99
|
+
context_filter = existing
|
|
100
|
+
continue
|
|
101
|
+
if context_filter is None:
|
|
102
|
+
context_filter = OtelContextLogFilter(plugin)
|
|
103
|
+
handler.addFilter(context_filter)
|
|
104
|
+
|
|
105
|
+
return context_filter
|