koopa-admin-log/scripts/taler-landing/merge_resources.py
Hernâni Marques 78bbd80d1f
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.
2026-07-17 08:55:00 +02:00

106 lines
3.6 KiB
Python
Executable file

#!/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())