111 lines
5.9 KiB
Python
111 lines
5.9 KiB
Python
"""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()))
|