feat: 监控架构优化 — 模型层 + Prometheus/Zabbix/SNMP pkg + Dockerfile升级Go1.24

整合 /srv/监控架构优化方案 核心代码:

后端模型(4个新文件):
- monitor.go: MonitorTarget(统一监控目标) + MonitorMetric(指标定义) + MetricValue(时序数据) + MetricHistory
- snmp.go: SNMPDevice + SNMPTemplate + OID模板管理
- site.go: SiteTask(拨测任务) + SiteResult(拨测结果)
- alert_v2.go: 增强告警(多条件规则/模板/升级策略/静默)

后端 pkg(3个包):
- pkg/prometheus/client.go (531行): PromQL查询/Range/Labels/Rules
- pkg/zabbix/client.go: Zabbix API (Host/Item/Trigger/History)
- pkg/snmp/collector.go (621行): gosnmp Walk/BulkWalk/自动发现

基础设施:
- Dockerfile: Go 1.21→1.24 (gosnmp依赖)
- Agent安装弹框: SSH一键安装Tab + server上下文
- 服务器列表操作栏: 安装按钮条件显示/隐藏

Handler层因类型不兼容暂时跳过,模型和客户端包已编译通过。
This commit is contained in:
attacker committed 2026-06-18 11:52:10 +00:00
1 parent f3a9e5e8d0
commit 24fa17c2a8
13 files changed
+3427 -98

No files matched your search

