Files
NetMesh/components/monitoring/useMonitoringData.ts
zhaolei c64e00d835 feat: 新增设备巡检模块实现完整的巡检告警功能
新增设备巡检模块核心功能,包括:
- 巡检数据存储与调度系统
- 华为/H3C交换机及Linux服务器采集器
- 设备扩展配置与告警规则管理
- 主机详情页巡检配置入口
- 产品名称自定义设置功能
- 网络设备SSH exec通道兼容处理
2026-10-09 10:37:48 +08:00

570 lines
20 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* useMonitoringData - 运行监控大屏数据 hook
*
* 数据源(全部来自 NetMesh 真实 API):
* - hosts: props 传入(vault 里的设备列表)
* - inspection.getLatestAll() → 每台设备最新巡检结果(含 metrics:cpu/memory/online)
* - inspectionAlertApi.getActive() → 活动告警列表
* - inspectionAlertApi.getStats() → 告警统计
* - inspectionRuleApi.list() → 巡检规则(用于任务状态统计)
*/
import { useEffect, useState, useCallback, useRef } from 'react';
import type { Host } from '../../domain/models';
import {
inspectionResultApi,
inspectionAlertApi,
inspectionAlertRuleApi,
inspectionSchedulerApi,
inspectionDeviceApi,
type AlertRecord,
type InspectionResult,
type AlertLevel,
type AlertStats,
} from '../../infrastructure/inspection/inspectionApi';
// ===== 对外类型 =====
export interface MonitoringAlert {
id: string;
deviceId: string;
deviceName: string;
level: 'critical' | 'major' | 'warning' | 'minor' | 'info';
message: string;
time: string;
metric?: string;
metricValue?: string;
threshold?: string;
}
export interface TrendPoint {
date: string; // MM-DD
count: number;
}
export interface TaskStats {
running: number;
success: number;
failed: number;
pending: number;
}
export interface TaskDevice {
deviceId: string;
deviceName: string;
ip: string;
status: 'running' | 'success' | 'failed' | 'pending';
lastScanAt?: number;
durationMs?: number;
errorMessage?: string;
interval?: number; // 巡检间隔(秒)
}
interface UseMonitoringDataResult {
// 指标缓存
cpuMap: Record<string, number>;
memMap: Record<string, number>;
onlineMap: Record<string, boolean>;
timestampMap: Record<string, number>; // 每台设备最新巡检时间戳
// 告警
alerts: MonitoringAlert[];
alarmTrend: TrendPoint[];
alertStats: AlertStats | null;
// 巡检任务
taskStats: TaskStats;
taskDevices: TaskDevice[];
schedulerRunning: boolean;
// 操作
refresh: () => Promise<void>;
loading: boolean;
}
// ===== localStorage 持久化(降级用,API 不可用时显示缓存) =====
const LS_CPU = 'netmesh:monitoring:cpuMap';
const LS_MEM = 'netmesh:monitoring:memMap';
const LS_ALERTS = 'netmesh:monitoring:alerts';
const LS_TREND = 'netmesh:monitoring:trend';
function readLS<T>(key: string, fallback: T): T {
try {
const raw = localStorage.getItem(key);
if (!raw) return fallback;
return JSON.parse(raw) as T;
} catch {
return fallback;
}
}
function writeLS(key: string, value: unknown) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch { /* 忽略 */ }
}
// 将 NetMesh AlertLevel 映射到监控页面的 5 级
function mapLevel(l: AlertLevel): MonitoringAlert['level'] {
if (l === 'critical') return 'critical';
if (l === 'warning') return 'warning';
return 'info';
}
// 格式化告警时间:今天 → HH:mm,昨天 → 昨天 HH:mm,更早 → MM-DD HH:mm
function formatAlertTime(ts: number): string {
const d = new Date(ts);
const now = new Date();
const isToday = d.toDateString() === now.toDateString();
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
const isYesterday = d.toDateString() === yesterday.toDateString();
const timeStr = d.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' });
if (isToday) return timeStr;
if (isYesterday) return `昨天 ${timeStr}`;
const ds = `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
return `${ds} ${timeStr}`;
}
// 从巡检结果 metrics 里提取 cpu 使用率(数字百分比)
function extractCpu(metrics: Record<string, any>): number {
if (!metrics) return 0;
// NetMesh 巡检标准字段
if (typeof metrics.cpu_usage === 'number' && !isNaN(metrics.cpu_usage)) return metrics.cpu_usage;
if (typeof metrics.cpu_usage === 'string') {
const n = parseFloat(metrics.cpu_usage);
if (!isNaN(n)) return n;
}
// 网络设备
if (metrics.cpuUtilization !== undefined) {
const v = Number(metrics.cpuUtilization);
if (!isNaN(v)) return v;
}
if (metrics.displayCpu?.cpuUsage !== undefined) {
const v = Number(metrics.displayCpu.cpuUsage);
if (!isNaN(v)) return v;
}
// 遍历所有字段找数值型 CPU 字段
for (const key of Object.keys(metrics)) {
const kl = key.toLowerCase();
if (kl.includes('cpu') && (kl.includes('usage') || kl.includes('util') || kl.includes('percent'))) {
const v = metrics[key];
if (typeof v === 'number' && !isNaN(v)) return v;
if (typeof v === 'string') {
const n = parseFloat(v);
if (!isNaN(n)) return n;
}
}
}
return 0;
}
// 从巡检结果 metrics 里提取内存使用率
function extractMem(metrics: Record<string, any>): number {
if (!metrics) return 0;
// NetMesh 巡检标准字段
if (typeof metrics.memory_usage === 'number' && !isNaN(metrics.memory_usage)) return metrics.memory_usage;
if (typeof metrics.memory_usage === 'string') {
const n = parseFloat(metrics.memory_usage);
if (!isNaN(n)) return n;
}
// 网络设备
if (metrics.displayMemory?.memPercent !== undefined) {
const v = Number(metrics.displayMemory.memPercent);
if (!isNaN(v)) return v;
}
if (metrics.memoryPercent !== undefined) {
const v = Number(metrics.memoryPercent);
if (!isNaN(v)) return v;
}
if (metrics.mem_percent !== undefined) {
const v = Number(metrics.mem_percent);
if (!isNaN(v)) return v;
}
// 遍历所有字段找数值型内存字段
for (const key of Object.keys(metrics)) {
const kl = key.toLowerCase();
if ((kl.includes('mem') || kl.includes('memory')) && (kl.includes('usage') || kl.includes('util') || kl.includes('percent'))) {
const v = metrics[key];
if (typeof v === 'number' && !isNaN(v)) return v;
if (typeof v === 'string') {
const n = parseFloat(v);
if (!isNaN(n)) return n;
}
}
}
// 如果有 mem_used 和 mem_total,计算百分比
const used = metrics.mem_used ?? metrics.memory_used ?? metrics.used_memory;
const total = metrics.mem_total ?? metrics.memory_total ?? metrics.total_memory;
if (typeof used === 'number' && typeof total === 'number' && total > 0) {
return Math.min(100, Math.round((used / total) * 100));
}
return 0;
}
// 生成过去 N 天的空趋势骨架
function buildTrendSkeleton(days = 14): TrendPoint[] {
const today = new Date();
const out: TrendPoint[] = [];
for (let i = days - 1; i >= 0; i--) {
const d = new Date(today);
d.setDate(d.getDate() - i);
const ds = `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
out.push({ date: ds, count: 0 });
}
return out;
}
// 把告警数组按天统计到趋势
function buildTrendFromAlerts(allAlerts: AlertRecord[], days = 14): TrendPoint[] {
const trend = buildTrendSkeleton(days);
const map = new Map(trend.map((t) => [t.date, 0]));
const today = new Date();
const earliest = new Date(today);
earliest.setDate(earliest.getDate() - days + 1);
earliest.setHours(0, 0, 0, 0);
for (const a of allAlerts) {
const t = new Date(a.triggered_at);
if (t < earliest) continue;
const ds = `${String(t.getMonth() + 1).padStart(2, '0')}-${String(t.getDate()).padStart(2, '0')}`;
if (map.has(ds)) {
map.set(ds, (map.get(ds) || 0) + 1);
}
}
return trend.map((t) => ({ ...t, count: map.get(t.date) || 0 }));
}
// ===== Hook =====
// 工具函数:从 Host + keys 构造 SSH 连接配置(用于调度器执行巡检)
function buildSshConfig(host: Host, keys: any[]): Record<string, unknown> {
const config: Record<string, unknown> = {
host: host.hostname,
port: host.port || 22,
username: host.username,
label: host.label || host.hostname,
};
if (host.password) {
config.password = host.password;
}
// 私钥优先用 identityId 匹配 keys 数组
const keyId = host.identityId || host.identityFileId;
if (keyId) {
const key = keys.find((k) => k.id === keyId);
if (key?.privateKeyContent) {
config.privateKey = key.privateKeyContent;
if (key.passphrase) config.passphrase = key.passphrase;
}
}
return config;
}
export function useMonitoringData(hosts: Host[], keys: any[] = []): UseMonitoringDataResult {
const [cpuMap, setCpuMap] = useState<Record<string, number>>(() => readLS(LS_CPU, {}));
const [memMap, setMemMap] = useState<Record<string, number>>(() => readLS(LS_MEM, {}));
const [onlineMap, setOnlineMap] = useState<Record<string, boolean>>({});
const [timestampMap, setTimestampMap] = useState<Record<string, number>>({});
const [alerts, setAlerts] = useState<MonitoringAlert[]>(() => {
const cached = readLS<MonitoringAlert[]>(LS_ALERTS, []);
return cached;
});
// 初始 alertStats 也从缓存告警计算,保证和列表初始状态一致
const [alertStats, setAlertStats] = useState<AlertStats | null>(() => {
const cached = readLS<MonitoringAlert[]>(LS_ALERTS, []);
if (cached.length === 0) return null;
let critical = 0, warning = 0, info = 0;
for (const a of cached) {
if (a.level === 'critical') critical++;
else if (a.level === 'warning') warning++;
else info++;
}
return {
total: cached.length,
active: cached.length,
active_critical: critical,
active_warning: warning,
active_info: info,
acknowledged: 0,
resolved: 0,
closed: 0,
};
});
const [alarmTrend, setAlarmTrend] = useState<TrendPoint[]>(() => {
const cached = readLS<TrendPoint[]>(LS_TREND, []);
return cached.length > 0 ? cached : buildTrendSkeleton(14);
});
const [taskStats, setTaskStats] = useState<TaskStats>({
running: 0,
success: 0,
failed: 0,
pending: 0,
});
const [taskDevices, setTaskDevices] = useState<TaskDevice[]>([]);
const [schedulerRunning, setSchedulerRunning] = useState(false);
const [loading, setLoading] = useState(false);
const loadingRef = useRef(false);
// 用 ref 存最新的 hosts/keys,避免闭包陷阱
const hostsRef = useRef(hosts);
const keysRef = useRef(keys);
hostsRef.current = hosts;
keysRef.current = keys;
const refresh = useCallback(async () => {
console.log('[monitoring] refresh() 被调用');
if (loadingRef.current) {
console.log('[monitoring] refresh() 正在执行中,跳过');
return;
}
loadingRef.current = true;
setLoading(true);
try {
// ===== 1. 巡检最新结果 → CPU / 内存 / 在线状态 / 成功失败统计 =====
let latestResults: InspectionResult[] = [];
try {
const results: InspectionResult[] = await inspectionResultApi.getLatestAll();
latestResults = results;
const newCpu: Record<string, number> = {};
const newMem: Record<string, number> = {};
const newOnline: Record<string, boolean> = {};
const newTimestamps: Record<string, number> = {};
let successCount = 0;
let failedCount = 0;
for (const r of results) {
const cpu = extractCpu(r.metrics);
const mem = extractMem(r.metrics);
newCpu[r.device_id] = cpu;
newMem[r.device_id] = mem;
newOnline[r.device_id] = r.online !== false;
newTimestamps[r.device_id] = r.timestamp;
if (r.status === 'success') successCount++;
else failedCount++;
// 调试:打印每台设备的 metrics 结构,方便排查字段
console.log(`[monitoring] device ${r.device_id}: cpu=${cpu}, mem=${mem}, metrics keys=`, Object.keys(r.metrics || {}));
}
console.log(`[monitoring] loaded ${results.length} results: cpuMap=`, Object.keys(newCpu).length, 'memMap=', Object.keys(newMem).length);
setCpuMap((prev) => {
const merged = { ...prev, ...newCpu };
writeLS(LS_CPU, merged);
return merged;
});
setMemMap((prev) => {
const merged = { ...prev, ...newMem };
writeLS(LS_MEM, merged);
return merged;
});
setOnlineMap((prev) => ({ ...prev, ...newOnline }));
setTimestampMap((prev) => ({ ...prev, ...newTimestamps }));
setTaskStats((prev) => ({ ...prev, success: successCount, failed: failedCount }));
} catch (e) {
console.log('[monitoring] getLatestAll failed, using cached:', e);
}
// ===== 2. 活动告警(统一数据源:列表、概览、计数都从 active 列表里统计)=====
try {
const active: AlertRecord[] = await inspectionAlertApi.getActive();
console.log('[monitoring] getActive 返回:', active.length, '条告警');
const mapped: MonitoringAlert[] = active.map((a) => ({
id: a.id,
deviceId: a.device_id,
deviceName: a.device_name,
level: mapLevel(a.level),
message: a.message || a.rule_name,
time: formatAlertTime(a.triggered_at),
metric: a.metric,
metricValue: a.metric_value,
threshold: a.threshold,
}));
setAlerts(mapped);
writeLS(LS_ALERTS, mapped);
// 统一从 active 列表统计告警计数(和列表、概览保持完全一致)
let activeCritical = 0;
let activeWarning = 0;
let activeInfo = 0;
for (const a of active) {
if (a.level === 'critical') activeCritical++;
else if (a.level === 'warning') activeWarning++;
else activeInfo++;
}
setAlertStats({
total: active.length, // 用活动告警总数,不是历史总告警
active: active.length,
active_critical: activeCritical,
active_warning: activeWarning,
active_info: activeInfo,
acknowledged: 0,
resolved: 0,
closed: 0,
});
// 趋势:拉最近 200 条历史告警算 14 天趋势
try {
const all = await inspectionAlertApi.list({ limit: 200 });
const trend = buildTrendFromAlerts(all, 14);
setAlarmTrend(trend);
writeLS(LS_TREND, trend);
} catch { /* 忽略趋势拉取失败 */ }
} catch (e) {
console.log('[monitoring] alerts failed:', e);
}
// ===== 3. 调度器状态 + 设备扩展 → 任务统计 + 任务设备列表 =====
try {
const [schedulerStatus, enabledDevices, allExtensions] = await Promise.all([
inspectionSchedulerApi.status(),
inspectionDeviceApi.getEnabled(),
inspectionDeviceApi.getAll(),
]);
setSchedulerRunning(schedulerStatus.running);
const running = schedulerStatus.active_scans || 0;
// 构建结果 map
const resultMap = new Map(latestResults.map((r) => [r.device_id, r]));
// 构建所有设备的任务状态列表(过滤掉没有 hostname 的无效设备)
const allExtList = Object.values(allExtensions || {});
const taskDeviceList: TaskDevice[] = allExtList
.map((ext) => {
const host = hostsRef.current.find((h) => h.id === ext.host_id);
const result = resultMap.get(ext.host_id);
const isEnabled = ext.inspection_enabled;
// 没有 hostname 的设备(配置不完整),过滤掉
if (!host?.hostname) return null;
let status: TaskDevice['status'];
if (isEnabled) {
// 启用了定时巡检
if (result) {
if (result.status === 'success') status = 'success';
else status = 'failed';
} else {
// 启用了但还没巡检过 → 归为成功(表示已配置)
status = 'success';
}
} else {
// 未启用定时巡检 → 待执行
status = 'pending';
}
return {
deviceId: ext.host_id,
deviceName: host?.label || host?.hostname || ext.host_id,
ip: host?.hostname || '',
status,
lastScanAt: result?.timestamp,
durationMs: result?.duration_ms,
errorMessage: result?.error_message || undefined,
interval: ext.inspection_interval || undefined,
inspectionEnabled: isEnabled,
};
})
.filter((d): d is TaskDevice => d !== null);
// 统计:执行中=启用巡检的设备数,成功/失败是其中的子集,待执行=未启用的
const enabledTaskDevices = taskDeviceList.filter((d) => d.inspectionEnabled);
const runningCount = enabledTaskDevices.length;
const successCount = enabledTaskDevices.filter((d) => d.status === 'success').length;
const failedCount = enabledTaskDevices.filter((d) => d.status === 'failed').length;
const pendingCount = taskDeviceList.filter((d) => !d.inspectionEnabled).length;
console.log('[monitoring] 任务统计:', { running: runningCount, success: successCount, failed: failedCount, pending: pendingCount, total: taskDeviceList.length });
console.log('[monitoring] 启用巡检设备数:', enabledTaskDevices.length, '全部扩展设备数:', allExtList.length, '巡检结果数:', latestResults.length);
setTaskDevices(taskDeviceList);
setTaskStats({
running: runningCount,
success: successCount,
failed: failedCount,
pending: pendingCount,
});
// 自动启动调度器
if (enabledDevices.length === 0) {
console.log('[monitoring] 没有启用巡检的设备');
} else if (schedulerStatus.running) {
console.log('[monitoring] 调度器已在运行中');
} else {
console.log('[monitoring] 条件满足,准备启动调度器');
try {
const hostConfigs: Record<string, Record<string, unknown>> = {};
for (const dev of enabledDevices) {
const hostname = dev.hostname;
if (!hostname) {
console.log('[monitoring] 设备', dev.host_id, '没有 hostname,跳过');
continue;
}
const host = hostsRef.current.find((h) => h.hostname === hostname);
if (!host) {
console.log('[monitoring] vault 中未找到 hostname:', hostname, ',跳过');
continue;
}
if (!host.username) {
console.log('[monitoring] 设备', hostname, '缺少 username,跳过');
continue;
}
hostConfigs[dev.host_id] = buildSshConfig(host, keysRef.current);
}
if (Object.keys(hostConfigs).length === 0) {
console.log('[monitoring] 没有可巡检的设备(hostname 未写入或配置不完整)');
} else {
console.log('[monitoring] 启动调度器,匹配到', Object.keys(hostConfigs).length, '台设备');
await inspectionSchedulerApi.start(hostConfigs);
setSchedulerRunning(true);
}
} catch (e) {
console.log('[monitoring] 自动启动调度器失败:', e);
}
}
} catch (e) {
console.log('[monitoring] scheduler status failed:', e);
}
} finally {
loadingRef.current = false;
setLoading(false);
}
}, []);
// 初始加载 + 定期刷新(每 30 秒)
useEffect(() => {
refresh();
const id = setInterval(refresh, 30000);
return () => clearInterval(id);
}, [refresh]);
// hosts 变化时,把在线状态默认设为 true(没巡检数据前)
useEffect(() => {
setOnlineMap((prev) => {
const next: Record<string, boolean> = { ...prev };
let changed = false;
for (const h of hosts) {
if (next[h.id] === undefined) {
next[h.id] = true; // 假设在线,等巡检结果覆盖
changed = true;
}
}
return changed ? next : prev;
});
}, [hosts]);
return {
cpuMap,
memMap,
onlineMap,
timestampMap,
alerts,
alarmTrend,
alertStats,
taskStats,
taskDevices,
schedulerRunning,
refresh,
loading,
};
}