Files
SenMeshNet/components/monitoring/useMonitoringData.ts
zhaolei 2f9f8da48e
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
Initial commit: NetMesh smart ops platform with 3D topology, inspection reports, RCA and AI ops assistant
2026-10-15 13:53:04 +08:00

641 lines
23 KiB
TypeScript
Raw 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, useMemo } 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;
// 告警规则 & 派生阈值
rules: any[];
thresholds: {
cpuCritical: number;
cpuWarning: number;
memCritical: number;
memWarning: number;
diskCritical: number;
diskWarning: number;
};
// 操作
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 [rules, setRules] = useState<any[]>([]);
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 (silent = false) => {
if (loadingRef.current) {
console.log('[monitoring] refresh() 正在执行中,跳过');
return;
}
loadingRef.current = true;
if (!silent) 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);
// 浅比较:数据没变化就不 setState,避免触发无意义重渲
setAlarmTrend((prev) => {
if (prev.length === trend.length && prev.every((p, i) => p.date === trend[i].date && p.count === trend[i].count)) return prev;
return trend;
});
writeLS(LS_TREND, trend);
} catch { /* 忽略趋势拉取失败 */ }
} catch (e) {
console.log('[monitoring] alerts failed:', e);
}
// ===== 3. 调度器状态 + 设备扩展 + 告警规则 → 任务统计 + 任务设备列表 + thresholds =====
try {
const [schedulerStatus, enabledDevices, allExtensions, rulesResult] = await Promise.all([
inspectionSchedulerApi.status(),
inspectionDeviceApi.getEnabled(),
inspectionDeviceApi.getAll(),
inspectionAlertRuleApi.list().catch(() => []),
]);
setRules(rulesResult || []);
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;
if (!silent) setLoading(false);
}
}, []);
// 初始加载 + 定期刷新(每 30 秒)
useEffect(() => {
refresh(false);
const id = setInterval(() => refresh(true), 30000);
return () => clearInterval(id);
}, [refresh]);
// 事件驱动:告警变化立即刷新(静默刷新,不显示 loading 避免闪动)
useEffect(() => {
const api = (window as any).netcatty?.inspection;
if (!api?.onAlertChanged) return;
// 前端侧 500ms debounce
let timer: ReturnType<typeof setTimeout> | null = null;
const handler = () => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => refresh(true), 500);
};
const unsub = api.onAlertChanged(handler);
return () => {
if (timer) clearTimeout(timer);
try { unsub?.(); } catch (_) {}
};
}, [refresh]);
// 规则变化 → 立刻 refresh(让 thresholds 同步)
useEffect(() => {
const api = (window as any).netcatty?.inspection;
if (!api?.onRuleChanged) return;
const handler = () => refresh(true);
const unsub = api.onRuleChanged(handler);
return () => { try { unsub?.(); } catch (_) {} };
}, [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]);
// 从告警规则派生阈值(和 alertRuleStore 默认规则对齐)
const thresholds = useMemo(() => {
let cpuCritical = 90, cpuWarning = 80;
let memCritical = 85, memWarning = 75;
let diskCritical = 90, diskWarning = 80;
for (const r of rules) {
if (!r.enabled) continue;
// 排除 scope_type='catalog' 的设备专属规则(会覆盖全局通用规则)
if (r.scope_type && r.scope_type !== 'all') continue;
const t = Number(r.threshold);
if (Number.isNaN(t)) continue;
if (r.metric === 'cpu_usage') {
if (r.level === 'critical') cpuCritical = t;
else if (r.level === 'warning') cpuWarning = t;
} else if (r.metric === 'memory_usage') {
if (r.level === 'critical') memCritical = t;
else if (r.level === 'warning') memWarning = t;
} else if (r.metric === 'disk_usage') {
if (r.level === 'critical') diskCritical = t;
else if (r.level === 'warning') diskWarning = t;
}
}
return { cpuCritical, cpuWarning, memCritical, memWarning, diskCritical, diskWarning };
}, [rules]);
return {
cpuMap,
memMap,
onlineMap,
timestampMap,
alerts,
alarmTrend,
alertStats,
taskStats,
taskDevices,
schedulerRunning,
rules,
thresholds,
refresh,
loading,
};
}