feat(settings): Settings → System shows table sizes and what the last cleanup removed (#5469)
#5469 "Settings → System: table sizes and what the last cleanup removed". - Storage: each table's size on disk (pg_total_relation_size, indexes and TOAST included) and Postgres's row estimate (reltuples), largest first, the 15 largest by name and the rest summed, under the database's total size. Read from the catalog only, never by counting rows. - Last cleanup: when it ran, how long it took, and what each pass removed (each aged table, run log files, the metric and Docker rollups, the log ring), or "Nothing was past its window". run_cleanup now returns that summary and keeps it as the cleanup.last_run setting, as reports.last_sent_at does; cleanup_secs (#5467) is recorded from the same figure. - A hint links to where each retention window is set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,16 @@ from quart import render_template, session
|
||||
from steward.core import health
|
||||
|
||||
AT = datetime(2026, 10, 9, 12, 0, tzinfo=timezone.utc)
|
||||
STORAGE = {"total": 3 * 2**30, "rest": {"count": 0, "bytes": 0},
|
||||
"tables": [{"name": "plugin_metrics", "bytes": 2 * 2**30, "rows": 41_000_000},
|
||||
{"name": "docker_logs", "bytes": 512 * 2**20, "rows": None}]}
|
||||
|
||||
|
||||
async def _page(app, ctx, storage=STORAGE, cleanup=None):
|
||||
async with app.test_request_context("/settings/system/"):
|
||||
session["user_role"] = "admin"
|
||||
page = await render_template("settings/system.html", **ctx, storage=storage, cleanup=cleanup)
|
||||
return " ".join(page.split())
|
||||
|
||||
|
||||
def _previews(**over):
|
||||
@@ -52,10 +62,50 @@ async def test_the_page_reads_as_a_verdict_then_each_measure(app, previews):
|
||||
got = await health.process_health(None)
|
||||
for r in got["rows"]:
|
||||
r["spark"] = ""
|
||||
async with app.test_request_context("/settings/system/"):
|
||||
session["user_role"] = "admin"
|
||||
page = " ".join((await render_template("settings/system.html", **got)).split())
|
||||
page = await _page(app, got)
|
||||
assert '<span class="tone-warn">Steward wasn't watching for up to 7m' in page
|
||||
assert 'title="<' not in page # a <time> element never lands inside an attribute
|
||||
assert "Database round trip" in page and "4 ms" in page and "2 ms – 9 ms" in page
|
||||
assert page.index("page-verdict") < page.index("Agent posts per minute")
|
||||
|
||||
|
||||
# ── Storage and the last cleanup (#5469) ─────────────────────────────────────
|
||||
|
||||
async def test_tables_largest_first_then_what_the_cleanup_removed(app, previews):
|
||||
previews()
|
||||
got = await health.process_health(None)
|
||||
for r in got["rows"]:
|
||||
r["spark"] = ""
|
||||
cleanup = {"at": AT, "secs": 12.5, "passes": 14,
|
||||
"removed": [("plugin_metrics: raw samples rolled into hourly", 86_400)]}
|
||||
page = await _page(app, got, cleanup=cleanup)
|
||||
assert "The database takes 3.0 GiB on disk." in page
|
||||
assert page.index(">plugin_metrics<") < page.index(">docker_logs<")
|
||||
assert "41,000,000" in page and "512.0 MiB" in page
|
||||
assert "took 12s" in page and "86,400" in page
|
||||
assert page.index("Steward itself") < page.index("Storage") < page.index("Last cleanup")
|
||||
|
||||
|
||||
async def test_a_quiet_cleanup_and_one_not_yet_run_each_say_so(app, previews):
|
||||
previews()
|
||||
got = await health.process_health(None)
|
||||
for r in got["rows"]:
|
||||
r["spark"] = ""
|
||||
quiet = await _page(app, got, cleanup={"at": AT, "secs": 1.0, "passes": 14, "removed": []})
|
||||
assert "Nothing was past its window (14 passes)." in quiet
|
||||
assert "None has finished since this version started" in await _page(app, got)
|
||||
|
||||
|
||||
async def test_the_last_cleanup_keeps_only_what_went(monkeypatch):
|
||||
stored = {"at": "2026-10-09T12:00:00+00:00", "secs": 3.2,
|
||||
"removed": [["audit_events: older than 365 days", 0], ["docker_logs: past the age window", 7]]}
|
||||
|
||||
async def fake(db, key):
|
||||
assert key == "cleanup.last_run"
|
||||
return stored
|
||||
monkeypatch.setattr("steward.core.settings.get_setting", fake)
|
||||
got = await health.last_cleanup(None)
|
||||
assert got["at"] == AT and got["passes"] == 2
|
||||
assert got["removed"] == [("docker_logs: past the age window", 7)]
|
||||
stored = {}
|
||||
assert await health.last_cleanup(None) is None
|
||||
|
||||
@@ -22,7 +22,7 @@ def app():
|
||||
def test_the_audit_window_comes_from_settings(app):
|
||||
from sqlalchemy import text
|
||||
from steward.core.cleanup import run_cleanup
|
||||
from steward.core.settings import set_setting
|
||||
from steward.core.settings import get_setting, set_setting
|
||||
from steward.models.audit import AuditEvent
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
@@ -35,16 +35,23 @@ def test_the_audit_window_comes_from_settings(app):
|
||||
s.add(AuditEvent(action=tag, entity_id="old", timestamp=now - timedelta(days=40)))
|
||||
s.add(AuditEvent(action=tag, entity_id="new", timestamp=now - timedelta(days=20)))
|
||||
try:
|
||||
await run_cleanup(app)
|
||||
done = await run_cleanup(app)
|
||||
finally:
|
||||
async with app.db_sessionmaker() as s:
|
||||
async with s.begin():
|
||||
await set_setting(s, "audit.retention_days", 365)
|
||||
async with app.db_sessionmaker() as s:
|
||||
return [r[0] for r in (await s.execute(text(
|
||||
kept = [r[0] for r in (await s.execute(text(
|
||||
"SELECT entity_id FROM audit_events WHERE action = :a"), {"a": tag})).all()]
|
||||
return kept, done, await get_setting(s, "cleanup.last_run")
|
||||
|
||||
assert asyncio.run(_go()) == ["new"]
|
||||
kept, done, stored = asyncio.run(_go())
|
||||
assert kept == ["new"]
|
||||
# What it did is kept for Settings → System (#5469), each pass by name.
|
||||
counts = dict(done["removed"])
|
||||
assert counts["audit_events: older than 30 days"] >= 1
|
||||
assert "plugin_metrics: raw samples rolled into hourly" in counts and done["secs"] >= 0
|
||||
assert stored == done
|
||||
|
||||
|
||||
def test_the_chart_seam_follows_the_raw_metrics_setting(app):
|
||||
@@ -77,3 +84,19 @@ def test_the_chart_seam_follows_the_raw_metrics_setting(app):
|
||||
await set_setting(s, "metrics.retention.raw_days", 7)
|
||||
|
||||
assert [v for _, v in asyncio.run(_go())] == [10.0]
|
||||
|
||||
|
||||
def test_table_sizes_come_from_the_catalog_largest_first(app):
|
||||
"""Settings → System's storage figures (#5469), read against real Postgres."""
|
||||
from steward.core.health import TABLES_SHOWN, table_sizes
|
||||
|
||||
async def _go():
|
||||
async with app.db_sessionmaker() as s:
|
||||
return await table_sizes(s)
|
||||
|
||||
got = asyncio.run(_go())
|
||||
sizes = [t["bytes"] for t in got["tables"]]
|
||||
assert sizes == sorted(sizes, reverse=True) and len(sizes) <= TABLES_SHOWN
|
||||
names = {t["name"] for t in got["tables"]}
|
||||
assert got["rest"]["count"] or "audit_events" in names # the schema's own tables
|
||||
assert got["total"] >= sum(sizes) + got["rest"]["bytes"] > 0
|
||||
|
||||
Reference in New Issue
Block a user