"""Simple system metrics collector for VoIdea VPS. Runs as a periodic task (Celery or cron) and logs: - CPU load - Memory usage - Disk usage - PostgreSQL connection count """ import json import logging import os import platform import shutil import subprocess import sys from datetime import datetime, timezone logger = logging.getLogger("voidea.metrics") def get_cpu_load() -> float: """Return CPU load average (1 min) or 0.0.""" try: if platform.system() == "Linux": load1, _, _ = os.getloadavg() return round(load1, 2) return 0.0 except OSError: return 0.0 def get_memory_usage() -> dict: """Return memory usage in MB.""" try: import psutil mem = psutil.virtual_memory() return { "total_mb": round(mem.total / 1024 / 1024, 1), "available_mb": round(mem.available / 1024 / 1024, 1), "percent_used": mem.percent, } except ImportError: return {"error": "psutil not installed"} def get_disk_usage(path: str = "/") -> dict: """Return disk usage for the given path.""" try: usage = shutil.disk_usage(path) return { "total_gb": round(usage.total / 1024**3, 1), "used_gb": round(usage.used / 1024**3, 1), "free_gb": round(usage.free / 1024**3, 1), "percent_used": round(usage.used / usage.total * 100, 1), } except OSError: return {"error": f"cannot access {path}"} def get_pg_connections(db_url: str | None = None) -> int: """Return active PostgreSQL connection count.""" url = db_url or os.environ.get("DATABASE_URL", "") if not url: return -1 try: parts = url.replace("postgresql://", "").replace("postgres://", "").split("@") user_pass = parts[0].split(":") host_db = parts[1].split("/") host_port = host_db[0].split(":") config = { "host": host_port[0], "port": host_port[1] if len(host_port) > 1 else "5432", "user": user_pass[0], "dbname": host_db[1].split("?")[0], } env = os.environ.copy() env["PGPASSWORD"] = user_pass[1] if len(user_pass) > 1 else "" result = subprocess.run( [ "psql", "-h", config["host"], "-p", config["port"], "-U", config["user"], "-d", config["dbname"], "-tA", "-c", "SELECT count(*) FROM pg_stat_activity;", ], env=env, capture_output=True, text=True, timeout=5, ) return int(result.stdout.strip()) except (ValueError, subprocess.TimeoutExpired, OSError): return -1 def collect() -> dict: """Collect all metrics into a single dict.""" return { "timestamp": datetime.now(timezone.utc).isoformat(), "hostname": platform.node(), "cpu_load_1min": get_cpu_load(), "memory": get_memory_usage(), "disk": get_disk_usage("/"), "pg_connections": get_pg_connections(), } def main(): logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") metrics = collect() log_line = json.dumps(metrics, ensure_ascii=False) logger.info("Metrics: %s", log_line) # Optionally write to a file for prometheus/node_exporter metrics_file = os.environ.get("METRICS_FILE", "") if metrics_file: os.makedirs(os.path.dirname(metrics_file), exist_ok=True) with open(metrics_file, "w", encoding="utf-8") as f: f.write(log_line + "\n") if __name__ == "__main__": main()