feat(docker): Docker disk is recorded over time per host, the Disk page shows each host's growth and folds images under their repository, and the overview has one disk line per host (#5304–#5306)
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 54s
CI / unit (push) Successful in 1m21s
CI / publish (push) Skipped

- #5304 "Docker disk totals recorded over time per host, with a growth chart and alertable metrics"
- #5305 "Disk page: images grouped by repository, versions folded under it"
- #5306 "Docker overview: one disk line per host linking to the Disk page"

Each `docker system df` report is now also recorded as metrics, with the host
as the resource: disk_total_bytes, its parts, and disk_reclaimable_bytes. An
alert rule can watch them. The Disk page starts with the hosts side by side,
showing how much each grew over the selected range with a line of it, then
gives each host its own section. An image's versions fold under its
repository, which is charged only for the layers its versions don't share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 20:16:13 -04:00
co-authored by Claude Opus 5.5
parent 0b5cd23d38
commit 8566c8f78e
14 changed files with 513 additions and 143 deletions
+22
View File
@@ -683,3 +683,25 @@ def stale_after_seconds(config) -> int:
"""How old a container's last report may be before the page calls it
stale: the host agent's own staleness window, since the agent reports it."""
return int(((config.get("PLUGINS") or {}).get("host_agent") or {}).get("stale_after_seconds", 180))
async def disk_hosts(db, since: datetime, *, points: int = 60) -> list[dict]:
"""Each Docker host's disk as `docker system df` last said, most space
first, with its recorded total since `since` (#5304): the points, oldest
first, and the growth over them (disk_view.growth; None until there are
two). Hosts are named by their Steward host."""
from steward.core import metric_series
from .disk_view import DISK_PARTS, growth
from .models import DockerDiskUsage
summaries = list((await db.execute(select(DockerDiskUsage))).scalars())
hosts = await host_map(db, {s.host_id for s in summaries})
out = []
for s in summaries:
name = hosts[s.host_id].name if s.host_id in hosts else s.host_id
parts = {m: int(getattr(s, f) or 0) for m, f in DISK_PARTS.items()}
series = await metric_series.points(db, "docker", name, "disk_total_bytes", since, target=points)
out.append({"host_id": s.host_id, "host_name": name, "as_of": s.updated_at,
"total": sum(parts.values()), "parts": parts,
"reclaimable": int(s.images_reclaimable or 0), "points": series,
"growth": growth(series)})
return sorted(out, key=lambda h: (-h["total"], h["host_name"].lower()))