feat(traefik): the page leads with its verdict and the request chart (#5710)
The Traefik page now reads state first: a verdict naming each router that needs a look, why, and when it started (from its history buckets over the page's range), then the request chart following the page's time range, then the problem cards (which say since when too), the proxy's own figures, and the healthy rest. The verdict is filled out of band by /rows each poll; the "Needs attention" and "Healthy routers" tiles it replaces are gone. The request chart takes ?range= from the page; past a day its times carry the day, and its clock is 24-hour (hourCycle h23). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -242,10 +242,10 @@ async def test_the_request_chart_says_its_figures_before_the_chart(app):
|
||||
async with app.test_request_context("/plugins/traefik/widget/request_chart"):
|
||||
page = await render_plugin_template(
|
||||
app, "traefik", "traefik/widget_request_chart.html", labels_json="[1, 2]",
|
||||
req_rate_json="[1.5, 4.25]", error_rate_json="[0, 0.5]", widget_id="7", hours=6,
|
||||
now_req=4.25, peak_req=4.25, peak_err=0.5)
|
||||
req_rate_json="[1.5, 4.25]", error_rate_json="[0, 0.5]", widget_id="7", span="6h",
|
||||
days=False, now_req=4.25, peak_req=4.25, peak_err=0.5)
|
||||
empty = await render_plugin_template(
|
||||
app, "traefik", "traefik/widget_request_chart.html", labels_json="[]", hours=6)
|
||||
app, "traefik", "traefik/widget_request_chart.html", labels_json="[]", span="6h")
|
||||
page = " ".join(page.split())
|
||||
assert "Now 4.25 req/s across all routers, peak 4.25. 5xx peaked at 0.50% of responses." in page
|
||||
assert page.index("w-verdict") < page.index("<canvas")
|
||||
@@ -313,3 +313,95 @@ async def test_the_page_links_an_admin_to_the_router_thresholds(app, role, linke
|
||||
page = await render_plugin_template(app, "traefik", "traefik/index.html", poll_interval=30,
|
||||
access_log_enabled=False, current_range="1h")
|
||||
assert ('href="/settings/thresholds/#traefik"' in page) is linked
|
||||
|
||||
|
||||
# ── State first: the verdict and the request chart lead the page (#5710) ────
|
||||
|
||||
def _bucket(at, e5=0.0, e4=0.0, p95=20.0):
|
||||
return NS(bucket=at, error_rate_5xx_pct=e5, error_rate_4xx_pct=e4, latency_p95_ms=p95)
|
||||
|
||||
|
||||
def test_a_problem_dates_from_the_start_of_its_trailing_run_of_bad_buckets():
|
||||
from plugins.traefik.routes import problem_since
|
||||
t = [datetime(2026, 10, 14, h, tzinfo=timezone.utc) for h in range(8, 13)]
|
||||
# Bad at 9, fine at 10, bad again from 11: the problem now began at 11.
|
||||
history = [_bucket(t[0]), _bucket(t[1], e5=4.0), _bucket(t[2]), _bucket(t[3], e5=4.0),
|
||||
_bucket(t[4], p95=900.0)]
|
||||
assert problem_since(history, TH) == (t[3], False)
|
||||
# Bad in every bucket: it began before the range shown.
|
||||
assert problem_since([_bucket(x, e4=6.0) for x in t], TH) == (t[0], True)
|
||||
# Fine in the newest bucket (a problem only the 15-minute window shows): no date.
|
||||
assert problem_since(history[:3], TH) == (None, False)
|
||||
assert problem_since([], TH) == (None, False)
|
||||
|
||||
|
||||
async def _rows(app, routers, **ctx):
|
||||
problems, healthy = split_routers(routers)
|
||||
ctx = {"gone": [], "global_stat": None, "certs": [], "old_certs": [], "range_key": "24h",
|
||||
"current_range": "24h", "error_4xx": 1.0, "error_5xx": 0.1, **ctx}
|
||||
async with app.test_request_context("/plugins/traefik/rows"):
|
||||
page = await render_plugin_template(app, "traefik", "traefik/rows.html", problems=problems,
|
||||
healthy=healthy, **ctx)
|
||||
return " ".join(page.split())
|
||||
|
||||
|
||||
async def test_the_verdict_names_each_problem_and_since_when(app):
|
||||
started = datetime.now(timezone.utc) - timedelta(hours=2)
|
||||
api = _router("api", _latest(e5=3.0))
|
||||
api.update(since=started, since_before=False)
|
||||
slow = _router("slow", _latest(p95=450.0))
|
||||
slow.update(since=started, since_before=True)
|
||||
stat = NS(config_last_reload_at=None, config_reloads_total=None, open_conns_total=12.0,
|
||||
process_memory_bytes=None)
|
||||
page = await _rows(app, [api, slow, _router("web", _latest())], global_stat=stat,
|
||||
gone=[_router("old", _latest())])
|
||||
|
||||
verdict = page[page.index('id="traefik-verdict"'):page.index("</p>")]
|
||||
assert 'hx-swap-oob="true"' in verdict
|
||||
assert '<strong class="tone-crit">2 routers need a look</strong>' in verdict
|
||||
assert "api</span>, 3.0% of responses are 5xx, started <time" in verdict and "2h ago" in verdict
|
||||
assert "slow</span>, p95 latency is 450 ms, all of the last 24h" in verdict
|
||||
assert "1 healthy." in verdict and "1 no longer reported." in verdict
|
||||
# The card says since when too; the proxy's figures follow the cards, not the verdict.
|
||||
card = page[page.index(">api</h3>"):]
|
||||
assert card.index("3.0% of responses are 5xx, started <time") < card.index("</p>")
|
||||
assert page.index(">api</h3>") < page.index("Open connections") < page.index('id="traefik-healthy"')
|
||||
assert "Needs attention" not in page and "Healthy routers" not in page # the verdict says them
|
||||
|
||||
|
||||
async def test_the_verdict_when_all_is_well_or_nothing_is_reported(app):
|
||||
well = await _rows(app, [_router("web", _latest()), _router("api", _latest())])
|
||||
assert '<strong class="tone-ok">All 2 routers healthy</strong>' in well
|
||||
assert "stat-strip" not in well # no figures, no empty strip
|
||||
nothing = await _rows(app, [])
|
||||
assert "No routers reported yet." in nothing
|
||||
|
||||
|
||||
async def test_the_verdict_lists_three_problems_then_counts_the_rest(app):
|
||||
page = await _rows(app, [_router(f"r{i}", _latest(e5=3.0)) for i in range(5)])
|
||||
verdict = page[page.index('id="traefik-verdict"'):page.index("</p>")]
|
||||
assert "r2</span>" in verdict and "r3</span>" not in verdict and "; 2 more below." in verdict
|
||||
|
||||
|
||||
async def test_the_page_puts_the_verdict_then_the_request_chart_above_the_routers(app):
|
||||
from quart import session
|
||||
async with app.test_request_context("/plugins/traefik/"):
|
||||
session["user_role"] = "viewer"
|
||||
page = await render_plugin_template(app, "traefik", "traefik/index.html", poll_interval=30,
|
||||
access_log_enabled=True, current_range="7d")
|
||||
assert (page.index('id="traefik-verdict"') < page.index('id="traefik-chart"')
|
||||
< page.index('id="traefik-rows"') < page.index('id="traefik-traffic"'))
|
||||
chart = page[page.index('id="traefik-chart"'):page.index('id="traefik-rows"')]
|
||||
assert 'hx-get="/plugins/traefik/widget/request_chart?wid=page"' in chart
|
||||
assert 'hx-include="#time-range"' in chart and "rangeChange from:body" in chart
|
||||
|
||||
|
||||
async def test_the_request_chart_over_days_labels_its_times_with_the_day(app):
|
||||
kw = dict(labels_json="[1, 2]", req_rate_json="[1, 2]", error_rate_json="[0, 0]", widget_id="page",
|
||||
span="7d", now_req=2, peak_req=2, peak_err=0)
|
||||
async with app.test_request_context("/plugins/traefik/widget/request_chart"):
|
||||
week = await render_plugin_template(app, "traefik", "traefik/widget_request_chart.html", days=True, **kw)
|
||||
hours = await render_plugin_template(app, "traefik", "traefik/widget_request_chart.html", days=False, **kw)
|
||||
assert '{ weekday: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }' in week
|
||||
assert '{ hour: "2-digit", minute: "2-digit", hourCycle: "h23" }' in hours
|
||||
assert "last 7d, your local time" in week
|
||||
|
||||
Reference in New Issue
Block a user