"""System stats endpoint and background WebSocket broadcaster.""" import asyncio import logging import time import psutil from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect from backend.auth import get_current_user, ws_authenticate from backend.config import STATS_INTERVAL from backend.ws.manager import manager router = APIRouter(prefix="/api/stats", tags=["stats"]) log = logging.getLogger("atlus.stats") def _gather_stats() -> dict: """Collect current system stats via psutil.""" cpu_percent = psutil.cpu_percent(interval=0) mem = psutil.virtual_memory() disk = psutil.disk_usage("/") # CPU temperature — varies by platform temps = psutil.sensors_temperatures() if hasattr(psutil, "sensors_temperatures") else {} cpu_temp = None for name in ("cpu_thermal", "coretemp", "cpu-thermal", "soc_thermal"): if name in temps and temps[name]: cpu_temp = temps[name][0].current break if cpu_temp is None and temps: first = next(iter(temps.values())) if first: cpu_temp = first[0].current # Network interfaces — filter out virtual/tunnel/loopback _SKIP_PREFIXES = ("lo", "utun", "anpi", "bridge", "awdl", "llw", "gif", "stf", "ap", "veth", "docker", "br-") addrs = psutil.net_if_addrs() net_stats = psutil.net_if_stats() interfaces = {} for iface, addr_list in addrs.items(): if any(iface.startswith(p) for p in _SKIP_PREFIXES): continue info: dict = {"up": net_stats.get(iface, None) is not None and net_stats[iface].isup} for a in addr_list: if a.family.name == "AF_INET": info["ipv4"] = a.address elif a.family.name == "AF_INET6" and not a.address.startswith("fe80"): info["ipv6"] = a.address interfaces[iface] = info # Network I/O net_io = psutil.net_io_counters() return { "ts": time.time(), "cpu_percent": cpu_percent, "memory": { "total": mem.total, "used": mem.used, "percent": mem.percent, }, "disk": { "total": disk.total, "used": disk.used, "percent": disk.percent, }, "cpu_temp": cpu_temp, "network": { "interfaces": interfaces, "bytes_sent": net_io.bytes_sent, "bytes_recv": net_io.bytes_recv, }, "uptime": time.time() - psutil.boot_time(), "load_avg": list(psutil.getloadavg()) if hasattr(psutil, "getloadavg") else None, } # --------------------------------------------------------------------------- # REST endpoint — one-shot stats # --------------------------------------------------------------------------- @router.get("") async def get_stats(_user: str = Depends(get_current_user)): return _gather_stats() # --------------------------------------------------------------------------- # WebSocket — live push # --------------------------------------------------------------------------- @router.websocket("/ws") async def stats_ws(websocket: WebSocket): username = await ws_authenticate(websocket) await manager.connect(websocket, channel="stats") try: while True: data = _gather_stats() await manager.send_personal(websocket, data) await asyncio.sleep(STATS_INTERVAL) except (WebSocketDisconnect, RuntimeError): pass except Exception: log.exception("stats ws error") finally: manager.disconnect(websocket, channel="stats") # --------------------------------------------------------------------------- # Background broadcaster (called from main.py lifespan) # --------------------------------------------------------------------------- async def stats_broadcaster(): """Push stats to all connected clients on the 'stats' channel.""" while True: try: data = _gather_stats() await manager.broadcast(data, channel="stats") except Exception: log.exception("broadcast error") await asyncio.sleep(STATS_INTERVAL)