diff --git a/src/dashboard/mod.rs b/src/dashboard/mod.rs index 91d3c03c..bd77aa79 100644 --- a/src/dashboard/mod.rs +++ b/src/dashboard/mod.rs @@ -92,6 +92,7 @@ pub fn router(state: AppState) -> Router { ) .route("/api/providers/{name}/test", post(providers::handle_test_provider)) .route("/api/system/health", get(system::handle_system_health)) + .route("/api/system/metrics", get(system::handle_system_metrics)) .route("/api/system/logs", get(system::handle_system_logs)) .route("/api/system/backup", post(system::handle_system_backup)) .route( diff --git a/src/dashboard/system.rs b/src/dashboard/system.rs index 416405c3..a365d9a0 100644 --- a/src/dashboard/system.rs +++ b/src/dashboard/system.rs @@ -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 { + std::fs::read_to_string(path).ok() +} + pub(super) async fn handle_system_health(State(state): State) -> Json> { 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) -> } // 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::().ok())) .map(|kb| kb / 1024.0) @@ -60,6 +64,162 @@ pub(super) async fn handle_system_health(State(state): State) -> }))) } +/// Real system metrics from /proc (Linux only). +pub(super) async fn handle_system_metrics( + State(state): State, +) -> Json> { + // --- 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 = 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::().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::().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::().unwrap_or(0.0) / 1024.0; + let used = parts[1].trim_end_matches('M').parse::().unwrap_or(0.0) / 1024.0; + let pct = parts[2].trim_end_matches('%').parse::().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::().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::().unwrap_or(0.0), + parts[1].parse::().unwrap_or(0.0), + parts[2].parse::().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::().unwrap_or(0); + let t = parts[9].parse::().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) -> Json> { let pool = &state.app_state.db_pool; diff --git a/static/css/dashboard.css b/static/css/dashboard.css index 066f6868..cc8d211c 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -1120,3 +1120,41 @@ body { ::-webkit-scrollbar-thumb:hover { background: var(--bg4); } + +/* Monitoring Layout */ +.monitoring-layout { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 1.5rem; +} + +.monitoring-stream { + height: 400px; + overflow-y: auto; + background-color: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + margin-top: 1rem; + padding: 1rem; +} + +.monitoring-charts { + margin-top: 1.5rem; +} + +/* Settings: Token Items */ +.token-item { + display: flex; + gap: 0.5rem; + align-items: center; + background: var(--bg0); + padding: 0.5rem; + border-radius: 6px; + border: 1px solid var(--bg2); +} + +/* Settings: Warning Card */ +.warning-card { + border: 1px dashed var(--warning); + background: rgba(215, 153, 33, 0.08); +} diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 782120dc..f1d28618 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -101,7 +101,8 @@ class Dashboard { 'providers': 'Providers', 'monitoring': 'Monitoring', 'settings': 'Settings', - 'logs': 'Logs' + 'logs': 'Logs', + 'models': 'Models' }; if (titleElement) titleElement.textContent = titles[page] || 'Dashboard'; @@ -193,8 +194,10 @@ class Dashboard {
-

System Health

-
+
+

System Health

