diff --git a/public/js/app.js b/public/js/app.js index ca71310..9a9aeb8 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -489,7 +489,9 @@ { key: 'swapUsedPct', label: 'SWAP 使用率', value: formatPercent(data.swapUsedPct) }, { key: 'rootFsUsedPct', label: '根分区使用率 (/)', value: formatPercent(data.rootFsUsedPct) }, { key: 'netRx', label: '网络接收速率 (RX)', value: formatBandwidth(data.netRx) }, - { key: 'netTx', label: '网络发送速率 (TX)', value: formatBandwidth(data.netTx) } + { key: 'netTx', label: '网络发送速率 (TX)', value: formatBandwidth(data.netTx) }, + { key: 'sockstatTcp', label: 'TCP 链接数 (Sockstat)', value: data.sockstatTcp.toFixed(0) }, + { key: 'sockstatTcpMem', label: 'TCP 内存占用', value: formatBytes(data.sockstatTcpMem) } ]; dom.detailMetricsList.innerHTML = metrics.map(m => ` @@ -565,7 +567,8 @@ let unit = ''; if (metricKey.includes('Pct') || metricKey === 'cpuBusy') unit = '%'; if (metricKey.startsWith('net')) unit = 'B/s'; - + if (metricKey === 'sockstatTcpMem') unit = 'B'; + chart = new MetricChart(canvas, unit); currentServerDetail.charts[metricKey] = chart; } diff --git a/public/js/chart.js b/public/js/chart.js index e04aa18..c7c0b8b 100644 --- a/public/js/chart.js +++ b/public/js/chart.js @@ -380,8 +380,15 @@ class MetricChart { ctx.textAlign = 'right'; let label = ''; - if (this.unit === 'B/s') { - label = window.formatBandwidth ? window.formatBandwidth(v) : v.toFixed(0); + if (this.unit === 'B/s' || this.unit === 'B') { + const isRate = this.unit === 'B/s'; + if (window.formatBandwidth && isRate) { + label = window.formatBandwidth(v); + } else if (window.formatBytes) { + label = window.formatBytes(v) + (isRate ? '/s' : ''); + } else { + label = v.toFixed(0) + this.unit; + } } else { label = (v >= 1000 ? (v / 1000).toFixed(1) + 'k' : v.toFixed(v < 10 && v > 0 ? 1 : 0)) + this.unit; } diff --git a/server/prometheus-service.js b/server/prometheus-service.js index b66383c..3f772b4 100644 --- a/server/prometheus-service.js +++ b/server/prometheus-service.js @@ -5,9 +5,21 @@ const https = require('https'); const QUERY_TIMEOUT = 10000; // Reusable agents to handle potential redirect issues and protocol mismatches +const crypto = require('crypto'); const httpAgent = new http.Agent({ keepAlive: true }); const httpsAgent = new https.Agent({ keepAlive: true, rejectUnauthorized: false }); +const serverIdMap = new Map(); // token -> { instance, job, source } +const SECRET = crypto.randomBytes(16).toString('hex'); + +function getServerToken(instance) { + const hash = crypto.createHmac('sha256', SECRET) + .update(instance) + .digest('hex') + .substring(0, 16); + return hash; +} + /** * Normalize URL and ensure protocol */ @@ -180,10 +192,16 @@ async function getOverviewMetrics(url, sourceName) { const instances = new Map(); const getOrCreate = (metric) => { - const key = metric.instance; - if (!instances.has(key)) { - instances.set(key, { - instance: key, + const originalInstance = metric.instance; + const token = getServerToken(originalInstance, sourceName); + + // Store mapping for detail queries + serverIdMap.set(token, { instance: originalInstance, source: sourceName, job: metric.job }); + + if (!instances.has(token)) { + instances.set(token, { + instance: token, // This is the masked IP SENT TO FRONTEND + originalInstance, // Keep internal for aggregation/parsing job: metric.job || 'Unknown', source: sourceName, cpuPercent: 0, @@ -197,7 +215,7 @@ async function getOverviewMetrics(url, sourceName) { up: false }); } - const inst = instances.get(key); + const inst = instances.get(token); // If job was Unknown but we now have a job name, update it if (inst.job === 'Unknown' && metric.job) { inst.job = metric.job; @@ -311,9 +329,13 @@ async function getOverviewMetrics(url, sourceName) { }, traffic24h: { rx: totalTraffic24hRx, - tx: totalTraffic24hTx + tx: totalTraffic24hTx, + total: totalTraffic24hRx + totalTraffic24hTx }, - servers: Array.from(instances.values()) + servers: Array.from(instances.values()).map(s => { + const { originalInstance, ...rest } = s; + return rest; + }) }; } @@ -412,12 +434,19 @@ function mergeCpuHistories(histories) { } +function resolveToken(token) { + if (serverIdMap.has(token)) { + return serverIdMap.get(token).instance; + } + return token; +} + /** * Get detailed metrics for a specific server (node) */ async function getServerDetails(baseUrl, instance, job) { const url = normalizeUrl(baseUrl); - const node = instance; + const node = resolveToken(instance); // Queries based on the requested dashboard structure const queries = { @@ -438,7 +467,9 @@ async function getServerDetails(baseUrl, instance, job) { memTotal: `node_memory_MemTotal_bytes{instance="${node}",job="${job}"}`, uptime: `node_time_seconds{instance="${node}",job="${job}"} - node_boot_time_seconds{instance="${node}",job="${job}"}`, netRx: `sum(rate(node_network_receive_bytes_total{instance="${node}",job="${job}",device!~'tap.*|veth.*|br.*|docker.*|virbr*|podman.*|lo.*|vmbr.*|fwbr.|ip.*|gre.*|virbr.*|vnet.*'}[1m]))`, - netTx: `sum(rate(node_network_transmit_bytes_total{instance="${node}",job="${job}",device!~'tap.*|veth.*|br.*|docker.*|virbr*|podman.*|lo.*|vmbr.*|fwbr.|ip.*|gre.*|virbr.*|vnet.*'}[1m]))` + netTx: `sum(rate(node_network_transmit_bytes_total{instance="${node}",job="${job}",device!~'tap.*|veth.*|br.*|docker.*|virbr*|podman.*|lo.*|vmbr.*|fwbr.|ip.*|gre.*|virbr.*|vnet.*'}[1m]))`, + sockstatTcp: `node_sockstat_TCP_inuse{instance="${node}",job="${job}"}`, + sockstatTcpMem: `node_sockstat_TCP_mem{instance="${node}",job="${job}"} * 4096` // Converting pages to bytes (assuming 4KB pages) }; const results = {}; @@ -461,7 +492,7 @@ async function getServerDetails(baseUrl, instance, job) { */ async function getServerHistory(baseUrl, instance, job, metric, range = '1h', start = null, end = null) { const url = normalizeUrl(baseUrl); - const node = instance; + const node = resolveToken(instance); // Custom multi-metric handler for CPU Busy if (metric === 'cpuBusy') { @@ -504,7 +535,9 @@ async function getServerHistory(baseUrl, instance, job, metric, range = '1h', st swapUsedPct: `((node_memory_SwapTotal_bytes{instance="${node}",job="${job}"} - node_memory_SwapFree_bytes{instance="${node}",job="${job}"}) / (node_memory_SwapTotal_bytes{instance="${node}",job="${job}"})) * 100`, rootFsUsedPct: `100 - ((node_filesystem_avail_bytes{instance="${node}",job="${job}",mountpoint="/",fstype!="rootfs"} * 100) / node_filesystem_size_bytes{instance="${node}",job="${job}",mountpoint="/",fstype!="rootfs"})`, netRx: `sum(rate(node_network_receive_bytes_total{instance="${node}",job="${job}",device!~'tap.*|veth.*|br.*|docker.*|virbr*|podman.*|lo.*|vmbr.*|fwbr.|ip.*|gre.*|virbr.*|vnet.*'}[1m]))`, - netTx: `sum(rate(node_network_transmit_bytes_total{instance="${node}",job="${job}",device!~'tap.*|veth.*|br.*|docker.*|virbr*|podman.*|lo.*|vmbr.*|fwbr.|ip.*|gre.*|virbr.*|vnet.*'}[1m]))` + netTx: `sum(rate(node_network_transmit_bytes_total{instance="${node}",job="${job}",device!~'tap.*|veth.*|br.*|docker.*|virbr*|podman.*|lo.*|vmbr.*|fwbr.|ip.*|gre.*|virbr.*|vnet.*'}[1m]))`, + sockstatTcp: `node_sockstat_TCP_inuse{instance="${node}",job="${job}"}`, + sockstatTcpMem: `node_sockstat_TCP_mem{instance="${node}",job="${job}"} * 4096` }; const expr = metricMap[metric];