From 8c37ed0841ea03efa7922595a9bca981ebfb8de3 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 14 Oct 2026 15:53:46 -0400 Subject: [PATCH] test(traefik): the page and widget routes read normals and anomalies (#5741) #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 --- tests/integration/test_traefik_latest.py | 53 ++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/integration/test_traefik_latest.py b/tests/integration/test_traefik_latest.py index 0e6bdde..08893b6 100644 --- a/tests/integration/test_traefik_latest.py +++ b/tests/integration/test_traefik_latest.py @@ -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