告警中心 - 新增统一告警(alert_unified)、监控告警规则(monitor_alert_rule)、指标目录(alert_metric_catalog) - 通知策略与消息分发(alert_notify_policy/dispatch/message)、静默规则 - 告警引擎按阈值/SNMP 拆分(alert_engine_monitor/snmp) 监控采集 - SNMP 常量/模板与历史指标(snmp templates/constants、snmp_metrics_history、snmp_collect) - 中间件监控 Redis 采集与种子(middleware_monitor_redis/seed) - 站点探测 pkg(siteprobe) + 站点视图(site_view) 自动化任务 - cron 调度器(cronscheduler)、脚本运行器(scriptrunner)、系统任务(systask)及运行记录(cron_job_run、system_task) 审计与凭证 - 审计设置(audit_setting) + 历史清理(auditcleaner) 前端 - 监控页面(host/middleware/site/snmp)拆分为子模块并大屏化 - 告警中心多 Tab(规则/指标目录/通知策略/静默)、任务管理组件重构、Agent CLI Markdown 渲染 - 移除旧脚本卡片/详情/日志组件,统一为 ScriptTable/ScriptCodeEditor/TerminalOutput Co-Authored-By: Claude <noreply@anthropic.com>
198 lines
8.2 KiB
TypeScript
198 lines
8.2 KiB
TypeScript
// 主机监控工具函数 // 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;
|
|
} |