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,228 @@
// 中间件实例可观测面板 — 批量拉取 + 健康度 + 仪表盘 // Logan.Li
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select';
import { Activity, LineChart, Loader2, RefreshCw } from 'lucide-react';
import { middlewareMonitorApi } from '@/api';
import type { MwChartDef, MwTarget, MwTimeRange } from './helpers';
import {
MW_TIME_RANGE_HOURS, fmtMetricDisplay, fmtMemory, fmtHitRate, fmtVal, fmtLastSeen,
mysqlConnPct, redisMemoryPct, redisModeLabel, groupHistoryByMetric, counterToDelta,
evalMwHealth, gaugeTone, gaugeToneLow, REDIS_PRIMARY_KEYS, MYSQL_PRIMARY_KEYS,
} from './helpers';
import { MwTrendChart } from './MwTrendChart';
import { MwGaugeBar } from './MwGaugeBar';
import { MwHealthStrip } from './MwHealthStrip';
import { ChartExpandShell } from '../shared/ChartExpandShell';
interface Props {
target: MwTarget | null;
chartDefs: MwChartDef[];
accent: 'redis' | 'mysql';
}
export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) {
const [timeRange, setTimeRange] = useState<MwTimeRange>('6h');
const [historyMap, setHistoryMap] = useState<Record<string, Array<{ timestamp: number; value: number }>>>({});
const [loading, setLoading] = useState(false);
const [showAll, setShowAll] = useState(false);
const primaryKeys = accent === 'redis' ? REDIS_PRIMARY_KEYS : MYSQL_PRIMARY_KEYS;
const visibleDefs = useMemo(
() => (showAll ? chartDefs : chartDefs.filter((d) => primaryKeys.includes(d.key))),
[showAll, chartDefs, primaryKeys],
);
const loadCharts = useCallback(async () => {
if (!target) return;
setLoading(true);
try {
const res = await middlewareMonitorApi.getMetrics(target.id, {
hours: MW_TIME_RANGE_HOURS[timeRange],
}) as { list?: Array<{ metric_name: string; timestamp: number; value: number }> };
setHistoryMap(groupHistoryByMetric(res?.list || []));
} catch {
setHistoryMap({});
} finally {
setLoading(false);
}
}, [target, timeRange]);
useEffect(() => { loadCharts(); }, [loadCharts]);
// 30s 静默刷新趋势 // Logan.Li
useEffect(() => {
if (!target) return;
const timer = setInterval(loadCharts, 30_000);
return () => clearInterval(timer);
}, [target, loadCharts]);
if (!target) {
return (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] h-full">
<CardContent className="p-8 text-center text-sm text-[var(--dg-text-muted)]">
选择左侧实例查看趋势与健康度
</CardContent>
</Card>
);
}
const m = target.latest_metrics || {};
const memPct = redisMemoryPct(m);
const connPct = mysqlConnPct(m);
const healthIssues = evalMwHealth(target);
return (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] h-full">
<CardContent className="p-4">
<div className="flex items-center justify-between gap-2 mb-3 flex-wrap">
<div className="flex items-center gap-2 min-w-0">
<Activity className="h-4 w-4 text-[var(--dg-text-muted)] shrink-0" />
<h3 className="text-sm font-medium truncate">可观测 · {target.name}</h3>
<Badge variant="outline" className="text-[10px] shrink-0">{target.ip}:{target.port}</Badge>
{accent === 'redis' && (
<Badge variant="outline" className="text-[10px] shrink-0">{redisModeLabel(target.config_public?.redis_mode)}</Badge>
)}
<span className="text-[10px] text-[var(--dg-text-muted)] shrink-0">更新 {fmtLastSeen(target.last_seen_at)}</span>
</div>
<div className="flex items-center gap-1.5">
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => setShowAll((v) => !v)}>
{showAll ? '核心指标' : '全部指标'}
</Button>
<Select value={timeRange} onValueChange={(v) => setTimeRange(v as MwTimeRange)}>
<SelectTrigger className="h-7 w-[92px] text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="1h">近 1 小时</SelectItem>
<SelectItem value="6h">近 6 小时</SelectItem>
<SelectItem value="24h">近 24 小时</SelectItem>
<SelectItem value="7d">近 7 天</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={loadCharts} disabled={loading}>
<RefreshCw className={`h-3 w-3 mr-1 ${loading ? 'animate-spin' : ''}`} />刷新
</Button>
</div>
</div>
<div className="mb-4"><MwHealthStrip issues={healthIssues} /></div>
{/* 仪表盘 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-4">
{accent === 'redis' && (
<>
<MwGaugeBar
label="命中率"
value={(m.redis_hit_rate || 0) * 100}
display={fmtHitRate(m.redis_hit_rate)}
level={m.redis_hit_rate > 0 ? gaugeToneLow((m.redis_hit_rate || 0) * 100, 90, 80) : 'ok'}
/>
<MwGaugeBar
label="内存占用"
value={memPct ?? (m.redis_used_memory > 0 ? 50 : 0)}
display={fmtMemory(m.redis_used_memory)}
level={memPct != null ? gaugeTone(memPct, 80, 90) : 'ok'}
hint={memPct != null ? `已用 ${memPct.toFixed(0)}%` : '未配置 maxmemory'}
/>
<MwGaugeBar
label="连接数"
value={Math.min(100, (m.redis_connected_clients || 0) / 2)}
display={fmtVal(m.redis_connected_clients)}
level={(m.redis_blocked_clients || 0) > 0 ? 'warn' : 'ok'}
hint={m.redis_cluster_known_nodes ? `集群 ${fmtVal(m.redis_cluster_known_nodes)} 节点` : undefined}
/>
</>
)}
{accent === 'mysql' && (
<>
<MwGaugeBar
label="缓冲池命中率"
value={(m.mysql_buffer_hit_rate || 0) * 100}
display={fmtHitRate(m.mysql_buffer_hit_rate)}
level={m.mysql_buffer_hit_rate > 0 ? gaugeToneLow((m.mysql_buffer_hit_rate || 0) * 100, 95, 90) : 'ok'}
/>
<MwGaugeBar
label="连接占用"
value={connPct ?? 0}
display={`${fmtVal(m.mysql_connections)} / ${fmtVal(m.mysql_max_connections)}`}
level={connPct != null ? gaugeTone(connPct, 80, 90) : 'ok'}
/>
<MwGaugeBar
label="活跃线程"
value={Math.min(100, (m.mysql_threads_running || 0) * 5)}
display={fmtVal(m.mysql_threads_running)}
level={(m.mysql_threads_running || 0) > 20 ? 'warn' : 'ok'}
hint={`慢查询 ${fmtVal(m.mysql_slow_queries)}`}
/>
</>
)}
</div>
{/* 核心摘要 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-4">
{accent === 'redis' ? (
<>
<SummaryTile label="QPS" value={fmtVal(m.redis_qps, 1)} />
<SummaryTile label="Key 数" value={fmtVal(m.redis_db_size)} />
<SummaryTile label="逐出 Key" value={fmtVal(m.redis_evicted_keys)} warn={(m.redis_evicted_keys || 0) > 0} />
<SummaryTile label="碎片率" value={m.redis_mem_fragmentation_ratio ? fmtVal(m.redis_mem_fragmentation_ratio, 2) : '--'} />
</>
) : (
<>
<SummaryTile label="QPS" value={fmtVal(m.mysql_qps, 1)} />
<SummaryTile label="接收流量" value={fmtMemory(m.mysql_bytes_received)} />
<SummaryTile label="发送流量" value={fmtMemory(m.mysql_bytes_sent)} />
<SummaryTile label="运行时长" value={m.mysql_uptime ? `${Math.floor(m.mysql_uptime / 3600)}h` : '--'} />
</>
)}
</div>
{loading ? (
<div className="py-10 flex items-center justify-center text-sm text-[var(--dg-text-muted)]">
<Loader2 className="h-4 w-4 animate-spin mr-2" />加载趋势...
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{visibleDefs.map((def) => {
const series = def.counterDelta ? counterToDelta(historyMap[def.key] || []) : (historyMap[def.key] || []);
return (
<ChartExpandShell
key={def.key}
className="rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)] p-3"
title={`${def.label} · ${fmtMetricDisplay(def, m[def.key] ?? 0)}`}
icon={<LineChart className="h-3.5 w-3.5 text-[var(--dg-text-muted)] shrink-0" />}
compactHeight={150}
expandedHeight={400}
empty={series.length === 0}
>
{(h) => (
<MwTrendChart
data={series}
color={def.color}
unit={def.unit}
percent={def.percent}
formatter={def.formatter}
height={h}
/>
)}
</ChartExpandShell>
);
})}
</div>
)}
</CardContent>
</Card>
);
}
function SummaryTile({ label, value, warn }: { label: string; value: string; warn?: boolean }) {
return (
<div className={`rounded-lg p-3 text-center ${warn ? 'bg-amber-500/10' : 'bg-[var(--dg-bg-tertiary)]'}`}>
<p className="text-[10px] text-[var(--dg-text-muted)]">{label}</p>
<p className={`text-sm font-mono font-semibold mt-0.5 ${warn ? 'dg-text-yellow' : ''}`}>{value}</p>
</div>
);
}