+
+
@@ -400,10 +403,10 @@ class Dashboard { Pause Stream
-
+

Incoming Requests

-
+

System Performance

@@ -411,7 +414,7 @@ class Dashboard {
-
+

Latency (ms)

diff --git a/static/js/pages/monitoring.js b/static/js/pages/monitoring.js index 6077b2c9..f2af8426 100644 --- a/static/js/pages/monitoring.js +++ b/static/js/pages/monitoring.js @@ -26,32 +26,41 @@ class MonitoringPage { async loadSystemMetrics() { const container = document.getElementById('system-metrics'); if (!container) return; - - const metrics = [ - { label: 'CPU Usage', value: '24%', trend: 'down', color: 'success' }, - { label: 'Memory Usage', value: '1.8 GB', trend: 'stable', color: 'warning' }, - { label: 'Disk I/O', value: '45 MB/s', trend: 'up', color: 'primary' }, - { label: 'Network', value: '125 KB/s', trend: 'up', color: 'info' }, - { label: 'Active Connections', value: '42', trend: 'stable', color: 'success' }, - { label: 'Queue Length', value: '3', trend: 'down', color: 'success' } - ]; - - container.innerHTML = metrics.map(metric => ` -
-
${metric.label}
-
${metric.value}
-
- + + try { + const data = await window.api.get('/system/metrics'); + const metrics = [ + { label: 'CPU Usage', value: `${data.cpu.usage_percent}%`, trend: data.cpu.usage_percent > 80 ? 'up' : data.cpu.usage_percent < 30 ? 'down' : 'stable' }, + { label: 'Memory', value: `${(data.memory.used_mb / 1024).toFixed(1)} / ${(data.memory.total_mb / 1024).toFixed(1)} GB`, trend: data.memory.usage_percent > 80 ? 'up' : 'stable' }, + { label: 'Disk', value: `${data.disk.used_gb.toFixed(1)} / ${data.disk.total_gb.toFixed(1)} GB`, trend: data.disk.usage_percent > 80 ? 'up' : 'stable' }, + { label: 'Process RSS', value: `${data.memory.process_rss_mb} MB`, trend: 'stable' }, + { label: 'Load Average', value: data.cpu.load_average.map(v => v.toFixed(2)).join(' / '), trend: data.cpu.load_average[0] > 2 ? 'up' : 'down' }, + { label: 'Connections', value: `${data.connections.db_active} DB, ${data.connections.websocket_listeners} WS`, trend: 'stable' }, + ]; + + container.innerHTML = metrics.map(metric => ` +
+
${metric.label}
+
${metric.value}
+
+ +
-
- `).join(''); - + `).join(''); + } catch (error) { + console.error('Error loading system metrics:', error); + container.innerHTML = '
Failed to load metrics
'; + } + // Add CSS for metrics this.addMetricStyles(); } addMetricStyles() { + // Avoid injecting duplicate styles + if (document.getElementById('monitoring-metric-styles')) return; const style = document.createElement('style'); + style.id = 'monitoring-metric-styles'; style.textContent = ` .metric-item { display: flex; @@ -100,15 +109,6 @@ class MonitoringPage { color: var(--text-secondary); } - .monitoring-stream { - height: 300px; - overflow-y: auto; - border: 1px solid var(--border-color); - border-radius: var(--border-radius-sm); - padding: 0.5rem; - background-color: var(--bg-secondary); - } - .stream-entry { display: flex; align-items: center; @@ -148,67 +148,19 @@ class MonitoringPage { color: var(--text-secondary); margin-top: 0.125rem; } - - .log-stream { - height: 400px; - overflow-y: auto; - border: 1px solid var(--border-color); - border-radius: var(--border-radius-sm); - padding: 0.5rem; - background-color: var(--bg-secondary); - font-family: monospace; - font-size: 0.75rem; - } - - .log-entry { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.25rem 0.5rem; - border-bottom: 1px solid var(--border-color); - } - - .log-entry:last-child { - border-bottom: none; - } - - .log-time { - color: var(--text-light); - min-width: 80px; - } - - .log-level { - width: 24px; - text-align: center; - } - - .log-info .log-level { - color: var(--info); - } - - .log-warn .log-level { - color: var(--warning); - } - - .log-error .log-level { - color: var(--danger); - } - - .log-debug .log-level { - color: var(--text-light); - } - - .log-message { - flex: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } `; document.head.appendChild(style); } async loadCharts() { + // Fetch recent logs for chart data + try { + const logs = await window.api.get('/system/logs'); + this.recentLogs = Array.isArray(logs) ? logs : []; + } catch (error) { + console.error('Error loading logs for charts:', error); + this.recentLogs = []; + } await this.loadResponseTimeChart(); await this.loadErrorRateChart(); await this.loadRateLimitChart(); @@ -216,29 +168,33 @@ class MonitoringPage { async loadResponseTimeChart() { try { - // Generate demo data for response time - const labels = Array.from({ length: 20 }, (_, i) => `${i + 1}m`); + // Bucket recent logs by minute for latency chart + const buckets = this.bucketByMinute(this.recentLogs, 20); + const labels = buckets.map(b => b.label); + const values = buckets.map(b => { + if (b.items.length === 0) return 0; + const total = b.items.reduce((sum, r) => sum + (r.duration || 0), 0); + return Math.round(total / b.items.length); + }); + const data = { - labels: labels, + labels, datasets: [{ - label: 'Response Time (ms)', - data: labels.map(() => Math.floor(Math.random() * 200) + 300), - color: '#3b82f6', + label: 'Avg Response Time (ms)', + data: values, + color: '#83a598', fill: true }] }; - + window.chartManager.createLineChart('response-time-chart', data, { scales: { y: { - title: { - display: true, - text: 'Milliseconds' - } + title: { display: true, text: 'Milliseconds' }, + beginAtZero: true } } }); - } catch (error) { console.error('Error loading response time chart:', error); } @@ -246,33 +202,35 @@ class MonitoringPage { async loadErrorRateChart() { try { - const labels = Array.from({ length: 20 }, (_, i) => `${i + 1}m`); + const buckets = this.bucketByMinute(this.recentLogs, 20); + const labels = buckets.map(b => b.label); + const values = buckets.map(b => { + if (b.items.length === 0) return 0; + const errors = b.items.filter(r => r.status === 'error').length; + return parseFloat((errors / b.items.length * 100).toFixed(1)); + }); + const data = { - labels: labels, + labels, datasets: [{ label: 'Error Rate (%)', - data: labels.map(() => Math.random() * 5), - color: '#ef4444', + data: values, + color: '#fb4934', fill: true }] }; - + window.chartManager.createLineChart('error-rate-chart', data, { scales: { y: { - title: { - display: true, - text: 'Percentage' - }, + title: { display: true, text: 'Percentage' }, + beginAtZero: true, ticks: { - callback: function(value) { - return value + '%'; - } + callback: function(value) { return value + '%'; } } } } }); - } catch (error) { console.error('Error loading error rate chart:', error); } @@ -280,37 +238,57 @@ class MonitoringPage { async loadRateLimitChart() { try { - const labels = ['Web App', 'Mobile App', 'API Integration', 'Internal Tools', 'Testing']; + // Show requests-per-client from recent logs + const clientCounts = {}; + for (const log of this.recentLogs) { + const client = log.client_id || 'unknown'; + clientCounts[client] = (clientCounts[client] || 0) + 1; + } + const sorted = Object.entries(clientCounts).sort((a, b) => b[1] - a[1]).slice(0, 8); + const labels = sorted.map(([c]) => c.length > 16 ? c.substring(0, 14) + '...' : c); + const values = sorted.map(([, v]) => v); + const data = { - labels: labels, + labels: labels.length > 0 ? labels : ['No data'], datasets: [{ - label: 'Rate Limit Usage', - data: [65, 45, 78, 34, 60], - color: '#10b981' + label: 'Requests by Client', + data: values.length > 0 ? values : [0], + color: '#8ec07c' }] }; - + window.chartManager.createBarChart('rate-limit-chart', data, { scales: { y: { - title: { - display: true, - text: 'Percentage' - }, - ticks: { - callback: function(value) { - return value + '%'; - } - } + title: { display: true, text: 'Request Count' }, + beginAtZero: true } } }); - } catch (error) { console.error('Error loading rate limit chart:', error); } } + /** Bucket log entries into N-minute-wide bins ending at now. */ + bucketByMinute(logs, count) { + const now = Date.now(); + const buckets = Array.from({ length: count }, (_, i) => { + const minutesAgo = count - 1 - i; + return { label: minutesAgo === 0 ? 'now' : `${minutesAgo}m`, items: [], start: now - (minutesAgo + 1) * 60000, end: now - minutesAgo * 60000 }; + }); + for (const log of logs) { + const ts = new Date(log.timestamp).getTime(); + for (const bucket of buckets) { + if (ts >= bucket.start && ts < bucket.end) { + bucket.items.push(log); + break; + } + } + } + return buckets; + } + setupEventListeners() { // Pause/resume monitoring button const pauseBtn = document.getElementById('pause-monitoring'); diff --git a/static/js/pages/settings.js b/static/js/pages/settings.js index d191e5f3..c6e15fc0 100644 --- a/static/js/pages/settings.js +++ b/static/js/pages/settings.js @@ -42,7 +42,7 @@ class SettingsPage {
${this.settings.server.auth_tokens.map(token => ` -
+
${token}
-
+