182 lines
6.0 KiB
Python
182 lines
6.0 KiB
Python
"""Background metrics collector for the AYN Antivirus dashboard."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
import random
|
|
from datetime import datetime
|
|
from typing import Any, Dict, Optional
|
|
|
|
import psutil
|
|
|
|
from ayn_antivirus.constants import DASHBOARD_COLLECTOR_INTERVAL
|
|
|
|
logger = logging.getLogger("ayn_antivirus.dashboard.collector")
|
|
|
|
|
|
class MetricsCollector:
|
|
"""Periodically sample system metrics and store them in the dashboard DB.
|
|
|
|
Parameters
|
|
----------
|
|
store:
|
|
A :class:`DashboardStore` instance to write metrics into.
|
|
interval:
|
|
Seconds between samples.
|
|
"""
|
|
|
|
def __init__(self, store: Any, interval: int = DASHBOARD_COLLECTOR_INTERVAL) -> None:
|
|
self.store = store
|
|
self.interval = interval
|
|
self._task: Optional[asyncio.Task] = None
|
|
self._running = False
|
|
|
|
async def start(self) -> None:
|
|
"""Begin collecting metrics on a background asyncio task."""
|
|
self._running = True
|
|
self._task = asyncio.create_task(self._collect_loop())
|
|
logger.info("Metrics collector started (interval=%ds)", self.interval)
|
|
|
|
async def stop(self) -> None:
|
|
"""Cancel the background task and wait for it to finish."""
|
|
self._running = False
|
|
if self._task:
|
|
self._task.cancel()
|
|
try:
|
|
await self._task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
logger.info("Metrics collector stopped")
|
|
|
|
# ------------------------------------------------------------------
|
|
# Internal loop
|
|
# ------------------------------------------------------------------
|
|
|
|
async def _collect_loop(self) -> None:
|
|
while self._running:
|
|
try:
|
|
await asyncio.to_thread(self._sample)
|
|
except Exception as exc:
|
|
logger.error("Collector error: %s", exc)
|
|
await asyncio.sleep(self.interval)
|
|
|
|
def _sample(self) -> None:
|
|
"""Take a single metric snapshot and persist it."""
|
|
cpu = psutil.cpu_percent(interval=1)
|
|
mem = psutil.virtual_memory()
|
|
|
|
disks = []
|
|
for part in psutil.disk_partitions(all=False):
|
|
try:
|
|
usage = psutil.disk_usage(part.mountpoint)
|
|
disks.append({
|
|
"mount": part.mountpoint,
|
|
"device": part.device,
|
|
"total": usage.total,
|
|
"used": usage.used,
|
|
"free": usage.free,
|
|
"percent": usage.percent,
|
|
})
|
|
except (PermissionError, OSError):
|
|
continue
|
|
|
|
try:
|
|
load = list(os.getloadavg())
|
|
except (OSError, AttributeError):
|
|
load = [0.0, 0.0, 0.0]
|
|
|
|
try:
|
|
net_conns = len(psutil.net_connections(kind="inet"))
|
|
except (psutil.AccessDenied, OSError):
|
|
net_conns = 0
|
|
|
|
self.store.record_metric(
|
|
cpu=cpu,
|
|
mem_pct=mem.percent,
|
|
mem_used=mem.used,
|
|
mem_total=mem.total,
|
|
disk_usage=disks,
|
|
load_avg=load,
|
|
net_conns=net_conns,
|
|
)
|
|
|
|
# Periodic cleanup (~1 in 100 samples).
|
|
if random.randint(1, 100) == 1:
|
|
self.store.cleanup_old_metrics()
|
|
|
|
# ------------------------------------------------------------------
|
|
# One-shot snapshot (no storage)
|
|
# ------------------------------------------------------------------
|
|
|
|
@staticmethod
|
|
def get_snapshot() -> Dict[str, Any]:
|
|
"""Return a live system snapshot without persisting it."""
|
|
cpu = psutil.cpu_percent(interval=0.1)
|
|
cpu_per_core = psutil.cpu_percent(interval=0.1, percpu=True)
|
|
cpu_freq = psutil.cpu_freq(percpu=False)
|
|
mem = psutil.virtual_memory()
|
|
swap = psutil.swap_memory()
|
|
|
|
disks = []
|
|
for part in psutil.disk_partitions(all=False):
|
|
try:
|
|
usage = psutil.disk_usage(part.mountpoint)
|
|
disks.append({
|
|
"mount": part.mountpoint,
|
|
"device": part.device,
|
|
"total": usage.total,
|
|
"used": usage.used,
|
|
"percent": usage.percent,
|
|
})
|
|
except (PermissionError, OSError):
|
|
continue
|
|
|
|
try:
|
|
load = list(os.getloadavg())
|
|
except (OSError, AttributeError):
|
|
load = [0.0, 0.0, 0.0]
|
|
|
|
try:
|
|
net_conns = len(psutil.net_connections(kind="inet"))
|
|
except (psutil.AccessDenied, OSError):
|
|
net_conns = 0
|
|
|
|
# Top processes by CPU
|
|
top_procs = []
|
|
try:
|
|
for p in sorted(psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']),
|
|
key=lambda x: x.info.get('cpu_percent', 0) or 0, reverse=True)[:8]:
|
|
info = p.info
|
|
if (info.get('cpu_percent') or 0) > 0.1:
|
|
top_procs.append({
|
|
"pid": info['pid'],
|
|
"name": info['name'] or '?',
|
|
"cpu": round(info.get('cpu_percent', 0) or 0, 1),
|
|
"mem": round(info.get('memory_percent', 0) or 0, 1),
|
|
})
|
|
except Exception:
|
|
pass
|
|
|
|
return {
|
|
"cpu_percent": cpu,
|
|
"cpu_per_core": cpu_per_core,
|
|
"cpu_cores": psutil.cpu_count(logical=True),
|
|
"cpu_freq_mhz": round(cpu_freq.current) if cpu_freq else 0,
|
|
"mem_percent": mem.percent,
|
|
"mem_used": mem.used,
|
|
"mem_total": mem.total,
|
|
"mem_available": mem.available,
|
|
"mem_cached": getattr(mem, 'cached', 0),
|
|
"mem_buffers": getattr(mem, 'buffers', 0),
|
|
"swap_percent": swap.percent,
|
|
"swap_used": swap.used,
|
|
"swap_total": swap.total,
|
|
"disk_usage": disks,
|
|
"load_avg": load,
|
|
"net_connections": net_conns,
|
|
"top_processes": top_procs,
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
}
|