hermes-hub/src/antigravity_provider/router/host_metrics.py

146 lines
5.5 KiB
Python

"""Hermes Hub — System Host Metrics Service using psutil.
Collects empirical host hardware indicators:
- CPU utilization percentage (warmed up on initial sample)
- RAM (used/total MB, percent)
- Disk (used/total GB, percent for system root)
- Network live speed (Mbps) and cumulative I/O counters (bytes sent/recv since boot)
Source: 'host_measurement'.
"""
from __future__ import annotations
import logging
import os
import threading
import time
from dataclasses import asdict, dataclass
from typing import Any, Dict, Optional
logger = logging.getLogger("hermes.router.host_metrics")
# Prime psutil internal CPU timer on module import
try:
import psutil
psutil.cpu_percent(interval=None)
except Exception:
pass
@dataclass
class HostMetricsSnapshot:
"""Empirical hardware telemetry of the host system."""
timestamp: float
cpu_percent: Optional[float] = None
memory_percent: Optional[float] = None
memory_used_mb: Optional[float] = None
memory_total_mb: Optional[float] = None
disk_percent: Optional[float] = None
disk_used_gb: Optional[float] = None
disk_total_gb: Optional[float] = None
net_speed_mbps: Optional[float] = None # Live total network speed (Mbps) between samples
net_sent_mbps: Optional[float] = None # Live upload speed (Mbps)
net_recv_mbps: Optional[float] = None # Live download speed (Mbps)
net_bytes_sent: Optional[int] = None # Cumulative bytes sent since host boot
net_bytes_recv: Optional[int] = None # Cumulative bytes received since host boot
source: str = "host_measurement"
has_data: bool = True
def to_dict(self) -> Dict[str, Any]:
return asdict(self)
class HostMetricsService:
"""Safe, non-blocking collector for host performance indicators."""
_last_cpu_time: float = 0.0
_last_net_time: float = 0.0
_last_net_bytes_sent: int = 0
_last_net_bytes_recv: int = 0
_lock = threading.Lock()
@classmethod
def reset_state(cls) -> None:
"""Reset internal sampling baselines (useful for testing)."""
with cls._lock:
cls._last_cpu_time = 0.0
cls._last_net_time = 0.0
cls._last_net_bytes_sent = 0
cls._last_net_bytes_recv = 0
@classmethod
def collect(cls) -> HostMetricsSnapshot:
now = time.time()
try:
import psutil
except ImportError:
return HostMetricsSnapshot(
timestamp=now,
source="host_measurement",
has_data=False,
)
try:
with cls._lock:
if cls._last_cpu_time == 0.0:
# Warm up CPU baseline with short interval to prevent cold start 0.0%
cpu = psutil.cpu_percent(interval=0.05)
cls._last_cpu_time = time.time()
else:
cpu = psutil.cpu_percent(interval=None)
cls._last_cpu_time = now
mem = psutil.virtual_memory()
# Disk usage of root drive / partition
root_path = os.path.abspath(os.sep)
disk = psutil.disk_usage(root_path)
net = None
net_speed_mbps = None
net_sent_mbps = None
net_recv_mbps = None
try:
net = psutil.net_io_counters()
if net:
with cls._lock:
if cls._last_net_time > 0.0 and now > cls._last_net_time:
dt = now - cls._last_net_time
if dt >= 0.05: # Valid time delta
d_sent = max(0, net.bytes_sent - cls._last_net_bytes_sent)
d_recv = max(0, net.bytes_recv - cls._last_net_bytes_recv)
# Convert bytes/sec to Megabits/sec (Mbps)
net_sent_mbps = round((d_sent * 8.0) / (dt * 1_000_000.0), 2)
net_recv_mbps = round((d_recv * 8.0) / (dt * 1_000_000.0), 2)
net_speed_mbps = round(((d_sent + d_recv) * 8.0) / (dt * 1_000_000.0), 2)
cls._last_net_time = now
cls._last_net_bytes_sent = net.bytes_sent
cls._last_net_bytes_recv = net.bytes_recv
except Exception:
pass
return HostMetricsSnapshot(
timestamp=now,
cpu_percent=round(float(cpu), 1),
memory_percent=round(float(mem.percent), 1),
memory_used_mb=round(float(mem.used) / (1024 * 1024), 1),
memory_total_mb=round(float(mem.total) / (1024 * 1024), 1),
disk_percent=round(float(disk.percent), 1),
disk_used_gb=round(float(disk.used) / (1024 * 1024 * 1024), 1),
disk_total_gb=round(float(disk.total) / (1024 * 1024 * 1024), 1),
net_speed_mbps=net_speed_mbps,
net_sent_mbps=net_sent_mbps,
net_recv_mbps=net_recv_mbps,
net_bytes_sent=int(net.bytes_sent) if net else None,
net_bytes_recv=int(net.bytes_recv) if net else None,
source="host_measurement",
has_data=True,
)
except Exception as exc:
logger.debug("Failed to collect host metrics via psutil: %s", exc)
return HostMetricsSnapshot(
timestamp=now,
source="host_measurement",
has_data=False,
)