Skip to content

Commit 80636b5

Browse files
authored
Update test_observability.py
1 parent 860fffa commit 80636b5

1 file changed

Lines changed: 97 additions & 0 deletions

File tree

‎tests/test_observability.py‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,100 @@ def test_json_formatter_emits_one_object(self) -> None:
7575
assert out["level"] == "INFO"
7676
assert out["request_id"] == "r1"
7777
assert out["message"] == "hello"
78+
79+
80+
class TestMetrics:
81+
def test_metrics_endpoint_exposes_prometheus_text(self, client: TestClient) -> None:
82+
# generate some traffic first
83+
client.get("/healthz")
84+
res = client.get("/metrics")
85+
assert res.status_code == 200
86+
assert "text/plain" in res.headers["content-type"]
87+
body = res.text
88+
assert "paw_http_requests_total" in body
89+
assert "# TYPE paw_http_requests_total counter" in body
90+
91+
def test_http_requests_counted_by_status(self, client: TestClient) -> None:
92+
from app.infra.metrics import get_registry
93+
94+
get_registry().reset()
95+
client.get("/healthz") # 200
96+
client.get("/auth/me") # 401
97+
body = client.get("/metrics").text
98+
assert 'paw_http_requests_total{method="GET",status="200"}' in body
99+
# the 401 from /auth/me is counted too
100+
assert 'status="401"' in body
101+
102+
def test_counter_and_histogram_render(self) -> None:
103+
from app.infra.metrics import MetricsRegistry
104+
105+
r = MetricsRegistry()
106+
r.counter("things_total", 3, labels={"kind": "a"})
107+
r.observe("lat_seconds", 0.2)
108+
out = r.render()
109+
assert 'things_total{kind="a"} 3.0' in out
110+
assert "lat_seconds_bucket" in out
111+
assert "lat_seconds_count" in out
112+
assert "lat_seconds_sum" in out
113+
114+
def test_method_label_is_bounded(self) -> None:
115+
from app.infra.metrics import normalize_method
116+
117+
assert normalize_method("get") == "GET"
118+
assert normalize_method("POST") == "POST"
119+
# an arbitrary/garbage verb collapses to a single bucket so it
120+
# cannot create unbounded metric series
121+
assert normalize_method("BREW") == "other"
122+
assert normalize_method("../../etc") == "other"
123+
124+
def test_garbage_method_does_not_grow_series(self, client: TestClient) -> None:
125+
from app.infra.metrics import get_registry
126+
127+
get_registry().reset()
128+
# a request with an unusual method must not mint a new label value
129+
client.request("BREW", "/healthz")
130+
body = client.get("/metrics").text
131+
assert 'method="other"' in body
132+
assert 'method="BREW"' not in body
133+
134+
135+
class TestTracing:
136+
def test_span_is_noop_without_tracer(self) -> None:
137+
from app.infra.metrics import span
138+
139+
# no tracer registered: must not raise, just runs the block
140+
ran = []
141+
with span("work", {"k": "v"}):
142+
ran.append(1)
143+
assert ran == [1]
144+
145+
def test_span_delegates_to_registered_tracer(self) -> None:
146+
from contextlib import contextmanager
147+
148+
from app.infra.metrics import set_tracer, span
149+
150+
calls = []
151+
152+
class FakeTracer:
153+
@contextmanager
154+
def span(self, name, attributes=None):
155+
calls.append((name, attributes))
156+
yield
157+
158+
set_tracer(FakeTracer())
159+
try:
160+
with span("run.exec", {"run_id": "r1"}):
161+
pass
162+
assert calls == [("run.exec", {"run_id": "r1"})]
163+
finally:
164+
set_tracer(None) # restore no-op for other tests
165+
166+
def test_span_records_duration_metric(self) -> None:
167+
from app.infra.metrics import get_registry, span
168+
169+
get_registry().reset()
170+
with span("timed.block"):
171+
pass
172+
out = get_registry().render()
173+
assert 'paw_span_duration_seconds' in out
174+
assert 'span="timed.block"' in out

0 commit comments

Comments
 (0)