feat(dashboard): add real system metrics endpoint and fix UI dark-theme issues
- Add /api/system/metrics endpoint reading real data from /proc (CPU, memory, disk, network, load avg, uptime, connections) - Replace hardcoded fake monitoring metrics with live API data - Replace random chart data with real latency/error-rate/client-request charts from DB logs - Fix light-mode colors leaking into dark theme (monitoring stream bg, settings tokens, warning card) - Add 'models' to page title map, fix System Health card structure - Move inline styles to CSS classes (monitoring-layout, monitoring-stream, token-item, warning-card) - Prevent duplicate style injection in monitoring page
This commit is contained in:
@@ -7,6 +7,11 @@ use tracing::warn;
|
||||
|
||||
use super::{ApiResponse, DashboardState};
|
||||
|
||||
/// Read a value from /proc files, returning None on any failure.
|
||||
fn read_proc_file(path: &str) -> Option<String> {
|
||||
std::fs::read_to_string(path).ok()
|
||||
}
|
||||
|
||||
pub(super) async fn handle_system_health(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
|
||||
let mut components = HashMap::new();
|
||||
components.insert("api_server".to_string(), "online".to_string());
|
||||
@@ -37,8 +42,7 @@ pub(super) async fn handle_system_health(State(state): State<DashboardState>) ->
|
||||
}
|
||||
|
||||
// Read real memory usage from /proc/self/status
|
||||
let memory_mb = std::fs::read_to_string("/proc/self/status")
|
||||
.ok()
|
||||
let memory_mb = read_proc_file("/proc/self/status")
|
||||
.and_then(|s| s.lines().find(|l| l.starts_with("VmRSS:")).map(|l| l.to_string()))
|
||||
.and_then(|l| l.split_whitespace().nth(1).and_then(|v| v.parse::<f64>().ok()))
|
||||
.map(|kb| kb / 1024.0)
|
||||
@@ -60,6 +64,162 @@ pub(super) async fn handle_system_health(State(state): State<DashboardState>) ->
|
||||
})))
|
||||
}
|
||||
|
||||
/// Real system metrics from /proc (Linux only).
|
||||
pub(super) async fn handle_system_metrics(
|
||||
State(state): State<DashboardState>,
|
||||
) -> Json<ApiResponse<serde_json::Value>> {
|
||||
// --- CPU usage (aggregate across all cores) ---
|
||||
// /proc/stat first line: cpu user nice system idle iowait irq softirq steal guest guest_nice
|
||||
let cpu_percent = read_proc_file("/proc/stat")
|
||||
.and_then(|s| {
|
||||
let line = s.lines().find(|l| l.starts_with("cpu "))?.to_string();
|
||||
let fields: Vec<u64> = line
|
||||
.split_whitespace()
|
||||
.skip(1)
|
||||
.filter_map(|v| v.parse().ok())
|
||||
.collect();
|
||||
if fields.len() >= 4 {
|
||||
let idle = fields[3];
|
||||
let total: u64 = fields.iter().sum();
|
||||
if total > 0 {
|
||||
Some(((total - idle) as f64 / total as f64 * 100.0 * 10.0).round() / 10.0)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(0.0);
|
||||
|
||||
// --- Memory (system-wide from /proc/meminfo) ---
|
||||
let meminfo = read_proc_file("/proc/meminfo").unwrap_or_default();
|
||||
let parse_meminfo = |key: &str| -> u64 {
|
||||
meminfo
|
||||
.lines()
|
||||
.find(|l| l.starts_with(key))
|
||||
.and_then(|l| l.split_whitespace().nth(1))
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(0)
|
||||
};
|
||||
let mem_total_kb = parse_meminfo("MemTotal:");
|
||||
let mem_available_kb = parse_meminfo("MemAvailable:");
|
||||
let mem_used_kb = mem_total_kb.saturating_sub(mem_available_kb);
|
||||
let mem_total_mb = mem_total_kb as f64 / 1024.0;
|
||||
let mem_used_mb = mem_used_kb as f64 / 1024.0;
|
||||
let mem_percent = if mem_total_kb > 0 {
|
||||
(mem_used_kb as f64 / mem_total_kb as f64 * 100.0 * 10.0).round() / 10.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// --- Process-specific memory (VmRSS) ---
|
||||
let process_rss_mb = read_proc_file("/proc/self/status")
|
||||
.and_then(|s| s.lines().find(|l| l.starts_with("VmRSS:")).map(|l| l.to_string()))
|
||||
.and_then(|l| l.split_whitespace().nth(1).and_then(|v| v.parse::<f64>().ok()))
|
||||
.map(|kb| (kb / 1024.0 * 10.0).round() / 10.0)
|
||||
.unwrap_or(0.0);
|
||||
|
||||
// --- Disk usage of the data directory ---
|
||||
let (disk_total_gb, disk_used_gb, disk_percent) = {
|
||||
// statvfs via libc would be ideal; use df as a simple fallback
|
||||
std::process::Command::new("df")
|
||||
.args(["-BM", "--output=size,used,pcent", "."])
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| {
|
||||
let out = String::from_utf8_lossy(&o.stdout);
|
||||
let line = out.lines().nth(1)?.to_string();
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 3 {
|
||||
let total = parts[0].trim_end_matches('M').parse::<f64>().unwrap_or(0.0) / 1024.0;
|
||||
let used = parts[1].trim_end_matches('M').parse::<f64>().unwrap_or(0.0) / 1024.0;
|
||||
let pct = parts[2].trim_end_matches('%').parse::<f64>().unwrap_or(0.0);
|
||||
Some(((total * 10.0).round() / 10.0, (used * 10.0).round() / 10.0, pct))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or((0.0, 0.0, 0.0))
|
||||
};
|
||||
|
||||
// --- Uptime ---
|
||||
let uptime_seconds = read_proc_file("/proc/uptime")
|
||||
.and_then(|s| s.split_whitespace().next().and_then(|v| v.parse::<f64>().ok()))
|
||||
.unwrap_or(0.0) as u64;
|
||||
|
||||
// --- Load average ---
|
||||
let (load_1, load_5, load_15) = read_proc_file("/proc/loadavg")
|
||||
.and_then(|s| {
|
||||
let parts: Vec<&str> = s.split_whitespace().collect();
|
||||
if parts.len() >= 3 {
|
||||
Some((
|
||||
parts[0].parse::<f64>().unwrap_or(0.0),
|
||||
parts[1].parse::<f64>().unwrap_or(0.0),
|
||||
parts[2].parse::<f64>().unwrap_or(0.0),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or((0.0, 0.0, 0.0));
|
||||
|
||||
// --- Network (from /proc/net/dev, aggregate non-lo interfaces) ---
|
||||
let (net_rx_bytes, net_tx_bytes) = read_proc_file("/proc/net/dev")
|
||||
.map(|s| {
|
||||
s.lines()
|
||||
.skip(2) // skip header lines
|
||||
.filter(|l| !l.trim().starts_with("lo:"))
|
||||
.fold((0u64, 0u64), |(rx, tx), line| {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 10 {
|
||||
let r = parts[1].parse::<u64>().unwrap_or(0);
|
||||
let t = parts[9].parse::<u64>().unwrap_or(0);
|
||||
(rx + r, tx + t)
|
||||
} else {
|
||||
(rx, tx)
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
// --- Database pool ---
|
||||
let db_pool_size = state.app_state.db_pool.size();
|
||||
let db_pool_idle = state.app_state.db_pool.num_idle();
|
||||
|
||||
// --- Active WebSocket listeners ---
|
||||
let ws_listeners = state.app_state.dashboard_tx.receiver_count();
|
||||
|
||||
Json(ApiResponse::success(serde_json::json!({
|
||||
"cpu": {
|
||||
"usage_percent": cpu_percent,
|
||||
"load_average": [load_1, load_5, load_15],
|
||||
},
|
||||
"memory": {
|
||||
"total_mb": (mem_total_mb * 10.0).round() / 10.0,
|
||||
"used_mb": (mem_used_mb * 10.0).round() / 10.0,
|
||||
"usage_percent": mem_percent,
|
||||
"process_rss_mb": process_rss_mb,
|
||||
},
|
||||
"disk": {
|
||||
"total_gb": disk_total_gb,
|
||||
"used_gb": disk_used_gb,
|
||||
"usage_percent": disk_percent,
|
||||
},
|
||||
"network": {
|
||||
"rx_bytes": net_rx_bytes,
|
||||
"tx_bytes": net_tx_bytes,
|
||||
},
|
||||
"uptime_seconds": uptime_seconds,
|
||||
"connections": {
|
||||
"db_active": db_pool_size - db_pool_idle as u32,
|
||||
"db_idle": db_pool_idle,
|
||||
"websocket_listeners": ws_listeners,
|
||||
},
|
||||
"timestamp": chrono::Utc::now().to_rfc3339(),
|
||||
})))
|
||||
}
|
||||
|
||||
pub(super) async fn handle_system_logs(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
|
||||
let pool = &state.app_state.db_pool;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user