hardware statistics integrate with storage collector

This commit is contained in:
Lloyd
2025-11-29 22:42:15 +00:00
parent 330d97d862
commit 748ecf33b3
2 changed files with 101 additions and 79 deletions

View File

@@ -3,7 +3,13 @@ Hardware statistics collection using psutil.
KISS - Keep It Simple Stupid approach.
"""
import psutil
try:
import psutil
PSUTIL_AVAILABLE = True
except ImportError:
PSUTIL_AVAILABLE = False
psutil = None
import time
import logging
@@ -11,17 +17,19 @@ logger = logging.getLogger("HardwareStats")
class HardwareStatsCollector:
"""Simple hardware statistics collector using psutil."""
def __init__(self):
"""Initialize the hardware stats collector."""
self.start_time = time.time()
def get_stats(self):
"""
Get current hardware statistics.
Returns a dictionary with system stats.
"""
if not PSUTIL_AVAILABLE:
logger.error("psutil not available - cannot collect hardware stats")
return {
"error": "psutil library not available - cannot collect hardware statistics"
}
try:
# Get current timestamp
now = time.time()
@@ -34,7 +42,6 @@ class HardwareStatsCollector:
# Memory stats
memory = psutil.virtual_memory()
swap = psutil.swap_memory()
# Disk stats
disk = psutil.disk_usage('/')
@@ -42,123 +49,126 @@ class HardwareStatsCollector:
# Network stats (total across all interfaces)
net_io = psutil.net_io_counters()
# Temperature (if available)
temperature = None
try:
temps = psutil.sensors_temperatures()
if 'cpu_thermal' in temps and len(temps['cpu_thermal']) > 0:
temperature = temps['cpu_thermal'][0].current
elif temps:
# Fallback to first available temperature sensor
first_sensor = next(iter(temps.values()))
if first_sensor:
temperature = first_sensor[0].current
except (AttributeError, OSError):
# Temperature sensors not available
pass
# Load average (Unix only)
load_avg = None
try:
load_avg = psutil.getloadavg()
except (AttributeError, OSError):
# Not available on all systems
pass
# Not available on all systems - use zeros
load_avg = (0.0, 0.0, 0.0)
# System boot time
boot_time = psutil.boot_time()
system_uptime = now - boot_time
# Temperature (if available)
temperatures = {}
try:
temps = psutil.sensors_temperatures()
for name, entries in temps.items():
for i, entry in enumerate(entries):
temp_name = f"{name}_{i}" if len(entries) > 1 else name
temperatures[temp_name] = entry.current
except (AttributeError, OSError):
# Temperature sensors not available
pass
# Format data structure to match Vue component expectations
stats = {
"timestamp": now,
"uptime_seconds": uptime,
"system_uptime_seconds": system_uptime,
# CPU
"cpu": {
"percent": cpu_percent,
"usage_percent": cpu_percent,
"count": cpu_count,
"frequency_mhz": cpu_freq.current if cpu_freq else None,
"load_average": list(load_avg) if load_avg else None
"frequency": cpu_freq.current if cpu_freq else 0,
"load_avg": {
"1min": load_avg[0],
"5min": load_avg[1],
"15min": load_avg[2]
}
},
# Memory
"memory": {
"total_mb": round(memory.total / 1024 / 1024, 1),
"available_mb": round(memory.available / 1024 / 1024, 1),
"used_mb": round(memory.used / 1024 / 1024, 1),
"percent": memory.percent
"total": memory.total,
"available": memory.available,
"used": memory.used,
"usage_percent": memory.percent
},
# Swap
"swap": {
"total_mb": round(swap.total / 1024 / 1024, 1),
"used_mb": round(swap.used / 1024 / 1024, 1),
"percent": swap.percent
},
# Disk
"disk": {
"total_gb": round(disk.total / 1024 / 1024 / 1024, 1),
"used_gb": round(disk.used / 1024 / 1024 / 1024, 1),
"free_gb": round(disk.free / 1024 / 1024 / 1024, 1),
"percent": round((disk.used / disk.total) * 100, 1)
"total": disk.total,
"used": disk.used,
"free": disk.free,
"usage_percent": round((disk.used / disk.total) * 100, 1)
},
# Network
"network": {
"bytes_sent": net_io.bytes_sent,
"bytes_recv": net_io.bytes_recv,
"packets_sent": net_io.packets_sent,
"packets_recv": net_io.packets_recv,
"errors_in": net_io.errin,
"errors_out": net_io.errout,
"drops_in": net_io.dropin,
"drops_out": net_io.dropout
"packets_recv": net_io.packets_recv
},
# Temperature
"temperature_celsius": temperature
"system": {
"uptime": system_uptime,
"boot_time": boot_time
}
}
# Add temperatures if available
if temperatures:
stats["temperatures"] = temperatures
return stats
except Exception as e:
logger.error(f"Error collecting hardware stats: {e}")
return {
"timestamp": time.time(),
"error": str(e)
}
def get_processes_summary(self, limit=10):
"""
Get top processes by CPU and memory usage.
Returns a dictionary with process information.
Returns a dictionary with process information in the format expected by the UI.
"""
if not PSUTIL_AVAILABLE:
logger.error("psutil not available - cannot collect process stats")
return {
"processes": [],
"total_processes": 0,
"error": "psutil library not available - cannot collect process statistics"
}
try:
processes = []
# Get all processes
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']):
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'memory_info']):
try:
processes.append(proc.info)
pinfo = proc.info
# Calculate memory in MB
memory_mb = 0
if pinfo['memory_info']:
memory_mb = pinfo['memory_info'].rss / 1024 / 1024 # RSS in MB
process_data = {
"pid": pinfo['pid'],
"name": pinfo['name'] or 'Unknown',
"cpu_percent": pinfo['cpu_percent'] or 0.0,
"memory_percent": pinfo['memory_percent'] or 0.0,
"memory_mb": round(memory_mb, 1)
}
processes.append(process_data)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
# Sort by CPU usage
top_cpu = sorted(processes, key=lambda x: x['cpu_percent'] or 0, reverse=True)[:limit]
# Sort by memory usage
top_memory = sorted(processes, key=lambda x: x['memory_percent'] or 0, reverse=True)[:limit]
# Sort by CPU usage and get top processes
top_processes = sorted(processes, key=lambda x: x['cpu_percent'], reverse=True)[:limit]
return {
"top_cpu": top_cpu,
"top_memory": top_memory,
"processes": top_processes,
"total_processes": len(processes)
}
except Exception as e:
logger.error(f"Error collecting process stats: {e}")
return {
"processes": [],
"total_processes": 0,
"error": str(e)
}

View File

@@ -8,7 +8,6 @@ import cherrypy
from repeater import __version__
from repeater.config import update_global_flood_policy
from .cad_calibration_engine import CADCalibrationEngine
from repeater.data_acquisition.hardware_stats import HardwareStatsCollector
logger = logging.getLogger("HTTPServer")
@@ -65,7 +64,6 @@ class APIEndpoints:
self.daemon_instance = daemon_instance
self._config_path = config_path or '/etc/pymc_repeater/config.yaml'
self.cad_calibration = CADCalibrationEngine(daemon_instance, event_loop)
self.hardware_stats = HardwareStatsCollector()
def _is_cors_enabled(self):
return self.config.get("web", {}).get("cors_enabled", False)
@@ -250,8 +248,15 @@ class APIEndpoints:
def hardware_stats(self):
"""Get comprehensive hardware statistics"""
try:
stats = self.hardware_stats.get_stats()
return self._success(stats)
# Get hardware stats from storage collector
if hasattr(self.daemon_instance, 'storage_collector') and self.daemon_instance.storage_collector:
stats = self.daemon_instance.storage_collector.get_hardware_stats()
if stats:
return self._success(stats)
else:
return self._error("Hardware stats not available (psutil may not be installed)")
else:
return self._error("Storage collector not available")
except Exception as e:
logger.error(f"Error getting hardware stats: {e}")
return self._error(e)
@@ -261,8 +266,15 @@ class APIEndpoints:
def hardware_processes(self):
"""Get summary of top processes"""
try:
processes = self.hardware_stats.get_processes_summary()
return self._success(processes)
# Get process stats from storage collector
if hasattr(self.daemon_instance, 'storage_collector') and self.daemon_instance.storage_collector:
processes = self.daemon_instance.storage_collector.get_hardware_processes()
if processes:
return self._success(processes)
else:
return self._error("Process information not available (psutil may not be installed)")
else:
return self._error("Storage collector not available")
except Exception as e:
logger.error(f"Error getting process stats: {e}")
return self._error(e)