"""System metrics collector.""" from pathlib import Path from typing import Any from lib import schema from lib.state import _now_iso, register_collector, register_volatile def _parse_meminfo() -> dict[str, Any]: """Read /proc/meminfo and return dict with key memory stats in bytes.""" info: dict[str, int] = {} try: for line in Path("/proc/meminfo").read_text().splitlines(): if ":" not in line: continue key, value = line.split(":", 1) key = key.strip() parts = value.strip().split() val = int(parts[0]) # Convert kB to bytes if parts and parts[-1] == "kB": val *= 1024 info[key] = val except (OSError, ValueError): return {} return info def _collect_system() -> schema.SystemState: """Collect system-wide metrics: CPU load, memory, network traffic. Reads from /proc and /sys — no subprocess needed. Returns: Dict with load (1/5/15 min), memory usage, and per-interface traffic. """ # CPU load loads = [] try: parts = Path("/proc/loadavg").read_text().split() loads = [float(x) for x in parts[:3]] except (OSError, ValueError): loads = [0.0, 0.0, 0.0] # Memory meminfo_raw = _parse_meminfo() mem_total = meminfo_raw.get("MemTotal", 0) mem_free = meminfo_raw.get("MemFree", 0) mem_available = meminfo_raw.get("MemAvailable", mem_free) mem_buffers = meminfo_raw.get("Buffers", 0) mem_cached = meminfo_raw.get("Cached", 0) mem_used = mem_total - mem_free - mem_buffers - mem_cached if mem_used < 0: mem_used = mem_total - mem_available # Swap swap_total = meminfo_raw.get("SwapTotal", 0) swap_free = meminfo_raw.get("SwapFree", 0) swap_used = swap_total - swap_free # Network traffic from /sys/class/net//statistics/ traffic: dict[str, dict[str, int]] = {} try: net_root = Path("/sys/class/net") if net_root.is_dir(): for iface_dir in net_root.iterdir(): stats_dir = iface_dir / "statistics" if not stats_dir.is_dir(): continue iface_name = iface_dir.name rx_bytes = 0 tx_bytes = 0 rx_packets = 0 tx_packets = 0 try: rx_bytes = int((stats_dir / "rx_bytes").read_text().strip()) tx_bytes = int((stats_dir / "tx_bytes").read_text().strip()) rx_packets = int((stats_dir / "rx_packets").read_text().strip()) tx_packets = int((stats_dir / "tx_packets").read_text().strip()) except (OSError, ValueError): continue traffic[iface_name] = { "rx_bytes": rx_bytes, "tx_bytes": tx_bytes, "rx_packets": rx_packets, "tx_packets": tx_packets, } except OSError: pass return { "load": { "load1": loads[0], "load5": loads[1], "load15": loads[2], }, "memory": { "total": mem_total, "available": mem_available, "used": mem_used, "used_pct": round(mem_used / mem_total * 100, 1) if mem_total > 0 else 0, }, "swap": { "total": swap_total, "used": swap_used, "used_pct": round(swap_used / swap_total * 100, 1) if swap_total > 0 else 0, }, "traffic": traffic, "timestamp": _now_iso(), } register_collector("system", _collect_system) register_volatile( "system", frozenset( { "load", "memory", "swap", "traffic", } ), )