127 lines
3.6 KiB
Python
127 lines
3.6 KiB
Python
"""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()
|