feat(hosts): one health header, graded by one set of thresholds (#5122)
The host page's vitals strip becomes its health header: Live or Stale, then one sentence — "Memory 93% is past its critical threshold", "Every reading is within its thresholds", or, for a stale host, "No report since 20m ago; the readings below are from then" — and last seen as |ago. CPU, memory, root disk and load per core follow as stat tiles graded by Settings → Thresholds, with their sparklines. The full-metrics page drew its bars and gauges at a hard-coded 70/90 (temperatures 70/85) while every other page used the configured cutoffs (80/90 by default), so a 75 % CPU was amber here and fine everywhere else. bar() now takes the metric kind and colours its .meter-fill through status_class; gauges, temperatures and per-core bars do the same. The host's monitor rows move onto the Monitors list's shapes: a status dot, the name linking to the monitor's page, Ping/TCP/DNS/HTTP, pill-up/ pill-down, latency and uptime through status_class, labelled icon buttons. HEX_CEILING drops to 54. New formatting.worst_reading(readings, thresholds) picks the reading furthest past its cutoff, the first of equals winning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"""The host page says how the host is in one header, and grades every reading by
|
||||
Settings → Thresholds, nowhere else (#5122)."""
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from quart import render_template
|
||||
|
||||
from steward.core.formatting import worst_reading
|
||||
from steward.core.settings import to_thresholds_cfg
|
||||
from tests.test_style_tokens import hex_colours
|
||||
|
||||
TH = to_thresholds_cfg({}) # cpu/mem/disk warn 80 crit 90; load warn 80 crit 100
|
||||
|
||||
|
||||
@pytest.mark.parametrize("readings, worst", [
|
||||
([("CPU", 40, "cpu"), ("Memory", 50, "mem")], None),
|
||||
([("CPU", None, "cpu")], None), # no reading is not a problem
|
||||
([("CPU", 85, "cpu"), ("Memory", 95, "mem")], ("Memory", 95, "crit")), # crit beats warn
|
||||
([("CPU", 92, "cpu"), ("Memory", 95, "mem")], ("CPU", 92, "crit")), # first of equals
|
||||
([("Disk /", 81, "disk")], ("Disk /", 81, "warn")),
|
||||
])
|
||||
def test_worst_reading(readings, worst):
|
||||
assert worst_reading(readings, TH) == worst
|
||||
|
||||
|
||||
async def _vitals(app, *, cpu=40.0, mem=50.0, stale=False, seen_min=1):
|
||||
readings = (("CPU", cpu, "cpu"), ("Memory", mem, "mem"), ("Disk /", 30.0, "disk"),
|
||||
("Load per core", 20, "load"))
|
||||
async with app.test_request_context("/plugins/host_agent/vitals/h1"):
|
||||
page = await render_template(
|
||||
"_host_vitals.html", reporting=True, stale=stale,
|
||||
reg=SimpleNamespace(last_seen_at=datetime.now(timezone.utc) - timedelta(minutes=seen_min),
|
||||
agent_version="1.9.0"),
|
||||
cpu=cpu, mem=mem, disk_root=30.0, load_per_core=20,
|
||||
sparks={"cpu": "", "mem": "", "disk": "", "load": ""},
|
||||
psi={"cpu": None, "mem": None, "io": None}, worst=worst_reading(readings, TH))
|
||||
return re.sub(r">\s+<", "><", " ".join(page.split()))
|
||||
|
||||
|
||||
async def test_a_host_over_a_threshold_says_which_reading(app):
|
||||
page = await _vitals(app, cpu=40.0, mem=93.0)
|
||||
assert '<span class="badge badge-ok">Live</span>' in page
|
||||
assert '<span class="tone-crit">Memory 93%</span> is past its critical threshold.' in page
|
||||
assert 'class="stat-tile stat-crit"><span class="stat-tile-label">Memory</span>' in page
|
||||
assert 'class="stat-tile stat-ok"><span class="stat-tile-label">CPU</span>' in page
|
||||
assert "seen <time" in page and "agent v1.9.0" in page
|
||||
assert not hex_colours(page), hex_colours(page)
|
||||
|
||||
|
||||
async def test_a_quiet_host_says_so(app):
|
||||
assert "Every reading is within its thresholds." in await _vitals(app)
|
||||
|
||||
|
||||
async def test_a_stale_host_says_since_when_before_any_reading(app):
|
||||
page = await _vitals(app, mem=93.0, stale=True, seen_min=20)
|
||||
assert '<span class="badge badge-warn">Stale</span>' in page
|
||||
assert "No report since <time" in page and ">20m ago</time>; the readings below are from then." in page
|
||||
assert "past its" not in page # an old reading is not news
|
||||
|
||||
|
||||
async def test_the_metrics_page_grades_by_the_same_cutoffs(app):
|
||||
# 75 % was amber under the old hard-coded 70/90; Settings → Thresholds says warn at 80.
|
||||
async with app.test_request_context("/plugins/host_agent/h1/metrics"):
|
||||
page = await render_template(
|
||||
"_host_metrics.html", host=SimpleNamespace(address="10.0.0.2"), stale=False,
|
||||
reg=SimpleNamespace(distro=None, kernel=None, arch=None, agent_version=None,
|
||||
last_seen_at=datetime.now(timezone.utc)),
|
||||
hostlvl={"cpu_pct": 75.0, "mem_used_pct": 95.0, "temp_c_max": 72.0},
|
||||
cores=[(0, 85.0)], nets={}, disks_io={}, temps={"cpu": 90.0},
|
||||
mounts={"/": {"disk_used_pct": 50.0, "disk_used_bytes": 1, "disk_total_bytes": 2}})
|
||||
assert 'class="stat-val tone-ok" style="font-size:1.6rem;">75%' in page
|
||||
assert 'class="stat-val tone-crit" style="font-size:1.6rem;">95%' in page
|
||||
assert 'class="meter-fill meter-ok" style="width:75.0%;"' in page
|
||||
assert 'class="meter-fill meter-warn" style="width:85.0%;"' in page # the core
|
||||
assert 'class="stat-val tone-warn" style="font-size:1.6rem;">72°C' in page
|
||||
assert '<span class="tone-crit" style="font-variant-numeric:tabular-nums;">90°C' in page
|
||||
assert not hex_colours(page), hex_colours(page)
|
||||
Reference in New Issue
Block a user