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

+92 -6
View File
@@ -1,5 +1,4 @@
import { useState } from 'react';
import { toast } from 'sonner';
import { useUserStore } from '@/store';
import {
Dialog,
@@ -10,11 +9,19 @@ import {
} from '@/components/ui/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
import { Copy, Check, Terminal, Download, Settings, Play, Cpu } from 'lucide-react';
import { Copy, Check, Terminal, Download, Settings, Play, Cpu, Zap, Loader2, Server } from 'lucide-react';
import { serverApi } from '@/services/api';
import { toast } from 'sonner';
interface AgentWizardProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** 可选:从服务器列表传入的目标服务器信息 */
server?: { id: number; hostname: string; ip: string } | null;
/** SSH一键安装成功回调 */
onInstalled?: (serverId: number) => void;
/** SSH一键安装失败回调 */
onFailed?: (serverId: number, message: string) => void;
}
/** 自动检测地址:origin(通过 Nginx 80 代理)用于安装脚本下载,backend 8080 用于 agent 直连 */
@@ -28,11 +35,13 @@ function detectServerAddress() {
return { origin: 'http://127.0.0.1', backend: 'http://127.0.0.1:8080' };
}
export function AgentWizard({ open, onOpenChange }: AgentWizardProps) {
export function AgentWizard({ open, onOpenChange, server, onInstalled, onFailed }: AgentWizardProps) {
const token = useUserStore((s) => s.token) || '<YOUR_TOKEN>';
const { origin, backend } = detectServerAddress();
const [copied, setCopied] = useState(false);
const [showManualInput, setShowManualInput] = useState(false);
const [sshInstalling, setSshInstalling] = useState(false);
const [activeTab, setActiveTab] = useState('auto');
// install.sh 通过 Nginx 代理下载(origin:80 → /agent/ → backend:8080)
const installCommand = `curl -fsSL ${origin}/agent/install.sh | bash -s -- --server ${backend} --token ${token}`;
@@ -132,22 +141,58 @@ export function AgentWizard({ open, onOpenChange }: AgentWizardProps) {
},
];
// SSH 一键安装(当从服务器列表传入 server 上下文时可用)
const handleSshInstall = async () => {
if (!server || !token) return;
setSshInstalling(true);
try {
const res: any = await serverApi.installAgent(server.id, token);
const installed = res?.installed || false;
if (installed) {
toast.success(`${server.hostname} Agent 安装成功`);
onInstalled?.(server.id);
onOpenChange(false);
} else {
toast.warning(`${server.hostname}: Agent 安装命令已执行,但服务未检测到运行`);
onInstalled?.(server.id);
onOpenChange(false);
}
} catch (e: any) {
const msg = e?.message || '安装失败';
toast.error(`${server.hostname} ${msg}`);
onFailed?.(server.id, msg);
} finally {
setSshInstalling(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl bg-slate-900 border-slate-800 text-slate-100">
<DialogHeader>
<DialogTitle className="text-slate-100">部署 Agent</DialogTitle>
<DialogTitle className="text-slate-100">
{server ? `部署 Agent → ${server.hostname}` : '部署 Agent'}
</DialogTitle>
<DialogDescription className="text-slate-400">
在目标主机上执行以下命令即可完成 Agent 安装与注册
{server
? `目标主机 ${server.hostname} (${server.ip}),选择安装方式:`
: '在目标主机上执行以下命令即可完成 Agent 安装与注册'
}
</DialogDescription>
</DialogHeader>
<Tabs defaultValue="auto" className="w-full">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="bg-slate-800 border border-slate-700">
<TabsTrigger value="auto" className="data-[state=active]:bg-slate-700">
<Terminal className="h-4 w-4 mr-1.5" />
安装Agent
</TabsTrigger>
{server && (
<TabsTrigger value="ssh" className="data-[state=active]:bg-slate-700">
<Zap className="h-4 w-4 mr-1.5" />
SSH一键安装
</TabsTrigger>
)}
<TabsTrigger value="manual" className="data-[state=active]:bg-slate-700">
手动安装
</TabsTrigger>
@@ -195,6 +240,47 @@ export function AgentWizard({ open, onOpenChange }: AgentWizardProps) {
</div>
</TabsContent>
{/* SSH 一键安装 — 仅当从服务器列表传入时显示 */}
{server && (
<TabsContent value="ssh" className="mt-4 space-y-4">
<div className="rounded-lg bg-slate-950 border border-slate-800 p-4">
<div className="flex items-center gap-3 mb-3">
<Server className="h-5 w-5 text-blue-400" />
<div>
<p className="text-sm font-medium text-slate-200">{server.hostname}</p>
<code className="text-xs text-slate-400">{server.ip}</code>
</div>
</div>
<p className="text-sm text-slate-400 mb-4">
通过 SSH 远程连接到目标主机,自动下载并安装 Agent,完成后立即启动运行。
</p>
<Button
onClick={handleSshInstall}
disabled={sshInstalling}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
{sshInstalling ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
SSH 连接中,正在安装...
</>
) : (
<>
<Zap className="h-4 w-4 mr-2" />
一键安装 Agent
</>
)}
</Button>
</div>
<div className="flex items-start gap-2 text-xs text-slate-500">
<Terminal className="h-3.5 w-3.5 mt-0.5 shrink-0" />
<span>
系统将通过 SSH 连接 {server.ip},执行安装脚本。请确保服务器 SSH 凭据已正确配置(编辑服务器可修改密码/密钥)。
</span>
</div>
</TabsContent>
)}
{/* 手动安装 */}
<TabsContent value="manual" className="mt-4 space-y-4">
<div className="max-h-[420px] overflow-y-auto space-y-3 pr-1">
+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
@@ -12,7 +12,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { TableCell, TableRow } from '@/components/ui/table';
import { Cpu, Edit, Loader2, MoreHorizontal, Server as ServerIcon, Terminal, Trash2, CheckCircle, XCircle, Network, HardDrive } from 'lucide-react';
import { Cpu, Edit, Loader2, MoreHorizontal, Server as ServerIcon, Terminal, Trash2, CheckCircle, XCircle, Network, HardDrive, Rocket } from 'lucide-react';
import { getStatusBadge, getTags, getUsageColor } from './serverHelpers';
interface ServerTableRowProps {
@@ -151,6 +151,29 @@ export function ServerTableRow({
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-1">
{/* Agent 安装按钮 — agent_status=online 时隐藏 */}
{server.agent_status !== 'online' && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]"
onClick={() => onInstallAgent(server)}
disabled={installing}
title={installResult?.success ? 'Agent 已安装' : installResult ? '安装失败,点击重试' : '安装 Agent'}
>
{installing ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : installResult ? (
installResult.success ? (
<CheckCircle className="h-4 w-4 text-emerald-400" />
) : (
<XCircle className="h-4 w-4 text-red-400" />
)
) : (
<Rocket className="h-4 w-4" />
)}
</Button>
)}
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]" onClick={() => onOpenTerminal(server)}>
<Terminal className="h-4 w-4" />
</Button>
@@ -177,24 +200,6 @@ export function ServerTableRow({
<Terminal className="h-4 w-4 mr-2" />
终端
</DropdownMenuItem>
<DropdownMenuItem
className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
onClick={() => onInstallAgent(server)}
disabled={installing}
>
{installing ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : installResult ? (
installResult.success ? (
<CheckCircle className="h-4 w-4 mr-2 text-emerald-400" />
) : (
<XCircle className="h-4 w-4 mr-2 text-red-400" />
)
) : (
<Cpu className="h-4 w-4 mr-2" />
)}
{installing ? '安装中...' : installResult ? installResult.message : '安装Agent'}
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
<DropdownMenuItem
className="text-red-400 focus:bg-[var(--dg-bg-hover)] focus:text-red-400 cursor-pointer"