const axios = require('axios'); const QUERY_TIMEOUT = 10000; /** * Create an axios instance for a given Prometheus URL */ function createClient(baseUrl) { return axios.create({ baseURL: baseUrl.replace(/\/+$/, ''), timeout: QUERY_TIMEOUT }); } /** * Test Prometheus connection */ async function testConnection(url) { const client = createClient(url); const res = await client.get('/api/v1/status/buildinfo'); return res.data?.data?.version || 'unknown'; } /** * Execute a Prometheus instant query */ async function query(url, expr) { const client = createClient(url); const res = await client.get('/api/v1/query', { params: { query: expr } }); if (res.data.status !== 'success') { throw new Error(`Prometheus query failed: ${res.data.error || 'unknown error'}`); } return res.data.data.result; } /** * Execute a Prometheus range query */ async function queryRange(url, expr, start, end, step) { const client = createClient(url); const res = await client.get('/api/v1/query_range', { params: { query: expr, start, end, step } }); if (res.data.status !== 'success') { throw new Error(`Prometheus range query failed: ${res.data.error || 'unknown error'}`); } return res.data.data.result; } /** * Get overview metrics from a single Prometheus source */ async function getOverviewMetrics(url, sourceName) { // Run all queries in parallel const [ cpuResult, cpuCountResult, memTotalResult, memAvailResult, diskTotalResult, diskFreeResult, netRxResult, netTxResult, traffic24hRxResult, traffic24hTxResult, upResult ] = await Promise.all([ // CPU usage per instance: 1 - avg idle query(url, '100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)').catch(() => []), // CPU count per instance query(url, 'count by (instance) (node_cpu_seconds_total{mode="idle"})').catch(() => []), // Memory total per instance 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) (node_filesystem_size_bytes{mountpoint="/",fstype!="tmpfs"})').catch(() => []), // Disk free per instance (root filesystem) query(url, 'sum by (instance) (node_filesystem_free_bytes{mountpoint="/",fstype!="tmpfs"})').catch(() => []), // Network receive rate (bytes/sec) query(url, 'sum by (instance) (rate(node_network_receive_bytes_total{device!~"lo|veth.*|docker.*|br-.*"}[5m]))').catch(() => []), // Network transmit rate (bytes/sec) query(url, 'sum by (instance) (rate(node_network_transmit_bytes_total{device!~"lo|veth.*|docker.*|br-.*"}[5m]))').catch(() => []), // Total traffic received in last 24h query(url, 'sum by (instance) (increase(node_network_receive_bytes_total{device!~"lo|veth.*|docker.*|br-.*"}[24h]))').catch(() => []), // Total traffic transmitted in last 24h query(url, 'sum by (instance) (increase(node_network_transmit_bytes_total{device!~"lo|veth.*|docker.*|br-.*"}[24h]))').catch(() => []), // Up instances query(url, 'up{job=~".*node.*|.*exporter.*"}').catch(() => []) ]); // Build per-instance data map const instances = new Map(); const getOrCreate = (instance) => { if (!instances.has(instance)) { instances.set(instance, { instance, source: sourceName, cpuPercent: 0, cpuCores: 0, memTotal: 0, memUsed: 0, diskTotal: 0, diskUsed: 0, netRx: 0, netTx: 0, up: false }); } return instances.get(instance); }; // Parse UP status for (const r of upResult) { const inst = getOrCreate(r.metric.instance); inst.up = parseFloat(r.value[1]) === 1; } // Parse CPU usage for (const r of cpuResult) { const inst = getOrCreate(r.metric.instance); inst.cpuPercent = parseFloat(r.value[1]) || 0; } // Parse CPU count for (const r of cpuCountResult) { const inst = getOrCreate(r.metric.instance); inst.cpuCores = parseFloat(r.value[1]) || 0; } // Parse memory for (const r of memTotalResult) { const inst = getOrCreate(r.metric.instance); inst.memTotal = parseFloat(r.value[1]) || 0; } for (const r of memAvailResult) { const inst = getOrCreate(r.metric.instance); inst.memUsed = inst.memTotal - (parseFloat(r.value[1]) || 0); } // Parse disk for (const r of diskTotalResult) { const inst = getOrCreate(r.metric.instance); inst.diskTotal = parseFloat(r.value[1]) || 0; } for (const r of diskFreeResult) { const inst = getOrCreate(r.metric.instance); inst.diskUsed = inst.diskTotal - (parseFloat(r.value[1]) || 0); } // Parse network rates for (const r of netRxResult) { const inst = getOrCreate(r.metric.instance); inst.netRx = parseFloat(r.value[1]) || 0; } for (const r of netTxResult) { const inst = getOrCreate(r.metric.instance); inst.netTx = parseFloat(r.value[1]) || 0; } // Aggregate let totalCpuUsed = 0, totalCpuCores = 0; let totalMemUsed = 0, totalMemTotal = 0; let totalDiskUsed = 0, totalDiskTotal = 0; let totalNetRx = 0, totalNetTx = 0; let totalTraffic24hRx = 0, totalTraffic24hTx = 0; for (const inst of instances.values()) { totalCpuUsed += (inst.cpuPercent / 100) * inst.cpuCores; totalCpuCores += inst.cpuCores; totalMemUsed += inst.memUsed; totalMemTotal += inst.memTotal; totalDiskUsed += inst.diskUsed; totalDiskTotal += inst.diskTotal; totalNetRx += inst.netRx; totalNetTx += inst.netTx; } // Parse 24h traffic for (const r of traffic24hRxResult) { totalTraffic24hRx += parseFloat(r.value[1]) || 0; } for (const r of traffic24hTxResult) { totalTraffic24hTx += parseFloat(r.value[1]) || 0; } return { totalServers: instances.size, cpu: { used: totalCpuUsed, total: totalCpuCores, percent: totalCpuCores > 0 ? (totalCpuUsed / totalCpuCores * 100) : 0 }, memory: { used: totalMemUsed, total: totalMemTotal, percent: totalMemTotal > 0 ? (totalMemUsed / totalMemTotal * 100) : 0 }, disk: { used: totalDiskUsed, total: totalDiskTotal, percent: totalDiskTotal > 0 ? (totalDiskUsed / totalDiskTotal * 100) : 0 }, network: { rx: totalNetRx, tx: totalNetTx }, traffic24h: { rx: totalTraffic24hRx, tx: totalTraffic24hTx }, servers: Array.from(instances.values()) }; } /** * Get network traffic history (past 24h, 15-min intervals) */ async function getNetworkHistory(url) { const now = Math.floor(Date.now() / 1000); const start = now - 86400; // 24h ago const step = 900; // 15 minutes const [rxResult, txResult] = await Promise.all([ queryRange(url, 'sum(rate(node_network_receive_bytes_total{device!~"lo|veth.*|docker.*|br-.*"}[5m]))', start, now, step ).catch(() => []), queryRange(url, 'sum(rate(node_network_transmit_bytes_total{device!~"lo|veth.*|docker.*|br-.*"}[5m]))', start, now, step ).catch(() => []) ]); // Extract values - each result[0].values = [[timestamp, value], ...] const rxValues = rxResult.length > 0 ? rxResult[0].values : []; const txValues = txResult.length > 0 ? txResult[0].values : []; return { rxValues, txValues }; } /** * Merge network histories from multiple sources */ function mergeNetworkHistories(histories) { const timestampMap = new Map(); for (const history of histories) { for (const [ts, val] of history.rxValues) { const existing = timestampMap.get(ts) || { rx: 0, tx: 0 }; existing.rx += parseFloat(val) || 0; timestampMap.set(ts, existing); } for (const [ts, val] of history.txValues) { const existing = timestampMap.get(ts) || { rx: 0, tx: 0 }; existing.tx += parseFloat(val) || 0; timestampMap.set(ts, existing); } } const sorted = [...timestampMap.entries()].sort((a, b) => a[0] - b[0]); return { timestamps: sorted.map(([ts]) => ts * 1000), // ms for JS rx: sorted.map(([, v]) => v.rx), tx: sorted.map(([, v]) => v.tx) }; } /** * Get CPU usage history (past 1h, 1-min intervals) */ async function getCpuHistory(url) { const now = Math.floor(Date.now() / 1000); const start = now - 3600; // 1h ago const step = 60; // 1 minute const result = await queryRange(url, '100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)', start, now, step ).catch(() => []); const values = result.length > 0 ? result[0].values : []; return values; // [[timestamp, value], ...] } /** * Merge CPU histories from multiple sources (average) */ function mergeCpuHistories(histories) { const timestampMap = new Map(); for (const history of histories) { for (const [ts, val] of history) { const existing = timestampMap.get(ts) || { sum: 0, count: 0 }; existing.sum += parseFloat(val) || 0; existing.count += 1; timestampMap.set(ts, existing); } } const sorted = [...timestampMap.entries()].sort((a, b) => a[0] - b[0]); return { timestamps: sorted.map(([ts]) => ts * 1000), values: sorted.map(([, v]) => v.sum / v.count) }; } module.exports = { testConnection, query, queryRange, getOverviewMetrics, getNetworkHistory, mergeNetworkHistories, getCpuHistory, mergeCpuHistories };