wellapi 0.10.0__tar.gz → 0.10.2__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.
- {wellapi-0.10.0 → wellapi-0.10.2}/PKG-INFO +1 -1
- {wellapi-0.10.0 → wellapi-0.10.2}/docs/telemetry.md +30 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/pyproject.toml +1 -1
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/telemetry/config.py +36 -1
- {wellapi-0.10.0 → wellapi-0.10.2}/tests/telemetry/test_config.py +39 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/uv.lock +225 -225
- {wellapi-0.10.0 → wellapi-0.10.2}/.github/workflows/build.pipeline.yml +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/.gitignore +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/.python-version +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/README.md +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/docs/framework-usage.md +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/docs/superpowers/plans/2026-05-29-otel-native-telemetry.md +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/docs/superpowers/specs/2026-05-29-otel-native-telemetry-design.md +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/__init__.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/__main__.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/applications.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/awsmodel.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/build/__init__.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/build/cdk.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/build/packager.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/cli/__init__.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/cli/main.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/convertors.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/datastructures.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/dependencies/__init__.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/dependencies/models.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/dependencies/utils.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/exception_handlers.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/exceptions.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/local/__init__.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/local/router.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/local/server.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/middleware/__init__.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/middleware/base.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/middleware/cors.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/middleware/error.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/middleware/exceptions.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/middleware/main.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/models.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/openapi/__init__.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/openapi/docs.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/openapi/models.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/openapi/security_model.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/openapi/utils.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/params.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/routing.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/security.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/telemetry/__init__.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/telemetry/attributes.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/telemetry/flush.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/telemetry/middleware.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/testclient.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/utils.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/tests/conftest.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/tests/telemetry/__init__.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/tests/telemetry/test_applications.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/tests/telemetry/test_attributes.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/tests/telemetry/test_flush.py +0 -0
- {wellapi-0.10.0 → wellapi-0.10.2}/tests/telemetry/test_middleware.py +0 -0
|
@@ -107,6 +107,36 @@ to the function name).
|
|
|
107
107
|
`OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES` override these without code
|
|
108
108
|
changes.
|
|
109
109
|
|
|
110
|
+
## SnapStart and ID uniqueness
|
|
111
|
+
|
|
112
|
+
When `warmup`/`use_snap_start` is on, the function is published with AWS Lambda
|
|
113
|
+
SnapStart: Lambda boots one environment, snapshots its memory, and restores that
|
|
114
|
+
**same** snapshot into every execution environment. Anything seeded once at init
|
|
115
|
+
is therefore shared by all restored environments.
|
|
116
|
+
|
|
117
|
+
OpenTelemetry's default ID generator draws trace/span IDs from Python's
|
|
118
|
+
module-level `random` (a Mersenne Twister seeded once at import). Under SnapStart
|
|
119
|
+
that frozen state is cloned, so every restored environment emits the **same
|
|
120
|
+
sequence** of trace/span IDs — you see one `trace_id` reused across unrelated
|
|
121
|
+
requests. wellapi avoids this by configuring the tracer provider with a
|
|
122
|
+
`SystemRandom` (`os.urandom`-backed) ID generator: the kernel CSPRNG is reseeded
|
|
123
|
+
with fresh entropy on restore, so IDs stay unique. No action needed for telemetry.
|
|
124
|
+
|
|
125
|
+
If your own handlers (or other libraries) rely on the `random` module, `uuid1`,
|
|
126
|
+
or any non-CSPRNG source for uniqueness, they have the same SnapStart hazard.
|
|
127
|
+
Reseed them in an after-restore hook:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import random
|
|
131
|
+
from snapshot_restore_py import register_after_restore
|
|
132
|
+
|
|
133
|
+
@register_after_restore
|
|
134
|
+
def _reseed():
|
|
135
|
+
random.seed() # pull fresh entropy from the OS after restore
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`os.urandom`, `secrets`, and `uuid4` read the kernel CSPRNG and are already safe.
|
|
139
|
+
|
|
110
140
|
## Local development
|
|
111
141
|
|
|
112
142
|
Telemetry is off until `use_telemetry()` is called. If you call it locally without
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import random
|
|
2
3
|
from dataclasses import dataclass
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
@@ -13,6 +14,37 @@ _INSTALL_HINT = (
|
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
class _SystemRandomIdGenerator:
|
|
18
|
+
"""Trace/span ID generator backed by the kernel CSPRNG (`os.urandom`).
|
|
19
|
+
|
|
20
|
+
OpenTelemetry's default `RandomIdGenerator` draws IDs from the module-level
|
|
21
|
+
Mersenne Twister, which Python seeds once at import. AWS Lambda SnapStart
|
|
22
|
+
captures that PRNG state in the snapshot and shares it across every restored
|
|
23
|
+
execution environment, so the default generator emits identical trace/span
|
|
24
|
+
IDs in lockstep across environments — observed as duplicate `trace_id`s on
|
|
25
|
+
unrelated requests. The kernel CSPRNG is reseeded with fresh entropy on
|
|
26
|
+
restore, so `random.SystemRandom` (i.e. `os.urandom`) stays unique. Duck-typed
|
|
27
|
+
against `opentelemetry.sdk.trace.id_generator.IdGenerator` to avoid importing
|
|
28
|
+
the SDK at module import time (kept lazy, like the rest of this module)."""
|
|
29
|
+
|
|
30
|
+
_rand = random.SystemRandom()
|
|
31
|
+
|
|
32
|
+
def generate_span_id(self) -> int:
|
|
33
|
+
span_id = self._rand.getrandbits(64)
|
|
34
|
+
while span_id == trace.INVALID_SPAN_ID:
|
|
35
|
+
span_id = self._rand.getrandbits(64)
|
|
36
|
+
return span_id
|
|
37
|
+
|
|
38
|
+
def generate_trace_id(self) -> int:
|
|
39
|
+
trace_id = self._rand.getrandbits(128)
|
|
40
|
+
while trace_id == trace.INVALID_TRACE_ID:
|
|
41
|
+
trace_id = self._rand.getrandbits(128)
|
|
42
|
+
return trace_id
|
|
43
|
+
|
|
44
|
+
def is_trace_id_random(self) -> bool:
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
|
|
16
48
|
@dataclass
|
|
17
49
|
class TelemetryHandle:
|
|
18
50
|
"""Handles to the configured providers. Returned by `app.use_telemetry()` so
|
|
@@ -73,7 +105,10 @@ def _build_providers(resource: Any) -> tuple[Any, Any, Any]:
|
|
|
73
105
|
|
|
74
106
|
# Exporters default to OTEL_EXPORTER_OTLP_ENDPOINT or http://localhost:4318,
|
|
75
107
|
# i.e. the collector-only Lambda layer running on localhost.
|
|
76
|
-
|
|
108
|
+
# _SystemRandomIdGenerator keeps IDs unique under SnapStart (see its docstring).
|
|
109
|
+
tracer_provider = TracerProvider(
|
|
110
|
+
resource=resource, id_generator=_SystemRandomIdGenerator()
|
|
111
|
+
)
|
|
77
112
|
tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
|
|
78
113
|
|
|
79
114
|
meter_provider = MeterProvider(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
1
3
|
import wellapi.telemetry.config as config_mod
|
|
2
4
|
from wellapi.telemetry.config import TelemetryHandle, _build_resource
|
|
3
5
|
|
|
@@ -6,6 +8,40 @@ def _attrs(resource):
|
|
|
6
8
|
return dict(resource.attributes)
|
|
7
9
|
|
|
8
10
|
|
|
11
|
+
def _ids_after_seed(generator, seed):
|
|
12
|
+
"""IDs a freshly snapshot-restored env would emit: identical frozen
|
|
13
|
+
module-level random state (the AWS SnapStart failure mode)."""
|
|
14
|
+
state = random.getstate()
|
|
15
|
+
try:
|
|
16
|
+
random.seed(seed)
|
|
17
|
+
return generator.generate_trace_id(), generator.generate_span_id()
|
|
18
|
+
finally:
|
|
19
|
+
random.setstate(state)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_default_random_id_generator_collides_under_cloned_random_state():
|
|
23
|
+
# Documents the bug: the OTel default generator draws from the module-level
|
|
24
|
+
# Mersenne Twister, which SnapStart freezes and shares across restored envs.
|
|
25
|
+
from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
|
|
26
|
+
|
|
27
|
+
gen = RandomIdGenerator()
|
|
28
|
+
assert _ids_after_seed(gen, 1234) == _ids_after_seed(gen, 1234)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_snapstart_safe_id_generator_survives_cloned_random_state():
|
|
32
|
+
gen = config_mod._SystemRandomIdGenerator()
|
|
33
|
+
# Two envs restored from the same snapshot (same frozen MT state) must still
|
|
34
|
+
# produce distinct trace/span IDs.
|
|
35
|
+
assert _ids_after_seed(gen, 1234) != _ids_after_seed(gen, 1234)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_snapstart_safe_id_generator_marks_trace_id_random():
|
|
39
|
+
gen = config_mod._SystemRandomIdGenerator()
|
|
40
|
+
assert gen.is_trace_id_random() is True
|
|
41
|
+
assert 0 < gen.generate_trace_id() < 2**128
|
|
42
|
+
assert 0 < gen.generate_span_id() < 2**64
|
|
43
|
+
|
|
44
|
+
|
|
9
45
|
def test_resource_uses_faas_env(monkeypatch):
|
|
10
46
|
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "orders-fn")
|
|
11
47
|
monkeypatch.setenv("AWS_REGION", "eu-west-1")
|
|
@@ -53,6 +89,9 @@ def test_build_providers_attaches_resource_and_returns_sdk_types():
|
|
|
53
89
|
assert isinstance(mp, MeterProvider)
|
|
54
90
|
assert isinstance(lp, LoggerProvider)
|
|
55
91
|
assert dict(tp.resource.attributes)["service.name"] == "t"
|
|
92
|
+
# SnapStart-safe IDs: the tracer provider must not use the default MT-backed
|
|
93
|
+
# generator, otherwise restored envs emit duplicate trace/span IDs.
|
|
94
|
+
assert isinstance(tp.id_generator, config_mod._SystemRandomIdGenerator)
|
|
56
95
|
|
|
57
96
|
|
|
58
97
|
def test_configure_reuses_existing_sdk_provider(monkeypatch):
|