landing: central container memory/load stats for all three sites
Collect RSS groups, top processes, and loadavg from inside each podman container (not host /proc), merge into performance.memory, show compact labels with cgroup limits and byte tooltips, and cover this in the test suite.
This commit is contained in:
parent
9e0f85024b
commit
78bbd80d1f
11 changed files with 474 additions and 69 deletions
106
scripts/taler-landing/merge_resources.py
Executable file
106
scripts/taler-landing/merge_resources.py
Executable file
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Merge container resource snapshot into landing stats.json performance block.
|
||||
|
||||
Usage:
|
||||
merge_resources.py STATS.json RESOURCES.json [-o OUT.json]
|
||||
|
||||
RESOURCES shape (from mem_snapshot_emit / collect_container_resources.sh):
|
||||
{ "ok": true, "loadavg": "0.1,0.2,0.3", "memory": { ... } }
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
def compact_memory(mem: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Ensure human labels + short top cmd for UI width."""
|
||||
if not isinstance(mem, dict):
|
||||
return mem
|
||||
# Prefer explicit label (with limit); else human
|
||||
if not mem.get("container_rss_label"):
|
||||
h = mem.get("container_rss_human") or "—"
|
||||
lim = mem.get("cgroup_limit_human")
|
||||
if lim:
|
||||
mem["container_rss_label"] = f"{h} / {lim}"
|
||||
else:
|
||||
mem["container_rss_label"] = h
|
||||
tops = mem.get("top")
|
||||
if isinstance(tops, list):
|
||||
for t in tops:
|
||||
if not isinstance(t, dict):
|
||||
continue
|
||||
cmd = str(t.get("cmd") or "")
|
||||
if len(cmd) > 72:
|
||||
t["cmd_full"] = cmd
|
||||
t["cmd"] = cmd[:69] + "…"
|
||||
# ensure human present
|
||||
if not t.get("rss_human") and t.get("rss_bytes") is not None:
|
||||
try:
|
||||
b = int(t["rss_bytes"])
|
||||
if b < 1024:
|
||||
t["rss_human"] = f"{b} B"
|
||||
elif b < 1048576:
|
||||
t["rss_human"] = f"{b/1024:.1f} KiB"
|
||||
elif b < 1073741824:
|
||||
t["rss_human"] = f"{b/1048576:.1f} MiB"
|
||||
else:
|
||||
t["rss_human"] = f"{b/1073741824:.2f} GiB"
|
||||
except Exception:
|
||||
pass
|
||||
return mem
|
||||
|
||||
|
||||
def merge(stats: Dict[str, Any], resources: Dict[str, Any]) -> Dict[str, Any]:
|
||||
perf = stats.setdefault("performance", {})
|
||||
if not isinstance(perf, dict):
|
||||
perf = {}
|
||||
stats["performance"] = perf
|
||||
|
||||
if resources.get("loadavg"):
|
||||
perf["loadavg"] = resources["loadavg"]
|
||||
perf["loadavg_source"] = "container"
|
||||
|
||||
mem = resources.get("memory")
|
||||
if isinstance(mem, dict) and mem:
|
||||
perf["memory"] = compact_memory(dict(mem))
|
||||
perf["memory"]["source"] = resources.get("source") or "mem-snapshot"
|
||||
return stats
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("stats")
|
||||
ap.add_argument("resources")
|
||||
ap.add_argument("-o", "--out", default="")
|
||||
args = ap.parse_args()
|
||||
|
||||
stats = json.loads(Path(args.stats).read_text(encoding="utf-8"))
|
||||
resources = json.loads(Path(args.resources).read_text(encoding="utf-8"))
|
||||
if not isinstance(stats, dict) or not stats.get("ok"):
|
||||
print("skip: stats not ok", file=sys.stderr)
|
||||
return 1
|
||||
if not isinstance(resources, dict) or resources.get("ok") is False:
|
||||
print("skip: resources not ok", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
merge(stats, resources)
|
||||
out = Path(args.out) if args.out else Path(args.stats)
|
||||
tmp = out.with_suffix(out.suffix + f".tmp.{os.getpid()}")
|
||||
tmp.write_text(json.dumps(stats, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||
tmp.replace(out)
|
||||
mem = (stats.get("performance") or {}).get("memory") or {}
|
||||
print(
|
||||
f"merged memory container={mem.get('container_rss_human')} "
|
||||
f"pg={mem.get('postgres_rss_human')} loadavg={stats.get('performance', {}).get('loadavg')}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue