// 主机监控工具函数 // Logan.Li import type { BucketPoint, HostMonitorTimeRange } from './types'; import type { JsonData } from '@/types/common'; /** 按时间跨度格式化 X 轴标签(避免 7 天范围只显示时分) // Logan.Li */ export function formatAxisTime(ts: number, spanMinutes: number): string { const d = new Date(ts * 1000); if (spanMinutes <= 180) { return d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false }); } if (spanMinutes <= 2880) { return d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, }); } return d.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }); } function recordTimestamp(r: JsonData): number { const raw = r.report_time || r.created_at; if (!raw) return 0; const t = new Date(raw as string).getTime(); return Number.isNaN(t) ? 0 : t / 1000; } /** 客户端二次过滤,确保图表与所选时间一致 // Logan.Li */ export function filterHistoryByRange(history: JsonData[], range: HostMonitorTimeRange): JsonData[] { if (!history.length) return history; const now = Date.now(); let sinceMs: number; let untilMs = now; if (range.preset === 'custom' && range.start && range.end) { sinceMs = new Date(range.start).getTime(); untilMs = new Date(range.end).getTime(); } else { const mins = range.minutes ?? 60; sinceMs = now - mins * 60_000; } return history.filter((r) => { const ts = recordTimestamp(r) * 1000; return ts >= sinceMs && ts <= untilMs; }); } export function bucketWithStats( raw: number[], timestamps: number[], targetBuckets: number, spanMinutes: number, ): BucketPoint[] { if (!raw || raw.length === 0) return []; const perBucket = Math.max(1, Math.ceil(raw.length / targetBuckets)); const result: BucketPoint[] = []; for (let i = 0; i < raw.length; i += perBucket) { const slice = raw.slice(i, i + perBucket); const tsSlice = timestamps.slice(i, i + perBucket); const sum = slice.reduce((a, b) => a + b, 0); const avg = sum / slice.length; const max = Math.max(...slice); const min = Math.min(...slice); const ts = tsSlice[Math.floor(tsSlice.length / 2)] || 0; result.push({ time: formatAxisTime(ts, spanMinutes), timestamp: ts, avg: Number(avg.toFixed(2)), max: Number(max.toFixed(2)), min: Number(min.toFixed(2)), count: slice.length, }); } return result.slice(-targetBuckets); } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function parseJSON(raw: unknown): JsonData { if (!raw) return {}; if (typeof raw === 'object') return raw; try { return JSON.parse(raw as string); } catch { return {}; } } export function formatBPS(bps: number): string { if (bps >= 1024 * 1024 * 1024) return `${(bps / 1024 / 1024 / 1024).toFixed(1)} GB/s`; if (bps >= 1024 * 1024) return `${(bps / 1024 / 1024).toFixed(1)} MB/s`; if (bps >= 1024) return `${(bps / 1024).toFixed(1)} KB/s`; return `${bps.toFixed(1)} B/s`; } interface TimeConfig { buckets: number; spanMinutes: number; } export function buildMetricsFromHistory(history: JsonData[], timeConfig: TimeConfig) { const spanMinutes = timeConfig.spanMinutes; const bucket = (raw: number[], ts: number[]) => bucketWithStats(raw, ts, timeConfig.buckets, spanMinutes); const rawCpu: number[] = []; const rawMem: number[] = []; const rawLoad1: number[] = []; const rawDisk: number[] = []; const rawTcp: number[] = []; const rawTw: number[] = []; const rawCw: number[] = []; const rawRx: number[] = []; const rawTx: number[] = []; const rawIoR: number[] = []; const rawIoW: number[] = []; const rawSwap: number[] = []; const rawFD: number[] = []; const rawIOWait: number[] = []; const rawMemAvail: number[] = []; const rawIoUtil: number[] = []; const rawConnTotal: number[] = []; const rawRxErr: number[] = []; const rawTxErr: number[] = []; const rawInode: number[] = []; const timestamps: number[] = []; for (const r of history) { const ts = recordTimestamp(r); timestamps.push(ts); rawCpu.push(r.cpu_usage || 0); rawIOWait.push(r.cpu_iowait || 0); rawMem.push(r.mem_usage || 0); rawMemAvail.push(r.mem_available || 0); rawLoad1.push(r.load1 || 0); rawDisk.push(r.disk_usage || 0); const diskInfoArr = parseJSON(r.disk_info); if (Array.isArray(diskInfoArr) && diskInfoArr.length > 0) { const rootDisk = diskInfoArr.find((d: JsonData) => d.mount === '/') || diskInfoArr[0]; rawInode.push(rootDisk?.inode_usage_pct || 0); } else { rawInode.push(0); } const net = parseJSON(r.net_info); rawTcp.push(net.tcp_established || 0); rawTw.push(net.tcp_time_wait || 0); rawCw.push(net.tcp_close_wait || 0); rawConnTotal.push((net.tcp_established || 0) + (net.tcp_time_wait || 0) + (net.tcp_close_wait || 0)); rawRx.push(net.net_rx_bps || 0); rawTx.push(net.net_tx_bps || 0); rawRxErr.push(net.net_rx_errors || 0); rawTxErr.push(net.net_tx_errors || 0); const dio = parseJSON(r.disk_io_info); rawIoR.push(dio.read_bps || 0); rawIoW.push(dio.write_bps || 0); rawIoUtil.push(dio.util || 0); const swap = parseJSON(r.swap_info); rawSwap.push(swap?.usage_percent || 0); const fd = parseJSON(r.fd_info); rawFD.push(fd?.usage_pct || 0); } return { cpu: bucket(rawCpu, timestamps), mem: bucket(rawMem, timestamps), load1: bucket(rawLoad1, timestamps), disk: bucket(rawDisk, timestamps), tcpEst: bucket(rawTcp, timestamps), tcpTW: bucket(rawTw, timestamps), tcpCW: bucket(rawCw, timestamps), rx: bucket(rawRx.map((v) => v / 1024), timestamps), tx: bucket(rawTx.map((v) => v / 1024), timestamps), ioRead: bucket(rawIoR.map((v) => v / 1024), timestamps), ioWrite: bucket(rawIoW.map((v) => v / 1024), timestamps), swap: bucket(rawSwap, timestamps), fd: bucket(rawFD, timestamps), iowait: bucket(rawIOWait, timestamps), memAvail: bucket(rawMemAvail, timestamps), ioUtil: bucket(rawIoUtil, timestamps), connTotal: bucket(rawConnTotal, timestamps), rxErr: bucket(rawRxErr, timestamps), txErr: bucket(rawTxErr, timestamps), inode: bucket(rawInode, timestamps), }; } /** 从过滤后历史计算实际数据时间跨度(用于展示) // Logan.Li */ export function getActualDataSpan(history: JsonData[], range: HostMonitorTimeRange): string | null { if (!history.length) return null; const ts = history.map(recordTimestamp).filter((t) => t > 0); if (!ts.length) return null; const min = Math.min(...ts); const max = Math.max(...ts); const spanMin = Math.max(1, Math.round((max - min) / 60)); return `${formatAxisTime(min, spanMin)} — ${formatAxisTime(max, spanMin)} · ${history.length} 点`; } export function getCurrentVal(latest: JsonData, netInfo: JsonData, diskIOInfo: JsonData, swapInfo: JsonData, fdInfo: JsonData, key: string, fallback = '--', ): string { if (!latest) return fallback; if (key === 'cpu') return latest.cpu_usage != null ? `${latest.cpu_usage.toFixed(1)}%` : fallback; if (key === 'mem') return latest.mem_usage != null ? `${latest.mem_usage.toFixed(1)}%` : fallback; if (key === 'disk') return latest.disk_usage != null ? `${latest.disk_usage.toFixed(1)}%` : fallback; if (key === 'load1') return latest.load1 != null ? String(latest.load1) : fallback; if (key === 'tcp') return netInfo.tcp_established != null ? String(netInfo.tcp_established) : fallback; if (key === 'tw') return netInfo.tcp_time_wait != null ? String(netInfo.tcp_time_wait) : fallback; if (key === 'rx') return netInfo.net_rx_bps != null ? `${(netInfo.net_rx_bps / 1024).toFixed(1)} KB/s` : fallback; if (key === 'tx') return netInfo.net_tx_bps != null ? `${(netInfo.net_tx_bps / 1024).toFixed(1)} KB/s` : fallback; if (key === 'ioR') return diskIOInfo.read_bps != null ? `${formatBPS(diskIOInfo.read_bps)}` : fallback; if (key === 'ioW') return diskIOInfo.write_bps != null ? `${formatBPS(diskIOInfo.write_bps)}` : fallback; if (key === 'swap') return swapInfo?.usage_percent != null ? `${swapInfo.usage_percent.toFixed(1)}%` : fallback; if (key === 'fd') return fdInfo?.usage_pct != null ? `${fdInfo.usage_pct.toFixed(1)}%` : fallback; return fallback; }