"""Host and DB metrics for SAC sidebar (best-effort, no heavy queries).""" from __future__ import annotations import os import shutil import time from datetime import datetime, timedelta, timezone from pathlib import Path from sqlalchemy import func, select, text from sqlalchemy.orm import Session from app.models import Event, Problem def _disk_path() -> str: explicit = os.environ.get("SAC_STATS_DISK_PATH", "").strip() if explicit: return explicit return str(Path("/").resolve()) def _read_cpu_percent() -> float | None: try: import psutil # type: ignore[import-untyped] return round(float(psutil.cpu_percent(interval=None)), 1) except Exception: return None def _read_memory() -> dict[str, float | int] | None: try: import psutil # type: ignore[import-untyped] vm = psutil.virtual_memory() return { "used_mb": int(vm.used / (1024 * 1024)), "total_mb": int(vm.total / (1024 * 1024)), "percent": round(float(vm.percent), 1), } except Exception: return None def _read_disk(path: str) -> dict[str, float | int | str] | None: try: usage = shutil.disk_usage(path) total = int(usage.total) used = int(usage.used) if total <= 0: return None return { "path": path, "used_gb": round(used / (1024**3), 1), "total_gb": round(total / (1024**3), 1), "percent": round(100.0 * used / total, 1), } except Exception: return None def collect_system_stats(db: Session) -> dict: collected_at = datetime.now(timezone.utc).isoformat() disk_path = _disk_path() db_status = "ok" db_latency_ms: float | None = None db_connections: int | None = None t0 = time.perf_counter() try: db.execute(text("SELECT 1")) db_latency_ms = round((time.perf_counter() - t0) * 1000, 1) try: db_connections = int( db.scalar( text( "SELECT count(*) FROM pg_stat_activity " "WHERE datname = current_database() AND pid <> pg_backend_pid()" ) ) or 0 ) except Exception: db_connections = None except Exception: db_status = "error" since = datetime.now(timezone.utc) - timedelta(hours=1) events_last_hour = 0 problems_open = 0 if db_status == "ok": events_last_hour = int( db.scalar(select(func.count()).select_from(Event).where(Event.received_at >= since)) or 0 ) problems_open = int( db.scalar(select(func.count()).select_from(Problem).where(Problem.status == "open")) or 0 ) return { "collected_at": collected_at, "cpu_percent": _read_cpu_percent(), "memory": _read_memory(), "disk": _read_disk(disk_path), "database": { "status": db_status, "latency_ms": db_latency_ms, "active_connections": db_connections, }, "app": { "events_last_hour": events_last_hour, "problems_open": problems_open, }, }