feat(norms): hosts, containers and monitor response times are graded against their own normal (#5742)
CI / lint (push) Successful in 2s
CI / unit (push) Failing after 1m26s
CI / integration (push) Failing after 1m27s
CI / publish (push) Skipped

Host CPU, memory and load per core, container CPU and memory, and every
monitor's response time learn a normal per hour (milestone 513, System 182).
A reading past its fixed cutoff but within its normal reads healthy and says
what is usual at this hour; one well out of it keeps its grade and is a
metric anomaly. Disk, a monitor down and an agent offline keep their cutoffs.

- Each normal's floor, the smallest departure that counts, is a setting:
  Settings -> Thresholds -> Normals (norms.floor.*). Defaults: host CPU 10
  points, host memory 5, load per core 10, container CPU 10 points of one
  core, container memory 5, monitor response 100 ms; Traefik's 2-point 4xx
  and 50 ms p95 floors move there from constants. learn_normal takes a
  floor function (floor_setting) read at each pass.
- learn_normal(parts=False): a host learns, its cores don't.
- Ingest records load_pct (load_1m / cores x 100), the figure pages grade.
- Shared grading: anomalies.against / graded, worst_reading(normals=),
  hosts' host_grades, docker usage(thresholds=, norms=), monitors'
  latency_grade. Applied to host vitals, the Hosts widget, MCP overview,
  Docker usage, and the monitor list, widget and page.
- The monitor and container pages list their anomalies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 16:17:04 -04:00
co-authored by Claude Opus 5.5
parent f3cfb89612
commit f3433fe3ce
30 changed files with 638 additions and 93 deletions
+9
View File
@@ -129,6 +129,10 @@ def create_app(
app.jinja_env.globals["threshold_style"] = (
lambda value, kind: _ts_for(value, kind, app.config.get("THRESHOLDS", {}))
)
# `grade_style(grade)` — the same styling for a grade worked out already
# (one graded against its own normal, #5742).
from .core.settings import grade_style as _grade_style
app.jinja_env.globals["grade_style"] = _grade_style
# One way to write a size, a rate, a span and a moment, and to grade a
# reading ('ok' / 'warn' / 'crit' / 'unknown') against the same cutoffs.
@@ -445,6 +449,11 @@ def _register_core_tasks(app: Quart) -> None:
from .backup.job import tick
await tick(app)
# Monitor response times learn their normal (#5742); plugins register
# their own metrics from setup().
from .monitors.service import learn_response_times
learn_response_times(app)
async def run_metric_norms():
from .core.norms import run_norms
await run_norms(app)