+109 -56
View File
@@ -37,6 +37,9 @@ import {
Cloud,
FileSpreadsheet,
RefreshCw,
Rocket,
ChevronLeft,
ChevronRight,
Cpu,
} from 'lucide-react';
import { serverApi } from '@/services/api';
@@ -73,44 +76,21 @@ export function ServerManager() {
const [terminalOpen, setTerminalOpen] = useState(false);
const [terminalServer, setTerminalServer] = useState<{ id: number; name: string; ip: string } | null>(null);
// 分页状态
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
// Agent 安装向导状态
const [agentWizardOpen, setAgentWizardOpen] = useState(false);
const [wizardServer, setWizardServer] = useState<any>(null);
// Agent 安装状态: { [serverId]: 'installing' | 'success' | 'failed' }
const [installingIds, setInstallingIds] = useState<Set<number>>(new Set());
const [installResults, setInstallResults] = useState<Map<number, { success: boolean; message: string }>>(new Map());
const openInstallAgent = async (server: any) => {
const token = useUserStore.getState().token;
if (!token) { toast.error('请先登录'); return; }
setInstallingIds(prev => new Set(prev).add(server.id));
try {
const res: any = await serverApi.installAgent(server.id, token);
const installed = res?.installed || false;
setInstallResults(prev => {
const next = new Map(prev);
next.set(server.id, {
success: installed,
message: installed ? '安装成功 ✓' : '命令已执行,Agent 未检测到运行'
});
return next;
});
if (installed) toast.success(`${server.hostname} Agent 安装成功`);
else toast.warning(`${server.hostname}: Agent 安装命令已执行`);
} catch (e: any) {
setInstallResults(prev => {
const next = new Map(prev);
next.set(server.id, { success: false, message: e?.message || '安装失败' });
return next;
});
toast.error(`${server.hostname} 安装失败: ${e?.message || '未知错误'}`);
} finally {
setInstallingIds(prev => {
const next = new Set(prev);
next.delete(server.id);
return next;
});
}
const openInstallAgent = (server: any) => {
setWizardServer(server);
setAgentWizardOpen(true);
};
const handleBatchInstallAgents = async () => {
@@ -151,11 +131,17 @@ export function ServerManager() {
setTerminalOpen(true);
};
const loadData = async () => {
const loadData = async (kw?: string, st?: string) => {
try {
setLoading(true);
const res: any = await serverApi.getServers();
const q = kw ?? searchQuery;
const s = st ?? statusFilter;
const params: any = { page, page_size: pageSize };
if (q) params.keyword = q;
if (s && s !== 'all') params.status = s;
const res: any = await serverApi.getServers(params);
setServers(res?.list || []);
setTotal(res?.total || 0);
} catch (e) {
console.error('加载服务器列表失败:', e);
setServers([]);
@@ -164,9 +150,19 @@ export function ServerManager() {
}
};
// 搜索/筛选变化时重置到第1页
const handleSearch = (val: string) => {
setSearchQuery(val);
setPage(1);
};
const handleStatusFilter = (val: string) => {
setStatusFilter(val);
setPage(1);
};
useEffect(() => {
loadData();
}, []);
}, [page, pageSize]);
// WebSocket 实时接收 Agent 指标推送 // Logan.Li
const wsRef = useRef<WebSocket | null>(null);
@@ -244,20 +240,12 @@ export function ServerManager() {
};
}, []);
const filteredServers = servers.filter((server) => {
const matchesSearch =
(server.hostname || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
(server.ip || '').includes(searchQuery) ||
(server.public_ip || '').includes(searchQuery);
const matchesStatus = statusFilter === 'all' || server.status === statusFilter;
return matchesSearch && matchesStatus;
});
// 导出 CSV
const toggleSelectAll = () => {
if (selectedServers.length === filteredServers.length) {
if (selectedServers.length === servers.length) {
setSelectedServers([]);
} else {
setSelectedServers(filteredServers.map((s) => s.id));
setSelectedServers(servers.map((s) => s.id));
}
};
@@ -382,12 +370,12 @@ export function ServerManager() {
// 导出 CSV — 将当前服务器列表导出为 CSV 文件 // Logan.Li
const handleExport = () => {
if (filteredServers.length === 0) {
if (servers.length === 0) {
toast.error('没有可导出的数据');
return;
}
const headers = ['主机名', 'IP地址', '公网IP', '端口', '操作系统', 'CPU(核)', '内存(GB)', '磁盘(GB)', '状态', '标签'];
const rows = filteredServers.map((s) => {
const rows = servers.map((s) => {
const memGB = s.memory ? Math.round(s.memory / 1024) : 0;
const tags = getTags(s).join(';');
return [s.hostname || '', s.ip || '', s.public_ip || '', s.port || 22, s.os || '',
@@ -403,7 +391,7 @@ export function ServerManager() {
a.download = `servers_${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
toast.success(`已导出 ${filteredServers.length} 台服务器`);
toast.success(`已导出 ${servers.length} 台服务器`);
};
const colSpanCount = 8;
@@ -420,7 +408,7 @@ export function ServerManager() {
<Button
variant="outline"
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
onClick={loadData}
onClick={() => loadData()}
>
<RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />
刷新
@@ -485,11 +473,11 @@ export function ServerManager() {
<Input
placeholder="搜索主机名或IP..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onChange={(e) => handleSearch(e.target.value)}
className="pl-9 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<Select value={statusFilter} onValueChange={handleStatusFilter}>
<SelectTrigger className="w-32 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<SelectValue placeholder="状态" />
</SelectTrigger>
@@ -534,7 +522,7 @@ export function ServerManager() {
<TableHead className="w-12">
<Checkbox
checked={
selectedServers.length === filteredServers.length && filteredServers.length > 0
selectedServers.length === servers.length && servers.length > 0
}
onCheckedChange={toggleSelectAll}
className="border-[var(--dg-border)]"
@@ -558,14 +546,14 @@ export function ServerManager() {
加载中...
</TableCell>
</TableRow>
) : filteredServers.length === 0 ? (
) : servers.length === 0 ? (
<TableRow className="border-[var(--dg-border)]">
<TableCell colSpan={colSpanCount} className="text-center text-[var(--dg-text-muted)] py-12">
暂无服务器数据
</TableCell>
</TableRow>
) : (
filteredServers.map((server) => (
servers.map((server) => (
<ServerTableRow
key={server.id}
server={server}
@@ -583,6 +571,48 @@ export function ServerManager() {
</TableBody>
</Table>
</div>
{/* 分页 */}
<div className="flex items-center justify-between px-4 py-3 border-t border-[var(--dg-border)]">
<div className="flex items-center gap-2 text-sm text-[var(--dg-text-muted)]">
<span>每页</span>
<Select value={String(pageSize)} onValueChange={(v) => { setPageSize(Number(v)); setPage(1); }}>
<SelectTrigger className="w-16 h-8 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="30">30</SelectItem>
</SelectContent>
</Select>
<span>条</span>
<span className="ml-4">共 {total} 条</span>
</div>
<div className="flex items-center gap-1">
<Button
variant="outline"
size="sm"
className="h-8 border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
disabled={page <= 1}
onClick={() => setPage(p => Math.max(1, p - 1))}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="text-sm text-[var(--dg-text-secondary)] px-2">
{page} / {Math.max(1, Math.ceil(total / pageSize))}
</span>
<Button
variant="outline"
size="sm"
className="h-8 border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
disabled={page >= Math.ceil(total / pageSize)}
onClick={() => setPage(p => p + 1)}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
@@ -616,7 +646,30 @@ export function ServerManager() {
/>
{/* Agent 安装向导 */}
<AgentWizard open={agentWizardOpen} onOpenChange={setAgentWizardOpen} />
<AgentWizard
open={agentWizardOpen}
onOpenChange={(open) => {
setAgentWizardOpen(open);
if (!open) setWizardServer(null);
}}
server={wizardServer}
onInstalled={(serverId) => {
setInstallResults(prev => {
const next = new Map(prev);
next.set(serverId, { success: true, message: 'Agent 已安装' });
return next;
});
// 刷新列表以更新 agent_status
loadData();
}}
onFailed={(serverId, msg) => {
setInstallResults(prev => {
const next = new Map(prev);
next.set(serverId, { success: false, message: msg || '安装失败' });
return next;
});
}}
/>
{/* 终端 Dialog */}
<TerminalDialog