Files

111 lines
5.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()))