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:
1 parent
f3a9e5e8d0
commit
24fa17c2a8
13 files changed
+3427
-98
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user