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