Files
virtuality/web/metrics.py
T
Claude 374bcbb29f Производительность панели была улучшена
Добавлен virt_backend: одно постоянное libvirt-соединение вместо
десятков вызовов virsh на каждую страницу, с автоматическим fallback
на virsh, если libvirt-python недоступен. Обзорные запросы (список VM,
пулы, сервисы) закешированы с коротким TTL и сбросом после действий.
Появился фоновый сборщик метрик хоста и спарклайны CPU/RAM на дашборде.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKUoZwHMCHnytfswFVRUHw
2026-08-15 19:23:14 +00:00

116 строки
3.5 KiB
Python

#!/usr/bin/env python3
"""Лёгкий сборщик метрик хоста: кольцевой буфер в памяти, без БД и зависимостей.
Раз в METRICS_INTERVAL секунд снимаются CPU, память и load average хоста
плюс число работающих VM. Буфер хранит около часа истории для спарклайнов
на дашборде. Стоимость — одно чтение /proc и один вызов libvirt за цикл.
"""
import threading
import time
from collections import deque
from pathlib import Path
from typing import Any
import virt_backend
METRICS_INTERVAL = 15
SAMPLES = deque(maxlen=240) # ~1 час при интервале 15 секунд
_LOCK = threading.Lock()
_STARTED = threading.Event()
_prev_cpu: tuple[int, int] | None = None
def _read_cpu_times() -> tuple[int, int] | None:
"""(busy, total) джиффи из /proc/stat."""
try:
fields = Path("/proc/stat").read_text().splitlines()[0].split()[1:]
values = [int(v) for v in fields[:8]]
idle = values[3] + values[4] # idle + iowait
total = sum(values)
return total - idle, total
except Exception:
return None
def _cpu_percent() -> float:
global _prev_cpu
current = _read_cpu_times()
if current is None:
return 0.0
if _prev_cpu is None:
_prev_cpu = current
return 0.0
busy = current[0] - _prev_cpu[0]
total = current[1] - _prev_cpu[1]
_prev_cpu = current
if total <= 0:
return 0.0
return round(100.0 * busy / total, 1)
def _memory_mb() -> tuple[int, int]:
"""(used_mb, total_mb) из /proc/meminfo."""
try:
info = {}
for line in Path("/proc/meminfo").read_text().splitlines():
key, _, rest = line.partition(":")
info[key] = int(rest.split()[0])
total = info.get("MemTotal", 0) // 1024
available = info.get("MemAvailable", 0) // 1024
return max(0, total - available), total
except Exception:
return 0, 0
def _load_average() -> float:
try:
return float(Path("/proc/loadavg").read_text().split()[0])
except Exception:
return 0.0
def _running_vms() -> int:
stats = virt_backend.domain_stats()
if stats is not None:
return len(stats)
return -1 # backend недоступен — не считаем через subprocess, чтобы не грузить хост
def collect_sample() -> dict[str, Any]:
used_mb, total_mb = _memory_mb()
return {
"ts": int(time.time()),
"cpu": _cpu_percent(),
"mem_used_mb": used_mb,
"mem_total_mb": total_mb,
"mem_percent": round(100.0 * used_mb / total_mb, 1) if total_mb else 0.0,
"load1": _load_average(),
"vms_running": _running_vms(),
}
def _worker() -> None:
_cpu_percent() # первая выборка задаёт базу для дельты
while True:
sample = collect_sample()
with _LOCK:
SAMPLES.append(sample)
time.sleep(METRICS_INTERVAL)
def start() -> None:
if _STARTED.is_set():
return
_STARTED.set()
threading.Thread(target=_worker, daemon=True, name="virtuality-metrics").start()
def snapshot(limit: int = 240) -> dict[str, Any]:
with _LOCK:
series = list(SAMPLES)[-limit:]
return {
"interval": METRICS_INTERVAL,
"series": series,
"current": series[-1] if series else None,
}