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

@@ -0,0 +1,205 @@
// 站点拨测可观测分析 — 按协议聚合指标与异常事件 // Logan.Li
import type { SiteResult, SiteTask } from '../helpers';
import { fmtMs, fmtTime, normalizeProbeType } from '../helpers';
export interface ProbeEvent {
id: string;
at: string;
timeLabel: string;
severity: 'info' | 'warn' | 'error';
title: string;
detail?: string;
}
export interface HttpObsSummary {
expectedCode: number;
okCount: number;
err4xx: number;
err5xx: number;
otherErr: number;
lastCode?: number;
lastCodeTone: string;
events: ProbeEvent[];
chart: Array<{ time: string; code: number; tone: string; ok: boolean }>;
}
export interface SslObsSummary {
expiry?: string;
issuer?: string;
days?: number;
daysTone: string;
handshakeMs: number;
events: ProbeEvent[];
chart: Array<{ time: string; days: number }>;
}
export interface LatencyObsSummary {
avg: number;
min: number;
max: number;
p95: number;
jitter: number;
spikeCount: number;
events: ProbeEvent[];
chart: Array<{ time: string; latency: number; failed: boolean; spike: boolean }>;
}
export interface ConnectivityObsSummary {
upRate: string;
failCount: number;
lastFailAt?: string;
events: ProbeEvent[];
chart: Array<{ time: string; up: number; failed: boolean }>;
}
const tooltipOrder = (results: SiteResult[]) => [...results].reverse();
function httpCodeTone(code: number, expected: number): string {
if (code === expected || (expected === 200 && code >= 200 && code < 300)) return 'dg-text-green';
if (code >= 500) return 'dg-text-red';
if (code >= 400) return 'dg-text-yellow';
return 'dg-text-red';
}
function pushEvent(events: ProbeEvent[], r: SiteResult, title: string, severity: ProbeEvent['severity'], detail?: string) {
events.push({
id: `${r.id}-${title}`,
at: r.checked_at,
timeLabel: fmtTime(r.checked_at),
severity,
title,
detail,
});
}
export function analyzeHttpResults(results: SiteResult[], task: SiteTask): HttpObsSummary {
const expected = task.expected_status_code || 200;
const chart = tooltipOrder(results).map((r) => {
const code = r.http_status_code ?? (r.status === 'success' ? expected : 0);
const ok = r.status === 'success' && (code === expected || (expected === 200 && code >= 200 && code < 300));
return { time: fmtTime(r.checked_at).slice(-5), code, tone: code > 0 ? httpCodeTone(code, expected) : 'dg-text-red', ok };
});
let okCount = 0;
let err4xx = 0;
let err5xx = 0;
let otherErr = 0;
const events: ProbeEvent[] = [];
for (const r of results) {
const code = r.http_status_code ?? 0;
if (r.status === 'success' && (code === expected || (expected === 200 && code >= 200 && code < 300))) {
okCount++;
continue;
}
if (code >= 500) {
err5xx++;
pushEvent(events, r, `HTTP ${code}`, 'error', r.error_message || '服务端错误');
} else if (code >= 400) {
err4xx++;
pushEvent(events, r, `HTTP ${code}`, 'warn', r.error_message || '客户端/鉴权错误');
} else if (r.status === 'timeout') {
otherErr++;
pushEvent(events, r, '请求超时', 'error', r.error_message);
} else {
otherErr++;
pushEvent(events, r, code > 0 ? `HTTP ${code}` : '请求失败', 'error', r.error_message);
}
}
const latest = results[0];
const lastCode = latest?.http_status_code;
return {
expectedCode: expected,
okCount,
err4xx,
err5xx,
otherErr,
lastCode,
lastCodeTone: lastCode ? httpCodeTone(lastCode, expected) : 'dg-text-red',
events: events.slice(0, 8),
chart,
};
}
export function analyzeSslResults(results: SiteResult[], task: SiteTask): SslObsSummary {
const latest = results.find((r) => r.ssl_days_remaining != null) ?? results[0];
const days = latest?.ssl_days_remaining ?? task.ssl_days_remaining;
const expiry = latest?.ssl_cert_expiry?.slice(0, 10) ?? task.ssl_cert_expiry?.slice(0, 10);
const issuer = latest?.ssl_cert_issuer;
const events: ProbeEvent[] = [];
for (const r of results) {
if (r.status !== 'success') {
pushEvent(events, r, 'TLS 握手失败', 'error', r.error_message);
} else if (r.ssl_days_remaining != null && r.ssl_days_remaining < 30) {
pushEvent(events, r, `证书剩余 ${r.ssl_days_remaining} 天`, r.ssl_days_remaining <= 7 ? 'error' : 'warn', expiry);
}
}
const chart = tooltipOrder(results)
.filter((r) => r.ssl_days_remaining != null)
.map((r) => ({ time: fmtTime(r.checked_at).slice(-5), days: r.ssl_days_remaining as number }));
const tone = days == null ? 'text-[var(--dg-text-muted)]' : days <= 7 ? 'dg-text-red' : days <= 30 ? 'dg-text-yellow' : 'dg-text-green';
const handshakeMs = results.length
? results.reduce((s, r) => s + (r.response_time || r.total_time || 0), 0) / results.length
: 0;
return { expiry, issuer, days, daysTone: tone, handshakeMs, events: events.slice(0, 8), chart };
}
function analyzeLatencyResults(results: SiteResult[], label: string): LatencyObsSummary {
const latencies = results.map((r) => r.response_time ?? r.total_time ?? 0).filter((v) => v > 0);
const sorted = [...latencies].sort((a, b) => a - b);
const avg = latencies.length ? latencies.reduce((a, b) => a + b, 0) / latencies.length : 0;
const min = sorted[0] ?? 0;
const max = sorted[sorted.length - 1] ?? 0;
const p95 = sorted.length ? sorted[Math.floor(sorted.length * 0.95)] ?? max : 0;
const jitter = max - min;
const spikeThreshold = Math.max(avg * 1.8, avg + 50);
const events: ProbeEvent[] = [];
let spikeCount = 0;
const chart = tooltipOrder(results).map((r) => {
const latency = r.response_time ?? r.total_time ?? 0;
const failed = r.status !== 'success';
const spike = !failed && latency > spikeThreshold;
if (failed) pushEvent(events, r, `${label}失败`, 'error', r.error_message || r.remote_ip);
else if (spike) {
spikeCount++;
pushEvent(events, r, `${label}延迟波动`, 'warn', fmtMs(latency));
}
return { time: fmtTime(r.checked_at).slice(-5), latency, failed, spike };
});
return { avg, min, max, p95, jitter, spikeCount, events: events.slice(0, 8), chart };
}
export function analyzePingResults(results: SiteResult[]): LatencyObsSummary {
return analyzeLatencyResults(results, 'PING');
}
export function analyzeDnsResults(results: SiteResult[]): LatencyObsSummary {
return analyzeLatencyResults(results, 'DNS 解析');
}
export function analyzeTcpResults(results: SiteResult[]): ConnectivityObsSummary {
const successCount = results.filter((r) => r.status === 'success').length;
const failCount = results.length - successCount;
const upRate = results.length > 0 ? `${(successCount / results.length * 100).toFixed(1)}%` : '--';
const events: ProbeEvent[] = [];
let lastFailAt: string | undefined;
const chart = tooltipOrder(results).map((r) => {
const failed = r.status !== 'success';
if (failed) {
lastFailAt = r.checked_at;
pushEvent(events, r, 'TCP 不通', 'error', r.error_message || '连接失败');
}
return { time: fmtTime(r.checked_at).slice(-5), up: failed ? 0 : 100, failed };
});
return { upRate, failCount, lastFailAt: lastFailAt ? fmtTime(lastFailAt) : undefined, events: events.slice(0, 8), chart };
}
export function getProbeObsSummary(task: SiteTask, results: SiteResult[]) {
const type = normalizeProbeType(task.type);
switch (type) {
case 'HTTP': return { type, http: analyzeHttpResults(results, task) } as const;
case 'SSL': return { type, ssl: analyzeSslResults(results, task) } as const;
case 'PING': return { type, latency: analyzePingResults(results) } as const;
case 'DNS': return { type, latency: analyzeDnsResults(results) } as const;
case 'TCP': return { type, tcp: analyzeTcpResults(results) } as const;
default: return { type: 'HTTP' as const, http: analyzeHttpResults(results, task) };
}
}