forked from iarv/pyMC_Repeater
Merge pull request #26 from rightup/feat/hardware-stats
Feat/hardware stats
This commit is contained in:
@@ -36,6 +36,7 @@ dependencies = [
|
||||
"cherrypy>=18.0.0",
|
||||
"paho-mqtt>=1.6.0",
|
||||
"cherrypy-cors==1.7.0",
|
||||
"psutil>=5.9.0",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
Hardware statistics collection using psutil.
|
||||
KISS - Keep It Simple Stupid approach.
|
||||
"""
|
||||
|
||||
try:
|
||||
import psutil
|
||||
PSUTIL_AVAILABLE = True
|
||||
except ImportError:
|
||||
PSUTIL_AVAILABLE = False
|
||||
psutil = None
|
||||
|
||||
import time
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("HardwareStats")
|
||||
|
||||
|
||||
class HardwareStatsCollector:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.start_time = time.time()
|
||||
|
||||
def get_stats(self):
|
||||
|
||||
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()
|
||||
uptime = now - self.start_time
|
||||
|
||||
# CPU stats
|
||||
cpu_percent = psutil.cpu_percent(interval=0.1)
|
||||
cpu_count = psutil.cpu_count()
|
||||
cpu_freq = psutil.cpu_freq()
|
||||
|
||||
# Memory stats
|
||||
memory = psutil.virtual_memory()
|
||||
|
||||
# Disk stats
|
||||
disk = psutil.disk_usage('/')
|
||||
|
||||
# Network stats (total across all interfaces)
|
||||
net_io = psutil.net_io_counters()
|
||||
|
||||
# Load average (Unix only)
|
||||
load_avg = None
|
||||
try:
|
||||
load_avg = psutil.getloadavg()
|
||||
except (AttributeError, OSError):
|
||||
# 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 = {
|
||||
"cpu": {
|
||||
"usage_percent": cpu_percent,
|
||||
"count": cpu_count,
|
||||
"frequency": cpu_freq.current if cpu_freq else 0,
|
||||
"load_avg": {
|
||||
"1min": load_avg[0],
|
||||
"5min": load_avg[1],
|
||||
"15min": load_avg[2]
|
||||
}
|
||||
},
|
||||
"memory": {
|
||||
"total": memory.total,
|
||||
"available": memory.available,
|
||||
"used": memory.used,
|
||||
"usage_percent": memory.percent
|
||||
},
|
||||
"disk": {
|
||||
"total": disk.total,
|
||||
"used": disk.used,
|
||||
"free": disk.free,
|
||||
"usage_percent": round((disk.used / disk.total) * 100, 1)
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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 {
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def get_processes_summary(self, limit=10):
|
||||
"""
|
||||
Get top processes by CPU and memory usage.
|
||||
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', 'memory_info']):
|
||||
try:
|
||||
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 and get top processes
|
||||
top_processes = sorted(processes, key=lambda x: x['cpu_percent'], reverse=True)[:limit]
|
||||
|
||||
return {
|
||||
"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)
|
||||
}
|
||||
@@ -64,6 +64,11 @@ class StorageCollector:
|
||||
self.disallowed_packet_types = set()
|
||||
else:
|
||||
self.disallowed_packet_types = set()
|
||||
|
||||
# Initialize hardware stats collector
|
||||
from .hardware_stats import HardwareStatsCollector
|
||||
self.hardware_stats = HardwareStatsCollector()
|
||||
logger.info("Hardware stats collector initialized")
|
||||
|
||||
def _get_live_stats(self) -> dict:
|
||||
"""Get live stats from RepeaterHandler"""
|
||||
@@ -228,3 +233,19 @@ class StorageCollector:
|
||||
|
||||
def delete_advert(self, advert_id: int) -> bool:
|
||||
return self.sqlite_handler.delete_advert(advert_id)
|
||||
|
||||
def get_hardware_stats(self) -> Optional[dict]:
|
||||
"""Get current hardware statistics"""
|
||||
try:
|
||||
return self.hardware_stats.get_stats()
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting hardware stats: {e}")
|
||||
return None
|
||||
|
||||
def get_hardware_processes(self) -> Optional[list]:
|
||||
"""Get current process summary"""
|
||||
try:
|
||||
return self.hardware_stats.get_processes_summary()
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting hardware processes: {e}")
|
||||
return None
|
||||
|
||||
@@ -243,6 +243,44 @@ class APIEndpoints:
|
||||
logger.error(f"Error fetching logs: {e}")
|
||||
return {"error": str(e), "logs": []}
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
def hardware_stats(self):
|
||||
"""Get comprehensive hardware statistics"""
|
||||
try:
|
||||
# Get hardware stats from storage collector
|
||||
storage = self._get_storage()
|
||||
if storage:
|
||||
stats = storage.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)
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
def hardware_processes(self):
|
||||
"""Get summary of top processes"""
|
||||
try:
|
||||
# Get process stats from storage collector
|
||||
storage = self._get_storage()
|
||||
if storage:
|
||||
processes = storage.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)
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
def packet_stats(self, hours=24):
|
||||
|
||||
+175
-175
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -8,8 +8,8 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script type="module" crossorigin src="/assets/index-r2jdFy7f.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Dmg9lJJT.css">
|
||||
<script type="module" crossorigin src="/assets/index-7Gn2-Mfw.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DB3Eq_QU.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user