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",
|
"cherrypy>=18.0.0",
|
||||||
"paho-mqtt>=1.6.0",
|
"paho-mqtt>=1.6.0",
|
||||||
"cherrypy-cors==1.7.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()
|
self.disallowed_packet_types = set()
|
||||||
else:
|
else:
|
||||||
self.disallowed_packet_types = set()
|
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:
|
def _get_live_stats(self) -> dict:
|
||||||
"""Get live stats from RepeaterHandler"""
|
"""Get live stats from RepeaterHandler"""
|
||||||
@@ -228,3 +233,19 @@ class StorageCollector:
|
|||||||
|
|
||||||
def delete_advert(self, advert_id: int) -> bool:
|
def delete_advert(self, advert_id: int) -> bool:
|
||||||
return self.sqlite_handler.delete_advert(advert_id)
|
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}")
|
logger.error(f"Error fetching logs: {e}")
|
||||||
return {"error": str(e), "logs": []}
|
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.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
def packet_stats(self, hours=24):
|
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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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>
|
<script type="module" crossorigin src="/assets/index-7Gn2-Mfw.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Dmg9lJJT.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DB3Eq_QU.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user