feat(alert): A2 告警引擎分布式安全 — Redis 分布式锁(SetNX)
- Cache 接口新增 SetNX 方法(go-redis 原子 / redka check-then-set 单副本安全) - AlertEngine 改造为支持分布式锁: tryAcquireLock/releaseLock/GetEngineStatus - 多副本部署时仅 Leader 执行告警检查, 60s TTL 防死锁, fail-open 容错 - 新增 GET /alerts/engine/status 监控端点(is_leader/instance_id/lock_holder) - 修复 GrafanaDashboard.tsx Panel.options 类型不匹配导致构建中断 - 同步未提交的前端类型安全迁移(140+ 文件) - ROADMAP/TDD_PLAN/PROJECT_EVALUATION 三文档同步更新 验证: go build 零错误, Docker 部署 healthy, 引擎状态端点正常, 锁竞争测试通过(非 Leader 跳过检查), 5/5 核心 API 回归通过
This commit is contained in:
1 parent
5ca0e6d83a
commit
c68aef4444
150 files changed
+1038
-690
No files matched your search
@@ -93,7 +93,7 @@ const AgentTab: React.FC = () => {
|
||||
finally { setBatchUninstalling(false); }
|
||||
};
|
||||
const totalPages = Math.max(1, Math.ceil(hosts.length / pageSize));
|
||||
const fetch = async () => { try { setHosts(asList(await agentApi.getLatest())); } catch {} finally { setLoading(false); } };
|
||||
const fetch = async () => { try { setHosts(asList(await agentApi.getLatest()) as typeof hosts); } catch {} finally { setLoading(false); } };
|
||||
useEffect(() => { fetch(); const t = setInterval(fetch, 30000); return () => clearInterval(t); }, []);
|
||||
const toggle = (f: SortField) => { if (sf === f) setSd(d => d === 'asc' ? 'desc' : 'asc'); else { setSf(f); setSd('desc'); } };
|
||||
const sorted = [...hosts].sort((a, b) => {
|
||||
@@ -419,7 +419,7 @@ const PrometheusTab: React.FC = () => {
|
||||
|
||||
// ═══════════ 自定义仪表盘 ═══════════
|
||||
interface Dashboard { id: string; name: string; description: string; datasource: string; datasourceUrl: string; panels: Panel[]; createdAt: string; updatedAt: string; createdBy: string; isDefault: boolean; }
|
||||
export interface Panel { id: string; title: string; type: string; query: string; datasource: string; width: number; height: number; x: number; y: number; options: any; }
|
||||
export interface Panel { id: string; title: string; type: string; query: string; datasource: string; width: number; height: number; x: number; y: number; options?: any; }
|
||||
|
||||
const DashboardTab: React.FC = () => {
|
||||
const [dashboards, setDashboards] = useState<Dashboard[]>([]);
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { serverApi } from '@/api';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
type SortField = 'hostname' | 'ip' | 'agent_version' | 'agent_status' | 'cpu_usage' | 'memory_usage' | 'disk_usage' | 'region';
|
||||
type SortDir = 'asc' | 'desc';
|
||||
@@ -48,7 +49,7 @@ function getUsageBar(val: number) {
|
||||
|
||||
export default function HostMonitorDashboard() {
|
||||
const navigate = useNavigate();
|
||||
const [hosts, setHosts] = useState<any[]>([]);
|
||||
const [hosts, setHosts] = useState<JsonData[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
@@ -67,16 +68,16 @@ export default function HostMonitorDashboard() {
|
||||
if (isRefresh) setRefreshing(true);
|
||||
else setLoading(true);
|
||||
try {
|
||||
const params: Record<string, any> = { page, page_size: pageSize };
|
||||
const params: JsonData = { page, page_size: pageSize };
|
||||
if (search.trim()) params.keyword = search.trim();
|
||||
const res = await serverApi.getServers(params);
|
||||
const list = (res as Record<string, any>)?.list || [];
|
||||
const list = (res as JsonData)?.list || [];
|
||||
setHosts(list);
|
||||
setTotal((res as Record<string, any>)?.total || 0);
|
||||
setTotal((res as JsonData)?.total || 0);
|
||||
|
||||
// 统计
|
||||
const online = list.filter(( h: any) => h.agent_status === 'online').length;
|
||||
setStats({ online, offline: list.length - online, total: (res as Record<string, any>)?.total || 0 });
|
||||
const online = list.filter((h: JsonData) => h.agent_status === 'online').length;
|
||||
setStats({ online, offline: list.length - online, total: (res as JsonData)?.total || 0 });
|
||||
} catch {
|
||||
setHosts([]);
|
||||
setTotal(0);
|
||||
@@ -89,8 +90,8 @@ export default function HostMonitorDashboard() {
|
||||
useEffect(() => { loadData(); }, [loadData]);
|
||||
|
||||
// 排序后的数据
|
||||
const sorted = [...hosts].sort(( a: any, b: any) => {
|
||||
let va: Record<string, any>, vb: any;
|
||||
const sorted = [...hosts].sort((a: JsonData, b: JsonData) => {
|
||||
let va: JsonData | undefined, vb: JsonData | undefined;
|
||||
if (sortField === 'region') {
|
||||
va = (a.region?.name || a.region?.Name || '').toLowerCase();
|
||||
vb = (b.region?.name || b.region?.Name || '').toLowerCase();
|
||||
@@ -178,9 +179,9 @@ export default function HostMonitorDashboard() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
{(() => {
|
||||
const onlines = hosts.filter(( h: any) => h.agent_status === 'online');
|
||||
const avgCpu = onlines.length > 0 ? onlines.reduce((s: number, h: any) => s + (h.cpu_usage || 0), 0) / onlines.length : 0;
|
||||
const avgMem = onlines.length > 0 ? onlines.reduce((s: number, h: any) => s + (h.memory_usage || 0), 0) / onlines.length : 0;
|
||||
const onlines = hosts.filter((h: JsonData) => h.agent_status === 'online');
|
||||
const avgCpu = onlines.length > 0 ? onlines.reduce((s: number, h: JsonData) => s + (h.cpu_usage || 0), 0) / onlines.length : 0;
|
||||
const avgMem = onlines.length > 0 ? onlines.reduce((s: number, h: JsonData) => s + (h.memory_usage || 0), 0) / onlines.length : 0;
|
||||
const cpuColor = avgCpu > 85 ? 'text-red-400' : avgCpu > 60 ? 'text-amber-400' : 'text-emerald-400';
|
||||
const memColor = avgMem > 85 ? 'text-red-400' : avgMem > 60 ? 'text-amber-400' : 'text-emerald-400';
|
||||
return (<>
|
||||
@@ -207,7 +208,7 @@ export default function HostMonitorDashboard() {
|
||||
</div>
|
||||
|
||||
{/* 资源条状图 — Grafana Bargauge 风格 (参照 K8s Node CPU/Memory Ratio) */}
|
||||
{hosts.filter(( h: any) => h.agent_status === 'online').length > 0 && (
|
||||
{hosts.filter((h: JsonData) => h.agent_status === 'online').length > 0 && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* CPU 排行 */}
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
@@ -220,10 +221,10 @@ export default function HostMonitorDashboard() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{[...hosts]
|
||||
.filter(( h: any) => h.agent_status === 'online')
|
||||
.sort(( a: any, b: any) => (b.cpu_usage || 0) - (a.cpu_usage || 0))
|
||||
.filter((h: JsonData) => h.agent_status === 'online')
|
||||
.sort((a: JsonData, b: JsonData) => (b.cpu_usage || 0) - (a.cpu_usage || 0))
|
||||
.slice(0, 5)
|
||||
.map(( h: any) => {
|
||||
.map((h: JsonData) => {
|
||||
const val = h.cpu_usage || 0;
|
||||
return (
|
||||
<div key={h.id} className="flex items-center gap-2">
|
||||
@@ -261,10 +262,10 @@ export default function HostMonitorDashboard() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{[...hosts]
|
||||
.filter(( h: any) => h.agent_status === 'online')
|
||||
.sort(( a: any, b: any) => (b.memory_usage || 0) - (a.memory_usage || 0))
|
||||
.filter((h: JsonData) => h.agent_status === 'online')
|
||||
.sort((a: JsonData, b: JsonData) => (b.memory_usage || 0) - (a.memory_usage || 0))
|
||||
.slice(0, 5)
|
||||
.map(( h: any) => {
|
||||
.map((h: JsonData) => {
|
||||
const val = h.memory_usage || 0;
|
||||
return (
|
||||
<div key={h.id} className="flex items-center gap-2">
|
||||
@@ -363,7 +364,7 @@ export default function HostMonitorDashboard() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sorted.map(( h: any) => (
|
||||
{sorted.map((h: JsonData) => (
|
||||
<tr key={h.id} className="border-b border-[var(--dg-border)]/50 hover:bg-[var(--dg-bg-tertiary)]/50 transition-colors">
|
||||
{/* 主机名/ID */}
|
||||
<td className="px-3 py-2">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { StatStrip } from '@/components/ui/stat-strip';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { prometheusProxyApi } from '@/api';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
interface Props { dsUrl?: string; }
|
||||
|
||||
@@ -36,17 +37,17 @@ const fmtBps = (b: number) => {
|
||||
return `${b.toFixed(1)} B/s`;
|
||||
};
|
||||
|
||||
function parsePrune( result: any[]): any[] {
|
||||
return (result || []).map(( r: any) => {
|
||||
const m: Record<string, any> = { ...(r.metric || {}) };
|
||||
function parsePrune( result: JsonData[]): JsonData[] {
|
||||
return (result || []).map((r: JsonData) => {
|
||||
const m: JsonData = { ...(r.metric || {}) };
|
||||
if (r.value) m.value = r.value[1];
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
const Box = ( p: any) => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>;
|
||||
const Box = (p: JsonData) => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>;
|
||||
|
||||
const K8sPanel: React.FC<{ d: Record<string, any>}> = ({ d }) => (
|
||||
const K8sPanel: React.FC<{ d: JsonData}> = ({ d }) => (
|
||||
<div className="space-y-4">
|
||||
<StatStrip
|
||||
items={[
|
||||
@@ -58,27 +59,27 @@ const K8sPanel: React.FC<{ d: Record<string, any>}> = ({ d }) => (
|
||||
/>
|
||||
<div className="rounded-lg bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] p-3 max-h-60 overflow-y-auto">
|
||||
<div className="text-xs font-medium mb-2 flex items-center gap-1"><Cpu className="w-3 h-3 text-purple-400" />节点 CPU / 内存利用率</div>
|
||||
{(d.nodeCPU || []).map(( n: any, i: number) => {
|
||||
const mem = (d.nodeMem || []).find(( m: any) => m.node === n.node);
|
||||
{(d.nodeCPU || []).map((n: JsonData, i: number) => {
|
||||
const mem = (d.nodeMem || []).find((m: JsonData) => m.node === n.node);
|
||||
return <div key={i} className="flex items-center gap-3 text-[11px] mb-1.5"><span className="w-28 truncate" title={n.node}>{n.node}</span><MiniBar val={Number(n.value||0)} label={`${Number(n.value||0).toFixed(1)}%`} /><MiniBar val={Number(mem?.value||0)} label={`${Number(mem?.value||0).toFixed(1)}%`} color="bg-blue-500" /></div>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const NodePanel: React.FC<{ d: Record<string, any>}> = ({ d }) => (
|
||||
const NodePanel: React.FC<{ d: JsonData}> = ({ d }) => (
|
||||
<div className="overflow-x-auto rounded-lg border border-[var(--dg-border)]">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-[var(--dg-bg-tertiary)]"><tr><th className="px-3 py-2 text-left">节点</th><th className="px-3 py-2 text-left">状态</th><th className="px-3 py-2 text-left">CPU</th><th className="px-3 py-2 text-left">内存</th><th className="px-3 py-2 text-left">磁盘</th><th className="px-3 py-2 text-left">流入</th><th className="px-3 py-2 text-left">流出</th></tr></thead>
|
||||
<tbody>
|
||||
{(d.nodes || []).map(( n: any, i: number) => {
|
||||
{(d.nodes || []).map((n: JsonData, i: number) => {
|
||||
const inst = n.instance || n.exported_instance || '';
|
||||
const cpu = (d.cpuUsage||[]).find(( x: any)=>x.instance===inst);
|
||||
const mem = (d.memUsage||[]).find(( x: any)=>x.instance===inst);
|
||||
const disk = (d.diskUsage||[]).find(( x: any)=>x.instance===inst);
|
||||
const up = (d.up||[]).find(( x: any)=>x.instance===inst);
|
||||
const rx = (d.netRx||[]).find(( x: any)=>x.instance===inst);
|
||||
const tx = (d.netTx||[]).find(( x: any)=>x.instance===inst);
|
||||
const cpu = (d.cpuUsage||[]).find((x: JsonData)=>x.instance===inst);
|
||||
const mem = (d.memUsage||[]).find((x: JsonData)=>x.instance===inst);
|
||||
const disk = (d.diskUsage||[]).find((x: JsonData)=>x.instance===inst);
|
||||
const up = (d.up||[]).find((x: JsonData)=>x.instance===inst);
|
||||
const rx = (d.netRx||[]).find((x: JsonData)=>x.instance===inst);
|
||||
const tx = (d.netTx||[]).find((x: JsonData)=>x.instance===inst);
|
||||
return <tr key={i} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-3 py-2 font-medium">{n.nodename||inst}</td>
|
||||
<td className="px-3 py-2"><Badge className={`text-[10px] ${up?.value===1?'bg-emerald-500/20 text-emerald-400':'bg-red-500/20 text-red-400'}`}>{up?.value===1?'UP':'DOWN'}</Badge></td>
|
||||
@@ -94,7 +95,7 @@ const NodePanel: React.FC<{ d: Record<string, any>}> = ({ d }) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const PodPanel: React.FC<{ d: Record<string, any>}> = ({ d }) => (
|
||||
const PodPanel: React.FC<{ d: JsonData}> = ({ d }) => (
|
||||
<div className="space-y-4">
|
||||
<StatStrip
|
||||
items={[
|
||||
@@ -102,12 +103,12 @@ const PodPanel: React.FC<{ d: Record<string, any>}> = ({ d }) => (
|
||||
{ label: 'Pending', value: d.pending?.[0]?.value || 0, color: 'amber' },
|
||||
]}
|
||||
/>
|
||||
<div className="rounded-lg bg-[var(--dg-bg-primary)] p-3 max-h-40 overflow-y-auto"><div className="text-xs font-medium mb-2">CPU Top 10</div>{(d.cpuTop||[]).map(( p: any,i:number)=><div key={i} className="flex items-center gap-2 text-[11px] mb-1"><span className="w-10 text-right text-[var(--dg-text-muted)]">{i+1}</span><span className="w-32 truncate" title={p.pod}>{p.pod}</span><MiniBar val={Number(p.value||0)*100} label={`${Number(p.value||0).toFixed(2)}c`} color="bg-purple-500" /></div>)}</div>
|
||||
<div className="rounded-lg bg-[var(--dg-bg-primary)] p-3 max-h-40 overflow-y-auto"><div className="text-xs font-medium mb-2">内存 Top 10</div>{(d.memTop||[]).map(( p: any,i:number)=><div key={i} className="flex items-center gap-2 text-[11px] mb-1"><span className="w-10 text-right text-[var(--dg-text-muted)]">{i+1}</span><span className="w-32 truncate" title={p.pod}>{p.pod}</span><span className="ml-auto">{fmtBytes(Number(p.value||0))}</span></div>)}</div>
|
||||
<div className="rounded-lg bg-[var(--dg-bg-primary)] p-3 max-h-40 overflow-y-auto"><div className="text-xs font-medium mb-2">CPU Top 10</div>{(d.cpuTop||[]).map((p: JsonData, i:number)=><div key={i} className="flex items-center gap-2 text-[11px] mb-1"><span className="w-10 text-right text-[var(--dg-text-muted)]">{i+1}</span><span className="w-32 truncate" title={p.pod}>{p.pod}</span><MiniBar val={Number(p.value||0)*100} label={`${Number(p.value||0).toFixed(2)}c`} color="bg-purple-500" /></div>)}</div>
|
||||
<div className="rounded-lg bg-[var(--dg-bg-primary)] p-3 max-h-40 overflow-y-auto"><div className="text-xs font-medium mb-2">内存 Top 10</div>{(d.memTop||[]).map((p: JsonData, i:number)=><div key={i} className="flex items-center gap-2 text-[11px] mb-1"><span className="w-10 text-right text-[var(--dg-text-muted)]">{i+1}</span><span className="w-32 truncate" title={p.pod}>{p.pod}</span><span className="ml-auto">{fmtBytes(Number(p.value||0))}</span></div>)}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const JvmPanel: React.FC<{ d: Record<string, any>}> = ({ d }) => {
|
||||
const JvmPanel: React.FC<{ d: JsonData}> = ({ d }) => {
|
||||
const h = Number(d.heapUsed?.[0]?.value||0);
|
||||
const uptime = Number(d.uptime?.[0]?.value||0);
|
||||
return (
|
||||
@@ -127,10 +128,10 @@ export const PrometheusDashboard: React.FC<Props> = ({ dsUrl }) => {
|
||||
const [targets, setTargets] = useState(0);
|
||||
const [error, setError] = useState('');
|
||||
const [tab, setTab] = useState('kubernetes');
|
||||
const [k8sD, setK8sD] = useState<any>({});
|
||||
const [nodeD, setNodeD] = useState<any>({});
|
||||
const [podD, setPodD] = useState<any>({});
|
||||
const [jvmD, setJvmD] = useState<any>({});
|
||||
const [k8sD, setK8sD] = useState<JsonData>({});
|
||||
const [nodeD, setNodeD] = useState<JsonData>({});
|
||||
const [podD, setPodD] = useState<JsonData>({});
|
||||
const [jvmD, setJvmD] = useState<JsonData>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (dsUrl) doConnect(dsUrl);
|
||||
@@ -139,20 +140,20 @@ export const PrometheusDashboard: React.FC<Props> = ({ dsUrl }) => {
|
||||
const doConnect = async (u: string) => {
|
||||
setLoading(true); setError('');
|
||||
try {
|
||||
const data: Record<string, any> = await prometheusProxyApi.connect(u);
|
||||
const data: JsonData = await prometheusProxyApi.connect(u);
|
||||
setConnected(true); setTargets(data.targets||0);
|
||||
loadAll(u);
|
||||
} catch (e) { setConnected(false); setError((e as Record<string, any>).message||'连接失败'); }
|
||||
} catch (e) { setConnected(false); setError((e as JsonData).message||'连接失败'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const loadAll = async (u: string) => {
|
||||
for (const t of ['kubernetes','node','pod','jvm']) {
|
||||
try {
|
||||
const data: Record<string, any> = await prometheusProxyApi.dashboard(u, t);
|
||||
const items: Record<string, any> = {};
|
||||
const data: JsonData = await prometheusProxyApi.dashboard(u, t);
|
||||
const items: JsonData = {};
|
||||
for (const [k, v] of Object.entries(data||{}))
|
||||
items[k] = Array.isArray(v) ? v : ((v as Record<string, any>)?.data?.result ? parsePrune((v as Record<string, any>).data.result) : v);
|
||||
items[k] = Array.isArray(v) ? v : ((v as JsonData)?.data?.result ? parsePrune((v as JsonData).data.result) : v);
|
||||
if (t==='kubernetes') setK8sD(items); if (t==='node') setNodeD(items);
|
||||
if (t==='pod') setPodD(items); if (t==='jvm') setJvmD(items);
|
||||
} catch {}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { zabbixProxyApi } from '@/api';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
interface ZabbixHost {
|
||||
hostid: string; hostname: string; name: string; ip: string; available: string;
|
||||
@@ -45,11 +46,11 @@ export const ZabbixDashboard: React.FC<Props> = ({ dsUrl, dsUsername, dsPassword
|
||||
const doConnect = async (u: string, un: string, pw: string) => {
|
||||
setLoading(true); setError('');
|
||||
try {
|
||||
const data: Record<string, any> = await zabbixProxyApi.connect(u, un, pw);
|
||||
const data: JsonData = await zabbixProxyApi.connect(u, un, pw);
|
||||
setHosts(data.hosts || []);
|
||||
setVersion(data.version || '');
|
||||
setConnected(true);
|
||||
} catch (e) { setConnected(false); setError((e as Record<string, any>).message || '连接失败'); }
|
||||
} catch (e) { setConnected(false); setError((e as JsonData).message || '连接失败'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
@@ -59,7 +60,7 @@ export const ZabbixDashboard: React.FC<Props> = ({ dsUrl, dsUsername, dsPassword
|
||||
else { setSortField(f); setSortDir('desc'); }
|
||||
};
|
||||
const sorted = [...hosts].sort((a, b) => {
|
||||
const va = (a as Record<string, any>)[sortField] ?? '', vb = (b as Record<string, any>)[sortField] ?? '';
|
||||
const va = (a as JsonData)[sortField] ?? '', vb = (b as JsonData)[sortField] ?? '';
|
||||
if (typeof va === 'number') return sortDir === 'asc' ? va - vb : vb - va;
|
||||
return sortDir === 'asc' ? String(va).localeCompare(String(vb)) : String(vb).localeCompare(String(va));
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user