koopa-admin-log/scripts/taler-landing/merge_resources.py
Hernâni Marques fb1a0b0b8e
landing: kGOA units, full money stats, rootless resource display
Public bank/exchange/merchant landing stats and performance UI only.
Secret goa-ui overlay stays on local/merchant-secret-ui (never push).
2026-07-17 18:13:13 +02:00

140 lines
5 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"]
# rootless: /proc/loadavg is almost always the host's
perf["loadavg_source"] = resources.get("loadavg_source") or "host_via_container_proc"
if resources.get("loadavg_note"):
perf["loadavg_note"] = resources["loadavg_note"]
if resources.get("cpu_percent") is not None:
perf["cpu_percent"] = resources["cpu_percent"]
perf["cpu_percent_label"] = resources.get("cpu_percent_label") or (
f'{resources["cpu_percent"]:.2f}%'
)
if resources.get("pids") is not None:
perf["pids"] = resources["pids"]
mem = resources.get("memory")
if isinstance(mem, dict) and mem:
m = compact_memory(dict(mem))
# normalize empty groups for UI
for key in ("taler", "java", "nginx", "postgres", "redis", "other"):
try:
n = int(m.get(f"{key}_n") or 0)
b = int(m.get(f"{key}_rss_bytes") or 0)
except Exception:
n, b = 0, 0
if n <= 0 or b <= 0:
m[f"{key}_n"] = 0
m[f"{key}_rss_bytes"] = 0
m[f"{key}_rss_human"] = None
h = m.get("container_rss_human") or ""
has_cgroup = bool(m.get("cgroup_bytes"))
if m.get("cgroup_limit_human") and h != "":
m["container_rss_label"] = f"{h} / {m['cgroup_limit_human']}"
m["memory_basis"] = "cgroup"
elif not has_cgroup and h != "":
m["container_rss_label"] = f"{h} (proc)"
m["memory_basis"] = m.get("memory_basis") or "proc_rss_sum"
elif not m.get("container_rss_label"):
m["container_rss_label"] = h
m["source"] = resources.get("source") or "mem-snapshot"
perf["memory"] = m
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())