mirror of
https://github.com/rightup/pyMC_Repeater.git
synced 2026-03-28 17:43:06 +01:00
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",
|
||||
]
|
||||
|
||||
|
||||
|
||||
174
repeater/data_acquisition/hardware_stats.py
Normal file
174
repeater/data_acquisition/hardware_stats.py
Normal file
@@ -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):
|
||||
|
||||
File diff suppressed because one or more lines are too long
1
repeater/web/html/assets/index-DB3Eq_QU.css
Normal file
1
repeater/web/html/assets/index-DB3Eq_QU.css
Normal file
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