"""Small read-only host snapshot; requires /proc and /sys mounted by the admin.""" import json import os import re import time from pathlib import Path def read_snapshot(proc=Path("/host/proc"), sys=Path("/host/sys")): if not (proc / "stat").is_file(): return {"available": False, "message": "请为主容器挂载 /proc:/host/proc:ro 和 /sys:/host/sys:ro;Worker 不需要挂载。"} cpu = next(line for line in (proc / "stat").read_text().splitlines() if line.startswith("cpu ")) fields = [int(v) for v in cpu.split()[1:9]] # guest times are already included in user/nice mem = {} for line in (proc / "meminfo").read_text().splitlines(): k, value = line.split(":", 1) mem[k] = int(value.split()[0]) * 1024 disks = {} for line in (proc / "diskstats").read_text().splitlines(): v = line.split() if len(v) < 14 or re.match(r"^(loop|ram|fd|sr)", v[2]): continue # Whole devices only: including partitions would double-count disk IO. if (sys / "class/block" / v[2] / "partition").exists(): continue disks[v[2]] = {"read_bytes": int(v[5]) * 512, "write_bytes": int(v[9]) * 512, "io_ms": int(v[12]), "inflight": int(v[11])} interfaces = {} network_error = None try: # /proc/1 is the host PID namespace; /proc/net would resolve to this container. for line in (proc / "1/net/dev").read_text().splitlines()[2:]: name, values = line.split(":", 1) name = name.strip() if name == "lo" or name.startswith(("veth", "docker", "br-", "virbr", "tun", "tap")): continue v = values.split() speed = None try: speed = int((sys / "class/net" / name / "speed").read_text()) except (OSError, ValueError): pass interfaces[name] = {"rx_bytes": int(v[0]), "tx_bytes": int(v[8]), "rx_errors": int(v[2]), "tx_errors": int(v[10]), "link_mbps": speed if speed and speed > 0 else None} except OSError: network_error = "无法读取宿主机网络命名空间,检查 /host/proc 挂载及读取权限" pressure = {} for name in ("cpu", "memory", "io"): try: pressure[name] = (proc / "pressure" / name).read_text().strip()[:400] except OSError: pressure[name] = None processes = {} page_size = getattr(os, "sysconf", lambda _: 4096)("SC_PAGE_SIZE") for item in proc.iterdir(): if not item.name.isdigit(): continue if len(processes) >= 5000: break try: stat = (item / "stat").read_text() end = stat.rfind(")") process_fields = stat[end + 2:].split() processes[item.name] = {"name": stat[stat.find("(") + 1:end][:80], "ticks": int(process_fields[11]) + int(process_fields[12]), "started": int(process_fields[19]), "rss_bytes": max(0, int(process_fields[21])) * page_size} except (OSError, ValueError, IndexError): continue return {"available": True, "at": time.time(), "cpu_total": sum(fields), "cpu_idle": fields[3] + fields[4], "cpu_iowait": fields[4], "cpu_count": sum(line.startswith("cpu") and line[3:4].isdigit() for line in (proc / "stat").read_text().splitlines()), "memory_total": mem.get("MemTotal", 0), "memory_available": mem.get("MemAvailable", mem.get("MemFree", 0)), "swap_total": mem.get("SwapTotal", 0), "swap_free": mem.get("SwapFree", 0), "load": [float(v) for v in (proc / "loadavg").read_text().split()[:3]], "disks": disks, "interfaces": interfaces, "network_error": network_error, "pressure": pressure, "process_counters": processes, "clock_ticks": getattr(os, "sysconf", lambda _: 100)("SC_CLK_TCK")} def rates(current, previous): result = dict(current) if not current.get("available"): return result result.update(cpu_percent=None, iowait_percent=None) elapsed = current["at"] - previous["at"] if previous and previous.get("available") else 0 if elapsed > 0: delta = current["cpu_total"] - previous["cpu_total"] if delta > 0: result["cpu_percent"] = round(100 * (1 - (current["cpu_idle"] - previous["cpu_idle"]) / delta), 2) result["iowait_percent"] = round(100 * (current["cpu_iowait"] - previous["cpu_iowait"]) / delta, 2) top = [] for pid, item in current.get("process_counters", {}).items(): old = previous.get("process_counters", {}).get(pid) if elapsed > 0 else None if old and old["started"] == item["started"]: percent = max(0, (item["ticks"] - old["ticks"]) / (current.get("clock_ticks", 100) * elapsed) * 100) top.append({"pid": int(pid), "name": item["name"], "cpu_percent": round(percent, 2), "rss_bytes": item["rss_bytes"]}) result.pop("process_counters", None) result["top_processes"] = sorted(top, key=lambda p: p["cpu_percent"], reverse=True)[:20] for name, item in result["disks"].items(): old = previous.get("disks", {}).get(name) if elapsed > 0 else None item.update(read_bps=max(0, (item["read_bytes"] - old["read_bytes"]) / elapsed) if old else None, write_bps=max(0, (item["write_bytes"] - old["write_bytes"]) / elapsed) if old else None, busy_percent=min(100, max(0, (item["io_ms"] - old["io_ms"]) / (elapsed * 10))) if old else None) for name, item in result["interfaces"].items(): old = previous.get("interfaces", {}).get(name) if elapsed > 0 else None item.update(rx_bps=max(0, (item["rx_bytes"] - old["rx_bytes"]) / elapsed) if old else None, tx_bps=max(0, (item["tx_bytes"] - old["tx_bytes"]) / elapsed) if old else None) return result if __name__ == "__main__": print(json.dumps(read_snapshot()))