test(traefik): the page and widget routes read normals and anomalies (#5741)
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 1m17s
CI / unit (push) Successful in 1m33s
CI / publish (push) Skipped

#5741 "Traefik routers are graded against their own normal". The rows
fragment and the Routers widget had no test that ran them against Postgres,
so router_norms and the anomaly list were covered only by template
renders. This one seeds a router answering 60% 4xx with a learned normal of
58% and an open p95 anomaly, and checks both routes answer 200, the page
says the share is usual, and lists the anomaly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 15:53:46 -04:00
co-authored by Claude Opus 5.5
parent 9f14620271
commit 8c37ed0841
+53
View File
@@ -177,3 +177,56 @@ def test_status_codes_sum_each_routers_codes_in_the_window_most_first(app):
got = asyncio.run(go())
assert got == {a: [("502", 50), ("404", 42)], b: [("401", 4)]}
def test_the_page_grades_a_router_against_its_normal_and_lists_its_anomalies(app):
"""#5741: the rows fragment and the Routers widget read each router's
normal for the hour and the routers' metric anomalies. A router that
answers 60% 4xx every day is healthy and says it is usual; its open p95
anomaly is listed on the page."""
import time
from steward.core.alerts import MetricBatch
from steward.models.norms import MetricAnomaly, MetricNorm
from steward.models.users import User, UserRole
from plugins.traefik.models import TraefikMetric
from plugins.traefik.routes import ROUTER_METRICS
now = datetime.now(timezone.utc).replace(microsecond=0)
tag = uuid.uuid4().hex[:8]
router, uid = f"errors-{tag}@docker", str(uuid.uuid4())
figures = {"request_rate": 5.0, "error_rate_4xx_pct": 60.0, "error_rate_5xx_pct": 0.0,
"latency_p50_ms": 5.0, "latency_p95_ms": 20.0, "latency_p99_ms": 30.0,
"response_bytes_rate": 100.0}
async def go():
async with app.db_sessionmaker() as s:
async with s.begin():
s.add(User(id=uid, username=f"tr-{tag}", email=f"tr-{tag}@test", password_hash="x",
role=UserRole.viewer, is_active=True))
s.add_all([TraefikMetric(router_name=router, scraped_at=now - timedelta(minutes=m),
**figures) for m in range(1, 6)])
batch = MetricBatch()
for metric in ROUTER_METRICS:
batch.add("traefik", router, metric, figures[metric], recorded_at=now)
await batch.record(s)
s.add(MetricNorm(source_module="traefik", resource_name=router,
metric_name="error_rate_4xx_pct", slot=now.hour, kind="day",
usual_hold=58.0, usual_peak=70.0, days_seen=7, updated_at=now))
s.add(MetricAnomaly(source_module="traefik", resource_name=router,
metric_name="latency_p95_ms", started_at=now - timedelta(minutes=20),
last_started_at=now - timedelta(minutes=20), occurrences=1,
peak_level=900.0, peak_at=now, usual_hold=20.0, usual_peak=40.0,
review="to_review"))
client = app.test_client()
async with client.session_transaction() as sess:
sess.update(user_id=uid, user_role="viewer", username=f"tr-{tag}", login_at=time.time())
rows = await client.get("/plugins/traefik/rows?range=1h")
widget = await client.get("/plugins/traefik/widget")
return (rows.status_code, await rows.get_data(as_text=True),
widget.status_code, await widget.get_data(as_text=True))
rows_status, page, widget_status, widget = asyncio.run(go())
assert rows_status == 200 and widget_status == 200
assert "4xx 60.0% is usual 58.0% at this hour" in page # healthy, and says why
assert "Out of its normal now" in page and router in page # the open anomaly's card
assert "4xx 60.0% is usual" not in widget # the widget's why is for problems