|
|
@@ -0,0 +1,104 @@
|
|
|
+#!/usr/bin/env python3
|
|
|
+"""采集本机运行指标 → JSON (跨平台: Linux/macOS/Windows-WSL)"""
|
|
|
+import json, os, subprocess, datetime, socket, sys
|
|
|
+
|
|
|
+WORK = os.path.expanduser("~/.fmode-harness-agent")
|
|
|
+
|
|
|
+def sh(cmd, timeout=15):
|
|
|
+ try:
|
|
|
+ return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout).stdout.strip()
|
|
|
+ except Exception as e:
|
|
|
+ return f"ERR:{e}"
|
|
|
+
|
|
|
+def collect():
|
|
|
+ now = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+08:00")
|
|
|
+ data = {
|
|
|
+ "timestamp": now,
|
|
|
+ "agent": os.environ.get("AGENT_NAME", socket.gethostname()),
|
|
|
+ "host": {
|
|
|
+ "platform": sys.platform,
|
|
|
+ "hostname": socket.gethostname(),
|
|
|
+ },
|
|
|
+ "resources": {},
|
|
|
+ "concurrency": {},
|
|
|
+ "services": {},
|
|
|
+ "events": {}
|
|
|
+ }
|
|
|
+
|
|
|
+ # --- CPU/内存 (跨平台: /proc/meminfo + uptime) ---
|
|
|
+ try:
|
|
|
+ meminfo = {}
|
|
|
+ for line in open("/proc/meminfo"):
|
|
|
+ k, v = line.split(":")
|
|
|
+ meminfo[k.strip()] = int(v.strip().split()[0]) # kB
|
|
|
+ total = meminfo["MemTotal"]
|
|
|
+ avail = meminfo["MemAvailable"]
|
|
|
+ data["resources"]["memory"] = {
|
|
|
+ "total_mb": round(total / 1024),
|
|
|
+ "used_mb": round((total - avail) / 1024),
|
|
|
+ "percent": round((total - avail) / total * 100, 1)
|
|
|
+ }
|
|
|
+ load = open("/proc/loadavg").read().split()
|
|
|
+ data["resources"]["cpu_load"] = {"m1": float(load[0]), "m5": float(load[1]), "m15": float(load[2])}
|
|
|
+ # CPU%
|
|
|
+ cpu = sh("top -bn1 | grep 'Cpu(s)' | awk '{print $2+$4}'")
|
|
|
+ data["resources"]["cpu_percent"] = round(float(cpu), 1) if cpu and not cpu.startswith("ERR") else None
|
|
|
+ except Exception as e:
|
|
|
+ data["resources"]["error"] = str(e)[:100]
|
|
|
+
|
|
|
+ # --- 磁盘 (工作区所在分区) ---
|
|
|
+ disk = sh(f"df -B1 {WORK} | tail -1 | awk '{{print $2, $3, $5}}'")
|
|
|
+ if disk and not disk.startswith("ERR"):
|
|
|
+ t, u, p = disk.split()
|
|
|
+ data["resources"]["disk"] = {"total_gb": round(int(t)/1e9,1), "used_gb": round(int(u)/1e9,1), "percent": p}
|
|
|
+
|
|
|
+ # --- 网络 (累计上下行, 取主网卡) ---
|
|
|
+ net = sh("cat /proc/net/dev | awk 'NR>2 && $1!~/lo:/ {gsub(\":\",\"\",$1); print $1, $2, $10}' | sort -k2 -rn | head -1")
|
|
|
+ if net and not net.startswith("ERR"):
|
|
|
+ iface, rx, tx = net.split()
|
|
|
+ data["resources"]["network"] = {"iface": iface, "rx_total_mb": round(int(rx)/1e6,1), "tx_total_mb": round(int(tx)/1e6,1)}
|
|
|
+
|
|
|
+ # --- GPU (有则记) ---
|
|
|
+ gpu = sh("nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv,noheader,nounits 2>/dev/null")
|
|
|
+ if gpu:
|
|
|
+ u, mu, mt = gpu.split(", ")
|
|
|
+ data["resources"]["gpu"] = {"percent": int(u), "mem_used_mb": int(mu), "mem_total_mb": int(mt)}
|
|
|
+
|
|
|
+ # --- 并发: profile / session / subagent ---
|
|
|
+ profiles = sh("ls /opt/data/profiles/ 2>/dev/null | wc -l")
|
|
|
+ data["concurrency"]["profiles_total"] = int(profiles) if profiles.isdigit() else None
|
|
|
+ # 活跃session(hermes state.db最近30分钟)
|
|
|
+ try:
|
|
|
+ import sqlite3
|
|
|
+ conn = sqlite3.connect("file:/opt/data/state.db?mode=ro", uri=True, timeout=5)
|
|
|
+ cur = conn.cursor()
|
|
|
+ cur.execute("SELECT COUNT(DISTINCT session_id) FROM messages WHERE timestamp > ?", (time.time()-1800,))
|
|
|
+ data["concurrency"]["active_sessions_30min"] = cur.fetchone()[0]
|
|
|
+ cur.execute("SELECT COUNT(*) FROM async_delegations WHERE status='running'")
|
|
|
+ data["concurrency"]["running_subagents"] = cur.fetchone()[0]
|
|
|
+ conn.close()
|
|
|
+ except Exception:
|
|
|
+ data["concurrency"]["active_sessions_30min"] = None
|
|
|
+
|
|
|
+ # --- 服务存活 ---
|
|
|
+ for svc, pat in [("gateway", "hermes gateway"), ("dashboard", "hermes-dashboard"), ("studio", "fmode-studio")]:
|
|
|
+ out = sh(f"pgrep -f '{pat}' | wc -l")
|
|
|
+ data["services"][svc] = "up" if out.isdigit() and int(out) > 0 else "down"
|
|
|
+
|
|
|
+ # --- 事件: 最近1小时错误日志计数 ---
|
|
|
+ errc = sh("find /opt/data/logs -name '*.log' -mmin -60 -exec grep -ci 'error\\|fatal' {} + 2>/dev/null | awk -F: '{s+=$2} END {print s}'")
|
|
|
+ data["events"]["errors_last_1h"] = int(errc) if errc.isdigit() else None
|
|
|
+ rest = sh("grep -c 'Reconnected' /opt/data/logs/gateway.log 2>/dev/null")
|
|
|
+ data["events"]["wecom_reconnects_total"] = int(rest) if rest.isdigit() else None
|
|
|
+
|
|
|
+ return data
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ import time
|
|
|
+ out = os.path.join(WORK, "tmp")
|
|
|
+ os.makedirs(out, exist_ok=True)
|
|
|
+ d = collect()
|
|
|
+ path = os.path.join(out, "agent-log.json")
|
|
|
+ json.dump(d, open(path, "w"), ensure_ascii=False, indent=2)
|
|
|
+ print(json.dumps(d, ensure_ascii=False)[:300])
|
|
|
+ print(f"\nsaved: {path}")
|