import React, { useState, useEffect, useCallback } from 'react'; import { Activity, Cpu, MemoryStick, GitBranch, Database, RefreshCw, Download, AlertTriangle, CheckCircle, XCircle, HelpCircle, } from 'lucide-react'; import { Card, CardHeader, CardContent, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; import { Badge } from '@/components/ui/badge'; import { useToast } from '@/components/ui/use-toast'; import { Skeleton } from '@/components/ui/skeleton'; import { LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, } from 'recharts'; import type { SystemMetrics, MetricHistoryPoint, ThresholdConfig } from './types'; import { getAllMetrics } from './service'; import { ThreadDetailDialog } from './components/ThreadDetailDialog'; // 告警阈值配置 const THRESHOLDS: ThresholdConfig = { cpu: { warning: 70, danger: 90, }, memory: { warning: 75, danger: 90, }, threadBlocked: { warning: 5, danger: 10, }, dbConnections: { warning: 80, danger: 95, }, }; // 线程状态颜色 const THREAD_STATE_COLORS: Record = { RUNNABLE: '#10b981', // 绿色 WAITING: '#3b82f6', // 蓝色 TIMED_WAITING: '#8b5cf6', // 紫色 BLOCKED: '#ef4444', // 红色 NEW: '#6b7280', // 灰色 TERMINATED: '#9ca3af', // 浅灰色 }; const MetricsDashboard: React.FC = () => { const { toast } = useToast(); const [metrics, setMetrics] = useState(null); const [loading, setLoading] = useState(false); const [history, setHistory] = useState([]); const [autoRefresh, setAutoRefresh] = useState(true); const [threadDialogOpen, setThreadDialogOpen] = useState(false); // 加载指标数据 const loadMetrics = useCallback(async (silent: boolean = false) => { if (!silent) { setLoading(true); } try { const data = await getAllMetrics(); setMetrics(data); // 添加到历史记录(保留最近20个数据点) setHistory(prev => { const newPoint = { timestamp: data.timestamp, heapUsed: data.memory.heapUsed / 1024 / 1024 / 1024, // 转换为 GB heapMax: data.memory.heapMax / 1024 / 1024 / 1024, cpuUsage: data.cpu.systemCpu * 100, }; // 检查是否有旧数据(MB单位),如果有则清空重新收集 if (prev.length > 0 && prev[0].heapUsed > 100) { // 旧数据是 MB 单位(通常 > 100),清空 return [newPoint]; } const newHistory = [...prev, newPoint]; return newHistory.slice(-20); // 只保留最近20个点 }); // 检查是否有数据异常(部分接口失败) if (!silent) { const hasIssues = data.health.status === 'UNKNOWN' || data.memory.heapMax === 0 || data.cpu.systemCpu === 0; if (hasIssues) { toast({ title: '部分指标获取失败', description: '某些监控指标无法获取,显示的可能是默认值', variant: 'default', }); } } } catch (error: any) { console.error('加载指标失败:', error); toast({ variant: 'destructive', title: '加载失败', description: error.response?.data?.message || '无法获取系统指标数据,请检查后端服务和 actuator 配置', }); } finally { if (!silent) { setLoading(false); } } }, [toast]); // 初始加载 useEffect(() => { loadMetrics(); }, [loadMetrics]); // 自动刷新(30秒) useEffect(() => { if (!autoRefresh) return; const interval = setInterval(() => { loadMetrics(true); // 静默刷新 }, 30000); // 30秒 return () => clearInterval(interval); }, [autoRefresh, loadMetrics]); // 格式化字节为 GB(统一单位) // 处理 JVM 返回 -1 表示"无限制"的情况 const formatBytes = (bytes: number, showUnlimited: boolean = false): string => { if (bytes < 0) { return showUnlimited ? '无限制' : '-'; } const gb = bytes / 1024 / 1024 / 1024; return `${gb.toFixed(2)} GB`; }; // 计算内存使用百分比(处理 max 为 -1 或 0 的情况) const calcMemoryPercent = (used: number, max: number, committed?: number): number => { if (max > 0) { return (used / max) * 100; } // 如果 max 无效,使用 committed 作为参考 if (committed && committed > 0) { return (used / committed) * 100; } return 0; }; // 格式化百分比 const formatPercent = (value: number): string => { return value.toFixed(2); }; // 获取状态颜色 const getStatusColor = (value: number, threshold: { warning: number; danger: number }) => { if (value >= threshold.danger) return 'text-red-600 dark:text-red-400'; if (value >= threshold.warning) return 'text-yellow-600 dark:text-yellow-400'; return 'text-green-600 dark:text-green-400'; }; // 获取健康状态图标 const getHealthIcon = (status: string) => { switch (status) { case 'UP': return ; case 'DOWN': return ; default: return ; } }; // 导出数据 const handleExport = () => { if (!metrics) return; const data = { timestamp: new Date(metrics.timestamp).toLocaleString('zh-CN'), health: metrics.health.status, cpu: { system: formatPercent(metrics.cpu.systemCpu * 100) + '%', process: formatPercent(metrics.cpu.processCpu * 100) + '%', }, memory: { heap: { used: formatBytes(metrics.memory.heapUsed), max: formatBytes(metrics.memory.heapMax), committed: formatBytes(metrics.memory.heapCommitted), percent: formatPercent(metrics.memory.heapPercent) + '%', }, nonHeap: { used: formatBytes(metrics.memory.nonHeapUsed), max: formatBytes(metrics.memory.nonHeapMax), committed: formatBytes(metrics.memory.nonHeapCommitted), percent: formatPercent(metrics.memory.nonHeapPercent) + '%', }, regions: metrics.memory.regions ? { eden: metrics.memory.regions.eden ? { used: formatBytes(metrics.memory.regions.eden.used), max: formatBytes(metrics.memory.regions.eden.max, true), committed: formatBytes(metrics.memory.regions.eden.committed), } : null, oldGen: metrics.memory.regions.oldGen ? { used: formatBytes(metrics.memory.regions.oldGen.used), max: formatBytes(metrics.memory.regions.oldGen.max, true), committed: formatBytes(metrics.memory.regions.oldGen.committed), } : null, survivor: metrics.memory.regions.survivor ? { used: formatBytes(metrics.memory.regions.survivor.used), max: formatBytes(metrics.memory.regions.survivor.max, true), committed: formatBytes(metrics.memory.regions.survivor.committed), } : null, metaspace: metrics.memory.regions.metaspace ? { used: formatBytes(metrics.memory.regions.metaspace.used), max: formatBytes(metrics.memory.regions.metaspace.max, true), committed: formatBytes(metrics.memory.regions.metaspace.committed), } : null, } : null, }, threads: metrics.threads, hikari: metrics.hikari, gc: metrics.gc, threadStates: metrics.threadStates, }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `system-metrics-${Date.now()}.json`; a.click(); URL.revokeObjectURL(url); toast({ title: '导出成功', description: '系统指标数据已导出', }); }; // 准备线程状态饼图数据 const threadStateData = metrics ? Object.entries(metrics.threadStates) .filter(([_, value]) => value > 0) .map(([name, value]) => ({ name, value, })) : []; if (loading && !metrics) { return (

系统指标监控

实时监控系统运行状态和性能指标

{[1, 2, 3, 4].map(i => ( ))}
); } if (!metrics) { return null; } const cpuPercent = metrics.cpu.systemCpu * 100; const memoryPercent = metrics.memory.heapPercent; const blockedThreads = metrics.threadStates.BLOCKED || 0; const dbUsagePercent = metrics.hikari.usagePercent; return (
{/* 标题栏 */}

系统指标监控

实时监控系统运行状态和性能指标 · {autoRefresh && 自动刷新 (30秒)}

{/* 统计卡片 */}
{/* 健康状态 */}
{getHealthIcon(metrics.health.status)} 健康状态
{metrics.health.status === 'UP' ? '正常' : metrics.health.status === 'DOWN' ? '异常' : '未知'}
{metrics.health.status}
{/* CPU 使用率 */}
CPU 使用率
{formatPercent(cpuPercent)}%
{cpuPercent >= THRESHOLDS.cpu.warning && (
CPU 使用率偏高
)}
{/* 内存使用 */}
JVM 堆内存
{formatBytes(metrics.memory.heapUsed)}
最大: {formatBytes(metrics.memory.heapMax)}
已分配: {formatBytes(metrics.memory.heapCommitted)}
{memoryPercent >= THRESHOLDS.memory.warning && (
内存使用率偏高
)}
{/* 线程数 */} setThreadDialogOpen(true)} >
活跃线程 点击查看详情
{metrics.threads.live}
守护线程: {metrics.threads.daemon}
峰值: {metrics.threads.peak}
{blockedThreads >= THRESHOLDS.threadBlocked.warning && (
{blockedThreads} 个线程阻塞
)}
{/* JVM 内存趋势图 */} JVM 内存趋势
new Date(value).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', }) } /> `${value.toFixed(1)}G`} /> new Date(value).toLocaleTimeString('zh-CN') } formatter={(value: number) => `${value.toFixed(2)} GB`} />
{/* 数据库连接池 & 线程状态 */}
{/* 数据库连接池 */} 数据库连接池
活跃连接
{metrics.hikari.active}
空闲连接
{metrics.hikari.idle}
等待连接
{metrics.hikari.pending}
最大连接
{metrics.hikari.max}
连接池使用率 {formatPercent(dbUsagePercent)}%
总计: {metrics.hikari.total} / {metrics.hikari.max}
{dbUsagePercent >= THRESHOLDS.dbConnections.warning && (
连接池使用率偏高,建议增加连接数
)}
{/* 线程状态分布 */} 线程状态分布
`${name}: ${value}`} outerRadius={80} fill="#8884d8" dataKey="value" > {threadStateData.map((entry, index) => ( ))}
{blockedThreads >= THRESHOLDS.threadBlocked.danger && (
检测到 {blockedThreads} 个线程处于阻塞状态,可能存在死锁或性能问题
)}
{/* 内存区域详情(如果可用) */} {metrics.memory.regions && ( JVM 内存区域详情
{/* Eden 区 */} {metrics.memory.regions.eden && (
Eden 区(年轻代)
已使用: {formatBytes(metrics.memory.regions.eden.used)}
已分配: {formatBytes(metrics.memory.regions.eden.committed)}
最大: {formatBytes(metrics.memory.regions.eden.max, true)}
)} {/* Old Gen */} {metrics.memory.regions.oldGen && (
Old Gen(老年代)
已使用: {formatBytes(metrics.memory.regions.oldGen.used)}
已分配: {formatBytes(metrics.memory.regions.oldGen.committed)}
最大: {formatBytes(metrics.memory.regions.oldGen.max, true)}
)} {/* Survivor 区 */} {metrics.memory.regions.survivor && (
Survivor 区
已使用: {formatBytes(metrics.memory.regions.survivor.used)}
已分配: {formatBytes(metrics.memory.regions.survivor.committed)}
最大: {formatBytes(metrics.memory.regions.survivor.max, true)}
)} {/* Metaspace */} {metrics.memory.regions.metaspace && (
Metaspace(元空间)
已使用: {formatBytes(metrics.memory.regions.metaspace.used)}
已分配: {formatBytes(metrics.memory.regions.metaspace.committed)}
最大: {formatBytes(metrics.memory.regions.metaspace.max, true)}
)}
)} {/* GC 统计 */} 垃圾回收 (GC) 统计
GC 次数
{metrics.gc.count}
总耗时
{metrics.gc.totalTime.toFixed(3)}
平均耗时
{metrics.gc.avgTime.toFixed(2)}
毫秒
最长耗时
{(metrics.gc.maxTime * 1000).toFixed(2)}
毫秒
{/* 底部时间戳 */}
最后更新: {new Date(metrics.timestamp).toLocaleString('zh-CN')}
{/* 线程详情对话框 */}
); }; export default MetricsDashboard;