feat: 监控告警体系统一重构 + 自动化任务调度引擎 + 监控大屏拆分

告警中心
- 新增统一告警(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>
This commit is contained in:
LoganandClaude committed 2026-07-01 23:44:45 +08:00
1 parent 092d0e4b9e
commit 471353e3d7
190 files changed
+16640 -3484

No files matched your search

@@ -1,8 +1,54 @@
// 主机监控工具函数 // Logan.Li
import type { BucketPoint } from './types';
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' });
}
export function bucketWithStats(raw: number[], timestamps: number[], targetBuckets: number): BucketPoint[] {
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[] = [];
@@ -15,7 +61,7 @@ export function bucketWithStats(raw: number[], timestamps: number[], targetBucke
const min = Math.min(...slice);
const ts = tsSlice[Math.floor(tsSlice.length / 2)] || 0;
result.push({
time: new Date(ts * 1000).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
time: formatAxisTime(ts, spanMinutes),
timestamp: ts,
avg: Number(avg.toFixed(2)),
max: Number(max.toFixed(2)),
@@ -40,9 +86,12 @@ export function formatBPS(bps: number): string {
return `${bps.toFixed(1)} B/s`;
}
interface TimeConfig { buckets: number; }
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[] = [];
@@ -55,7 +104,7 @@ export function buildMetricsFromHistory(history: JsonData[], timeConfig: TimeCon
const timestamps: number[] = [];
for (const r of history) {
const ts = r.report_time ? new Date(r.report_time).getTime() / 1000 : (r.created_at ? new Date(r.created_at).getTime() / 1000 : 0);
const ts = recordTimestamp(r);
timestamps.push(ts);
rawCpu.push(r.cpu_usage || 0);
rawIOWait.push(r.cpu_iowait || 0);
@@ -95,29 +144,40 @@ export function buildMetricsFromHistory(history: JsonData[], timeConfig: TimeCon
}
return {
cpu: bucketWithStats(rawCpu, timestamps, timeConfig.buckets),
mem: bucketWithStats(rawMem, timestamps, timeConfig.buckets),
load1: bucketWithStats(rawLoad1, timestamps, timeConfig.buckets),
disk: bucketWithStats(rawDisk, timestamps, timeConfig.buckets),
tcpEst: bucketWithStats(rawTcp, timestamps, timeConfig.buckets),
tcpTW: bucketWithStats(rawTw, timestamps, timeConfig.buckets),
tcpCW: bucketWithStats(rawCw, timestamps, timeConfig.buckets),
rx: bucketWithStats(rawRx.map(v => v / 1024), timestamps, timeConfig.buckets),
tx: bucketWithStats(rawTx.map(v => v / 1024), timestamps, timeConfig.buckets),
ioRead: bucketWithStats(rawIoR.map(v => v / 1024), timestamps, timeConfig.buckets),
ioWrite: bucketWithStats(rawIoW.map(v => v / 1024), timestamps, timeConfig.buckets),
swap: bucketWithStats(rawSwap, timestamps, timeConfig.buckets),
fd: bucketWithStats(rawFD, timestamps, timeConfig.buckets),
iowait: bucketWithStats(rawIOWait, timestamps, timeConfig.buckets),
memAvail: bucketWithStats(rawMemAvail, timestamps, timeConfig.buckets),
ioUtil: bucketWithStats(rawIoUtil, timestamps, timeConfig.buckets),
connTotal: bucketWithStats(rawConnTotal, timestamps, timeConfig.buckets),
rxErr: bucketWithStats(rawRxErr, timestamps, timeConfig.buckets),
txErr: bucketWithStats(rawTxErr, timestamps, timeConfig.buckets),
inode: bucketWithStats(rawInode, timestamps, timeConfig.buckets),
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 {