diff --git a/server/index.js b/server/index.js index c102090..e20842e 100644 --- a/server/index.js +++ b/server/index.js @@ -150,6 +150,8 @@ app.post('/api/setup/init', async (req, res) => { id INT AUTO_INCREMENT PRIMARY KEY, rx_bytes BIGINT UNSIGNED DEFAULT 0, tx_bytes BIGINT UNSIGNED DEFAULT 0, + rx_bandwidth DOUBLE DEFAULT 0, + tx_bandwidth DOUBLE DEFAULT 0, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); @@ -443,31 +445,22 @@ app.get('/api/metrics/overview', async (req, res) => { } }); -// Get network traffic history (past 24h in intervals) +// Get network traffic history from DB (past 24h) app.get('/api/metrics/network-history', async (req, res) => { try { - const [sources] = await db.query('SELECT * FROM prometheus_sources'); - if (sources.length === 0) { + const [rows] = await db.query('SELECT rx_bandwidth, tx_bandwidth, UNIX_TIMESTAMP(timestamp) as ts FROM traffic_stats WHERE timestamp >= NOW() - INTERVAL 1 DAY ORDER BY ts ASC'); + + if (rows.length === 0) { return res.json({ timestamps: [], rx: [], tx: [] }); } - const allHistories = await Promise.all(sources.map(source => - prometheusService.getNetworkHistory(source.url).catch(err => { - console.error(`Error fetching network history from ${source.name}:`, err.message); - return null; - }) - )); - - const validHistories = allHistories.filter(h => h !== null); - if (validHistories.length === 0) { - return res.json({ timestamps: [], rx: [], tx: [] }); - } - - // Merge all histories by timestamp - const merged = prometheusService.mergeNetworkHistories(validHistories); - res.json(merged); + res.json({ + timestamps: rows.map(r => r.ts * 1000), + rx: rows.map(r => r.rx_bandwidth), + tx: rows.map(r => r.tx_bandwidth) + }); } catch (err) { - console.error('Error fetching network history:', err); + console.error('Error fetching network history from DB:', err); res.status(500).json({ error: 'Failed to fetch network history' }); } }); @@ -511,32 +504,46 @@ async function recordTrafficStats() { const [sources] = await db.query('SELECT * FROM prometheus_sources'); if (sources.length === 0) return; - let totalRx = 0; - let totalTx = 0; + let totalRxBytes = 0; + let totalTxBytes = 0; + let totalRxBandwidth = 0; + let totalTxBandwidth = 0; const results = await Promise.all(sources.map(async source => { try { - const [rxRes, txRes] = await Promise.all([ + const [rxBytesRes, txBytesRes, rxBWRes, txBWRes] = await Promise.all([ prometheusService.query(source.url, 'sum(node_network_receive_bytes_total{device!~"lo|veth.*|docker.*|br-.*"})'), - prometheusService.query(source.url, 'sum(node_network_transmit_bytes_total{device!~"lo|veth.*|docker.*|br-.*"})') + prometheusService.query(source.url, 'sum(node_network_transmit_bytes_total{device!~"lo|veth.*|docker.*|br-.*"})'), + prometheusService.query(source.url, 'sum(rate(node_network_receive_bytes_total{device!~"lo|veth.*|docker.*|br-.*"}[5m]))'), + prometheusService.query(source.url, 'sum(rate(node_network_transmit_bytes_total{device!~"lo|veth.*|docker.*|br-.*"}[5m]))') ]); - const rx = (rxRes.length > 0) ? parseFloat(rxRes[0].value[1]) : 0; - const tx = (txRes.length > 0) ? parseFloat(txRes[0].value[1]) : 0; - return { rx, tx }; + return { + rxBytes: (rxBytesRes.length > 0) ? parseFloat(rxBytesRes[0].value[1]) : 0, + txBytes: (txBytesRes.length > 0) ? parseFloat(txBytesRes[0].value[1]) : 0, + rxBW: (rxBWRes.length > 0) ? parseFloat(rxBWRes[0].value[1]) : 0, + txBW: (txBWRes.length > 0) ? parseFloat(txBWRes[0].value[1]) : 0 + }; } catch (e) { - return { rx: 0, tx: 0 }; + return { rxBytes: 0, txBytes: 0, rxBW: 0, txBW: 0 }; } })); for (const r of results) { - totalRx += r.rx; - totalTx += r.tx; + totalRxBytes += r.rxBytes; + totalTxBytes += r.txBytes; + totalRxBandwidth += r.rxBW; + totalTxBandwidth += r.txBW; } - if (totalRx > 0 || totalTx > 0) { - await db.query('INSERT INTO traffic_stats (rx_bytes, tx_bytes) VALUES (?, ?)', [Math.round(totalRx), Math.round(totalTx)]); - console.log(`[Traffic Recorder] Saved stats: RX=${totalRx}, TX=${totalTx}`); + if (totalRxBytes > 0 || totalTxBytes > 0) { + await db.query('INSERT INTO traffic_stats (rx_bytes, tx_bytes, rx_bandwidth, tx_bandwidth) VALUES (?, ?, ?, ?)', [ + Math.round(totalRxBytes), + Math.round(totalTxBytes), + totalRxBandwidth, + totalTxBandwidth + ]); + console.log(`[Traffic Recorder] Saved stats: BW_RX=${totalRxBandwidth}, BW_TX=${totalTxBandwidth}`); } } catch (err) { console.error('[Traffic Recorder] Error recording stats:', err); @@ -552,9 +559,14 @@ async function ensureTrafficTable() { id INT AUTO_INCREMENT PRIMARY KEY, rx_bytes BIGINT UNSIGNED DEFAULT 0, tx_bytes BIGINT UNSIGNED DEFAULT 0, + rx_bandwidth DOUBLE DEFAULT 0, + tx_bandwidth DOUBLE DEFAULT 0, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); + // Add columns if missing for existing tables + try { await db.query('ALTER TABLE traffic_stats ADD COLUMN rx_bandwidth DOUBLE DEFAULT 0'); } catch(e) {} + try { await db.query('ALTER TABLE traffic_stats ADD COLUMN tx_bandwidth DOUBLE DEFAULT 0'); } catch(e) {} } catch (err) {} } diff --git a/server/prometheus-service.js b/server/prometheus-service.js index a55c871..857bc5f 100644 --- a/server/prometheus-service.js +++ b/server/prometheus-service.js @@ -159,10 +159,10 @@ async function getOverviewMetrics(url, sourceName) { query(url, 'node_memory_MemTotal_bytes').catch(() => []), // Memory available per instance query(url, 'node_memory_MemAvailable_bytes').catch(() => []), - // Disk total per instance (root filesystem) - query(url, 'sum by (instance, job) (node_filesystem_size_bytes{mountpoint="/",fstype!="tmpfs"})').catch(() => []), - // Disk free per instance (root filesystem) - query(url, 'sum by (instance, job) (node_filesystem_free_bytes{mountpoint="/",fstype!="tmpfs"})').catch(() => []), + // Disk total per instance (root filesystem + /data) + query(url, 'sum by (instance, job) (node_filesystem_size_bytes{mountpoint=~"/|/data",fstype!="tmpfs"})').catch(() => []), + // Disk free per instance (root filesystem + /data) + query(url, 'sum by (instance, job) (node_filesystem_free_bytes{mountpoint=~"/|/data",fstype!="tmpfs"})').catch(() => []), // Network receive rate (bytes/sec) query(url, 'sum by (instance, job) (rate(node_network_receive_bytes_total{device!~"lo|veth.*|docker.*|br-.*"}[5m]))').catch(() => []), // Network transmit rate (bytes/sec) @@ -171,8 +171,8 @@ async function getOverviewMetrics(url, sourceName) { query(url, 'sum by (instance, job) (increase(node_network_receive_bytes_total{device!~"lo|veth.*|docker.*|br-.*"}[24h]))').catch(() => []), // Total traffic transmitted in last 24h query(url, 'sum by (instance, job) (increase(node_network_transmit_bytes_total{device!~"lo|veth.*|docker.*|br-.*"}[24h]))').catch(() => []), - // Up instances - query(url, 'up{job=~".*node.*|.*exporter.*"}').catch(() => []) + // Up instances (at least one successful scrape in last 5m) + query(url, 'max_over_time(up{job=~".*node.*|.*exporter.*"}[5m])').catch(() => []) ]); // Build per-instance data map