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.
Files changed (59) hide show
  1. {wellapi-0.10.0 → wellapi-0.10.2}/PKG-INFO +1 -1
  2. {wellapi-0.10.0 → wellapi-0.10.2}/docs/telemetry.md +30 -0
  3. {wellapi-0.10.0 → wellapi-0.10.2}/pyproject.toml +1 -1
  4. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/telemetry/config.py +36 -1
  5. {wellapi-0.10.0 → wellapi-0.10.2}/tests/telemetry/test_config.py +39 -0
  6. {wellapi-0.10.0 → wellapi-0.10.2}/uv.lock +225 -225
  7. {wellapi-0.10.0 → wellapi-0.10.2}/.github/workflows/build.pipeline.yml +0 -0
  8. {wellapi-0.10.0 → wellapi-0.10.2}/.gitignore +0 -0
  9. {wellapi-0.10.0 → wellapi-0.10.2}/.python-version +0 -0
  10. {wellapi-0.10.0 → wellapi-0.10.2}/README.md +0 -0
  11. {wellapi-0.10.0 → wellapi-0.10.2}/docs/framework-usage.md +0 -0
  12. {wellapi-0.10.0 → wellapi-0.10.2}/docs/superpowers/plans/2026-05-29-otel-native-telemetry.md +0 -0
  13. {wellapi-0.10.0 → wellapi-0.10.2}/docs/superpowers/specs/2026-05-29-otel-native-telemetry-design.md +0 -0
  14. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/__init__.py +0 -0
  15. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/__main__.py +0 -0
  16. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/applications.py +0 -0
  17. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/awsmodel.py +0 -0
  18. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/build/__init__.py +0 -0
  19. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/build/cdk.py +0 -0
  20. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/build/packager.py +0 -0
  21. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/cli/__init__.py +0 -0
  22. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/cli/main.py +0 -0
  23. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/convertors.py +0 -0
  24. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/datastructures.py +0 -0
  25. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/dependencies/__init__.py +0 -0
  26. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/dependencies/models.py +0 -0
  27. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/dependencies/utils.py +0 -0
  28. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/exception_handlers.py +0 -0
  29. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/exceptions.py +0 -0
  30. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/local/__init__.py +0 -0
  31. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/local/router.py +0 -0
  32. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/local/server.py +0 -0
  33. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/middleware/__init__.py +0 -0
  34. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/middleware/base.py +0 -0
  35. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/middleware/cors.py +0 -0
  36. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/middleware/error.py +0 -0
  37. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/middleware/exceptions.py +0 -0
  38. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/middleware/main.py +0 -0
  39. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/models.py +0 -0
  40. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/openapi/__init__.py +0 -0
  41. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/openapi/docs.py +0 -0
  42. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/openapi/models.py +0 -0
  43. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/openapi/security_model.py +0 -0
  44. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/openapi/utils.py +0 -0
  45. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/params.py +0 -0
  46. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/routing.py +0 -0
  47. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/security.py +0 -0
  48. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/telemetry/__init__.py +0 -0
  49. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/telemetry/attributes.py +0 -0
  50. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/telemetry/flush.py +0 -0
  51. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/telemetry/middleware.py +0 -0
  52. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/testclient.py +0 -0
  53. {wellapi-0.10.0 → wellapi-0.10.2}/src/wellapi/utils.py +0 -0
  54. {wellapi-0.10.0 → wellapi-0.10.2}/tests/conftest.py +0 -0
  55. {wellapi-0.10.0 → wellapi-0.10.2}/tests/telemetry/__init__.py +0 -0
  56. {wellapi-0.10.0 → wellapi-0.10.2}/tests/telemetry/test_applications.py +0 -0
  57. {wellapi-0.10.0 → wellapi-0.10.2}/tests/telemetry/test_attributes.py +0 -0
  58. {wellapi-0.10.0 → wellapi-0.10.2}/tests/telemetry/test_flush.py +0 -0
  59. {wellapi-0.10.0 → wellapi-0.10.2}/tests/telemetry/test_middleware.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wellapi
3
- Version: 0.10.0
3
+ Version: 0.10.2
4
4
  Summary: A simple web framework for aws lambda
5
5
  Author-email: romayuhym <romayuhym@gmail.com>
6
6
  Requires-Python: >=3.12
@@ -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,6 +1,6 @@
1
1
  [project]
2
2
  name = "wellapi"
3
- version = "0.10.0"
3
+ version = "0.10.2"
4
4
  description = "A simple web framework for aws lambda"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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
- tracer_provider = TracerProvider(resource=resource)
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):