From f77f01689f93c2b27a8442d771f5a500f9c98b2d Mon Sep 17 00:00:00 2001 From: dengqichen Date: Tue, 9 Dec 2025 18:06:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=B3=BB=E7=BB=9F=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E7=BB=B4=E6=8A=A4=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ThreadDetailDialog.tsx | 505 +++++++++++++++ .../pages/System/Metrics/Dashboard/index.tsx | 597 ++++++++++++++++++ .../pages/System/Metrics/Dashboard/service.ts | 297 +++++++++ .../pages/System/Metrics/Dashboard/types.ts | 184 ++++++ frontend/vite.config.ts | 13 + 5 files changed, 1596 insertions(+) create mode 100644 frontend/src/pages/System/Metrics/Dashboard/components/ThreadDetailDialog.tsx create mode 100644 frontend/src/pages/System/Metrics/Dashboard/index.tsx create mode 100644 frontend/src/pages/System/Metrics/Dashboard/service.ts create mode 100644 frontend/src/pages/System/Metrics/Dashboard/types.ts diff --git a/frontend/src/pages/System/Metrics/Dashboard/components/ThreadDetailDialog.tsx b/frontend/src/pages/System/Metrics/Dashboard/components/ThreadDetailDialog.tsx new file mode 100644 index 00000000..78c95ceb --- /dev/null +++ b/frontend/src/pages/System/Metrics/Dashboard/components/ThreadDetailDialog.tsx @@ -0,0 +1,505 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogBody, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useToast } from '@/components/ui/use-toast'; +import { Search, Copy, ArrowUpDown, AlertTriangle, ChevronDown, ChevronRight } from 'lucide-react'; +import type { ThreadDetail } from '../types'; +import { getThreadDump } from '../service'; + +interface ThreadDetailDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +type SortField = 'id' | 'blocked' | 'waited'; +type SortOrder = 'asc' | 'desc'; + +// 线程状态颜色映射 +const getStateColor = (state: string) => { + switch (state) { + case 'RUNNABLE': + return 'default'; + case 'WAITING': + return 'secondary'; + case 'TIMED_WAITING': + return 'outline'; + case 'BLOCKED': + return 'destructive'; + case 'NEW': + return 'secondary'; + case 'TERMINATED': + return 'outline'; + default: + return 'secondary'; + } +}; + +export const ThreadDetailDialog: React.FC = ({ + open, + onOpenChange, +}) => { + const { toast } = useToast(); + const [threads, setThreads] = useState([]); + const [loading, setLoading] = useState(false); + const [searchKeyword, setSearchKeyword] = useState(''); + const [selectedState, setSelectedState] = useState(null); + const [sortField, setSortField] = useState('id'); + const [sortOrder, setSortOrder] = useState('asc'); + const [expandedThreads, setExpandedThreads] = useState>(new Set()); + const [showProblemsOnly, setShowProblemsOnly] = useState(false); + + // 加载线程详情 + useEffect(() => { + if (open) { + loadThreads(); + } + }, [open]); + + const loadThreads = async () => { + setLoading(true); + try { + const data = await getThreadDump(); + setThreads(data.threads || []); + } catch (error) { + console.error('加载线程详情失败:', error); + } finally { + setLoading(false); + } + }; + + // 过滤和排序线程 + const filteredThreads = threads + .filter(thread => { + // 仅显示问题线程 + if (showProblemsOnly) { + const hasProblems = + thread.threadState === 'BLOCKED' || + thread.blockedCount > 5 || + thread.waitedCount > 100; + if (!hasProblems) return false; + } + + // 状态筛选 + if (selectedState && thread.threadState !== selectedState) { + return false; + } + + // 关键词搜索 + if (!searchKeyword.trim()) return true; + const keyword = searchKeyword.toLowerCase(); + return ( + thread.threadName.toLowerCase().includes(keyword) || + thread.threadState.toLowerCase().includes(keyword) + ); + }) + .sort((a, b) => { + let comparison = 0; + + switch (sortField) { + case 'blocked': + comparison = a.blockedCount - b.blockedCount; + break; + case 'waited': + comparison = a.waitedCount - b.waitedCount; + break; + case 'id': + default: + comparison = a.threadId - b.threadId; + break; + } + + return sortOrder === 'asc' ? comparison : -comparison; + }); + + // 按状态分组统计 + const stateStats = threads.reduce((acc, thread) => { + acc[thread.threadState] = (acc[thread.threadState] || 0) + 1; + return acc; + }, {} as Record); + + // 复制线程详情(当前筛选的线程 + 完整堆栈) + const handleCopyThreadDetails = () => { + const details = filteredThreads.map(thread => { + // 格式化堆栈信息 + const stackTrace = thread.stackTrace && thread.stackTrace.length > 0 + ? '\n堆栈信息:\n' + thread.stackTrace + .map(s => ` at ${s.className}.${s.methodName}(${s.fileName}:${s.lineNumber})`) + .join('\n') + : '\n堆栈信息: 无'; + + return ` +线程ID: ${thread.threadId} +线程名称: ${thread.threadName} +线程状态: ${thread.threadState} +阻塞次数: ${thread.blockedCount} +阻塞时间: ${thread.blockedTime}ms +等待次数: ${thread.waitedCount} +等待时间: ${thread.waitedTime}ms +锁信息: ${thread.lockInfo ? '持有锁' : '无'}${stackTrace} +----------------------------------------`; + }).join('\n'); + + // 当前筛选条件说明 + let filterInfo = ''; + if (selectedState) { + filterInfo += `\n筛选条件: 状态=${selectedState}`; + } + if (showProblemsOnly) { + filterInfo += `\n筛选条件: 仅显示问题线程(BLOCKED 或 阻塞>5 或 等待>100)`; + } + if (searchKeyword) { + filterInfo += `\n搜索关键词: ${searchKeyword}`; + } + if (sortField !== 'id') { + filterInfo += `\n排序方式: ${sortField === 'blocked' ? '阻塞次数' : '等待次数'} (${sortOrder === 'desc' ? '降序' : '升序'})`; + } + + const summary = ` +===================================== +线程详情报告 +===================================== +导出时间: ${new Date().toLocaleString('zh-CN')} +总线程数: ${threads.length} 个 +显示线程数: ${filteredThreads.length} 个${filterInfo} +===================================== +状态统计: +${Object.entries(stateStats).map(([state, count]) => ` ${state}: ${count}`).join('\n')} +===================================== + +${details} +===================================== +报告结束 +===================================== +`; + + navigator.clipboard.writeText(summary).then(() => { + toast({ + title: '✓ 复制成功', + description: `已复制 ${filteredThreads.length} 个线程的详细信息(含堆栈)`, + }); + }).catch(() => { + toast({ + variant: 'destructive', + title: '复制失败', + description: '无法复制到剪贴板', + }); + }); + }; + + // 点击状态标签进行筛选 + const handleStateClick = (state: string) => { + if (selectedState === state) { + setSelectedState(null); + } else { + setSelectedState(state); + } + }; + + // 切换排序 + const handleSort = (field: SortField) => { + if (sortField === field) { + // 切换排序方向 + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + } else { + // 新字段,默认降序(问题线程在前) + setSortField(field); + setSortOrder('desc'); + } + }; + + // 展开/折叠线程堆栈 + const toggleThreadExpand = (threadId: number) => { + const newExpanded = new Set(expandedThreads); + if (newExpanded.has(threadId)) { + newExpanded.delete(threadId); + } else { + newExpanded.add(threadId); + } + setExpandedThreads(newExpanded); + }; + + // 判断是否为问题线程 + const isProblemThread = (thread: ThreadDetail) => { + return thread.threadState === 'BLOCKED' || + thread.blockedCount > 5 || + thread.waitedCount > 100; + }; + + return ( + + + + +
+ 线程详情 ({threads.length} 个线程) +
+ {!loading && ( +
+ {Object.entries(stateStats).map(([state, count]) => ( + handleStateClick(state)} + > + {state}: {count} + + ))} +
+ )} + + +
+
+
+
+ + + {/* 搜索框 */} +
+
+ + setSearchKeyword(e.target.value)} + className="pl-10" + /> +
+ {selectedState && ( +
+ 当前筛选: + + {selectedState} + + +
+ )} +
+ + {/* 线程列表 */} + {loading ? ( +
+ {[1, 2, 3, 4, 5].map(i => ( + + ))} +
+ ) : ( +
+ + + + + handleSort('id')} + > +
+ ID + {sortField === 'id' && } +
+
+ 线程名称 + 状态 + handleSort('blocked')} + > +
+ 阻塞次数 + {sortField === 'blocked' && } +
+
+ handleSort('waited')} + > +
+ 等待次数 + {sortField === 'waited' && } +
+
+ 锁信息 +
+
+ + {filteredThreads.length === 0 ? ( + + + {searchKeyword ? '未找到匹配的线程' : '暂无线程数据'} + + + ) : ( + filteredThreads.map((thread) => { + const isExpanded = expandedThreads.has(thread.threadId); + const hasProblem = isProblemThread(thread); + + return ( + + + + + + + {thread.threadId} + + +
+ {hasProblem && ( + + )} + + {thread.threadName} + +
+
+ + + {thread.threadState} + + + + {thread.blockedCount > 0 ? ( + 5 + ? 'text-red-600' + : 'text-orange-600' + }`}> + {thread.blockedCount} + + ) : ( + 0 + )} + + + 100 ? 'text-orange-600 font-medium' : 'text-muted-foreground'}> + {thread.waitedCount} + + + + {thread.lockInfo ? ( + 持有锁 + ) : ( + - + )} + +
+ {isExpanded && ( + + +
+
+

线程堆栈信息

+ +
+
+ {thread.stackTrace && thread.stackTrace.length > 0 ? ( + thread.stackTrace.map((stack, idx) => ( +
+ at {stack.className}.{stack.methodName} + ({stack.fileName}:{stack.lineNumber}) +
+ )) + ) : ( +
无堆栈信息
+ )} +
+
+
+
+ )} +
+ ); + }) + )} +
+
+
+ )} + + {/* 底部统计 */} +
+ 显示 {filteredThreads.length} / {threads.length} 个线程 +
+
+
+
+ ); +}; diff --git a/frontend/src/pages/System/Metrics/Dashboard/index.tsx b/frontend/src/pages/System/Metrics/Dashboard/index.tsx new file mode 100644 index 00000000..7dfc96b5 --- /dev/null +++ b/frontend/src/pages/System/Metrics/Dashboard/index.tsx @@ -0,0 +1,597 @@ +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(统一单位) + const formatBytes = (bytes: number): string => { + const gb = bytes / 1024 / 1024 / 1024; + return `${gb.toFixed(2)} GB`; + }; + + // 格式化百分比 + 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: { + heapUsed: formatBytes(metrics.memory.heapUsed), + heapMax: formatBytes(metrics.memory.heapMax), + heapPercent: formatPercent(metrics.memory.heapPercent) + '%', + }, + 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)} +
+ + {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} 个线程处于阻塞状态,可能存在死锁或性能问题 +
+ )} +
+
+
+ + {/* 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; diff --git a/frontend/src/pages/System/Metrics/Dashboard/service.ts b/frontend/src/pages/System/Metrics/Dashboard/service.ts new file mode 100644 index 00000000..2a5f8548 --- /dev/null +++ b/frontend/src/pages/System/Metrics/Dashboard/service.ts @@ -0,0 +1,297 @@ +import type { + HealthResponse, + MetricResponse, + ThreadDumpResponse, + JvmMemoryInfo, + CpuUsageInfo, + ThreadInfo, + HikariInfo, + GcInfo, + ThreadStateCount, + SystemMetrics, +} from './types'; + +// API 基础路径 +const ACTUATOR_BASE_URL = '/actuator'; + +/** + * 原生 fetch 调用(actuator 接口不使用项目的 request 封装) + */ +const fetchActuator = async (url: string): Promise => { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); +}; + +/** + * 获取健康检查信息 + */ +export const getHealth = () => + fetchActuator(`${ACTUATOR_BASE_URL}/health`); + +/** + * 获取指标数据(通用) + */ +export const getMetric = (metricName: string, tag?: string) => { + const url = tag + ? `${ACTUATOR_BASE_URL}/metrics/${metricName}?tag=${encodeURIComponent(tag)}` + : `${ACTUATOR_BASE_URL}/metrics/${metricName}`; + return fetchActuator(url); +}; + +/** + * 获取 JVM 内存信息 + */ +export const getJvmMemory = async (): Promise => { + const [heapUsedRes, heapMaxRes, nonHeapUsedRes, nonHeapMaxRes] = await Promise.all([ + getMetric('jvm.memory.used', 'area:heap'), + getMetric('jvm.memory.max', 'area:heap'), + getMetric('jvm.memory.used', 'area:nonheap'), + getMetric('jvm.memory.max', 'area:nonheap'), + ]); + + const heapUsed = heapUsedRes.measurements[0].value; + const heapMax = heapMaxRes.measurements[0].value; + const nonHeapUsed = nonHeapUsedRes.measurements[0].value; + const nonHeapMax = nonHeapMaxRes.measurements[0].value; + + return { + heapUsed, + heapMax, + heapPercent: (heapUsed / heapMax) * 100, + nonHeapUsed, + nonHeapMax, + nonHeapPercent: (nonHeapUsed / nonHeapMax) * 100, + }; +}; + +/** + * 获取 CPU 使用率 + */ +export const getCpuUsage = async (): Promise => { + const [systemCpuRes, processCpuRes] = await Promise.all([ + getMetric('system.cpu.usage'), + getMetric('process.cpu.usage'), + ]); + + return { + systemCpu: systemCpuRes.measurements[0].value, + processCpu: processCpuRes.measurements[0].value, + }; +}; + +/** + * 获取线程信息 + */ +export const getThreadInfo = async (): Promise => { + const [liveRes, daemonRes, peakRes] = await Promise.all([ + getMetric('jvm.threads.live'), + getMetric('jvm.threads.daemon'), + getMetric('jvm.threads.peak'), + ]); + + return { + live: liveRes.measurements[0].value, + daemon: daemonRes.measurements[0].value, + peak: peakRes.measurements[0].value, + }; +}; + +/** + * 获取 HikariCP 连接池信息 + */ +export const getHikariInfo = async (): Promise => { + try { + // 注意:根据 Spring Boot 版本不同,metric 名称可能是 hikaricp 或 hikari + // 这里尝试 hikaricp,如果失败会降级处理 + const [activeRes, idleRes, maxRes, minRes, pendingRes] = await Promise.all([ + getMetric('hikaricp.connections.active'), + getMetric('hikaricp.connections.idle'), + getMetric('hikaricp.connections.max'), + getMetric('hikaricp.connections.min'), + getMetric('hikaricp.connections.pending'), + ]); + + const active = activeRes.measurements[0].value; + const idle = idleRes.measurements[0].value; + const max = maxRes.measurements[0].value; + const min = minRes.measurements[0].value; + const pending = pendingRes.measurements[0].value; + const total = active + idle; + + return { + active, + idle, + max, + min, + pending, + total, + usagePercent: (total / max) * 100, + }; + } catch (error) { + // 如果 HikariCP 指标不可用,返回默认值 + return { + active: 0, + idle: 0, + max: 0, + min: 0, + pending: 0, + total: 0, + usagePercent: 0, + }; + } +}; + +/** + * 获取 GC 统计信息 + */ +export const getGcInfo = async (): Promise => { + try { + // 先获取不带 tag 的 GC 指标,获取所有 GC 暂停时间的汇总数据 + const gcRes = await getMetric('jvm.gc.pause'); + + const countMeasurement = gcRes.measurements.find(m => m.statistic === 'COUNT'); + const totalTimeMeasurement = gcRes.measurements.find(m => m.statistic === 'TOTAL_TIME'); + const maxMeasurement = gcRes.measurements.find(m => m.statistic === 'MAX'); + + const count = countMeasurement?.value || 0; + const totalTime = totalTimeMeasurement?.value || 0; + const maxTime = maxMeasurement?.value || 0; + const avgTime = count > 0 ? (totalTime / count) * 1000 : 0; // 转换为毫秒 + + return { + count, + totalTime, + maxTime, + avgTime, + }; + } catch (error) { + console.warn('GC 指标获取失败,使用默认值:', error); + // 如果 GC 指标不可用,返回默认值 + return { + count: 0, + totalTime: 0, + maxTime: 0, + avgTime: 0, + }; + } +}; + +/** + * 获取线程转储 + */ +export const getThreadDump = () => + fetchActuator(`${ACTUATOR_BASE_URL}/threaddump`); + +/** + * 获取线程状态统计 + */ +export const getThreadStateCount = async (): Promise => { + const threadDump = await getThreadDump(); + + const stateCount: ThreadStateCount = { + RUNNABLE: 0, + WAITING: 0, + TIMED_WAITING: 0, + BLOCKED: 0, + NEW: 0, + TERMINATED: 0, + }; + + threadDump.threads.forEach(thread => { + if (stateCount.hasOwnProperty(thread.threadState)) { + stateCount[thread.threadState]++; + } + }); + + return stateCount; +}; + +/** + * 获取所有系统指标(一次性获取) + * 使用 Promise.allSettled 确保部分接口失败不会影响整体数据获取 + */ +export const getAllMetrics = async (): Promise => { + const results = await Promise.allSettled([ + getHealth(), + getJvmMemory(), + getCpuUsage(), + getThreadInfo(), + getHikariInfo(), + getGcInfo(), + getThreadStateCount(), + ]); + + // 提取成功的结果,失败的使用默认值 + const [healthResult, memoryResult, cpuResult, threadsResult, hikariResult, gcResult, threadStatesResult] = results; + + return { + health: healthResult.status === 'fulfilled' ? healthResult.value : { status: 'UNKNOWN' }, + memory: memoryResult.status === 'fulfilled' ? memoryResult.value : { + heapUsed: 0, + heapMax: 0, + heapPercent: 0, + nonHeapUsed: 0, + nonHeapMax: 0, + nonHeapPercent: 0, + }, + cpu: cpuResult.status === 'fulfilled' ? cpuResult.value : { systemCpu: 0, processCpu: 0 }, + threads: threadsResult.status === 'fulfilled' ? threadsResult.value : { live: 0, daemon: 0, peak: 0 }, + hikari: hikariResult.status === 'fulfilled' ? hikariResult.value : { + active: 0, + idle: 0, + max: 0, + min: 0, + pending: 0, + total: 0, + usagePercent: 0, + }, + gc: gcResult.status === 'fulfilled' ? gcResult.value : { + count: 0, + totalTime: 0, + maxTime: 0, + avgTime: 0, + }, + threadStates: threadStatesResult.status === 'fulfilled' ? threadStatesResult.value : { + RUNNABLE: 0, + WAITING: 0, + TIMED_WAITING: 0, + BLOCKED: 0, + NEW: 0, + TERMINATED: 0, + }, + timestamp: Date.now(), + }; +}; + +/** + * 动态调整日志级别 + */ +export const updateLogLevel = async (loggerName: string, level: 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR') => { + const response = await fetch(`${ACTUATOR_BASE_URL}/loggers/${loggerName}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + configuredLevel: level, + }), + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } +}; + +/** + * 获取日志配置 + */ +export const getLoggers = () => + fetchActuator(`${ACTUATOR_BASE_URL}/loggers`); + +/** + * 获取环境变量 + */ +export const getEnv = () => + fetchActuator(`${ACTUATOR_BASE_URL}/env`); diff --git a/frontend/src/pages/System/Metrics/Dashboard/types.ts b/frontend/src/pages/System/Metrics/Dashboard/types.ts new file mode 100644 index 00000000..38c5dd98 --- /dev/null +++ b/frontend/src/pages/System/Metrics/Dashboard/types.ts @@ -0,0 +1,184 @@ +/** + * 系统指标监控相关类型定义 + */ + +/** + * 健康检查响应 + */ +export interface HealthResponse { + status: 'UP' | 'DOWN' | 'UNKNOWN'; + components?: { + db?: { + status: string; + details?: Record; + }; + diskSpace?: { + status: string; + details?: { + total: number; + free: number; + threshold: number; + }; + }; + ping?: { + status: string; + }; + [key: string]: any; + }; +} + +/** + * Metrics 测量值 + */ +export interface Measurement { + statistic: 'VALUE' | 'COUNT' | 'TOTAL_TIME' | 'MAX' | 'TOTAL'; + value: number; +} + +/** + * Metrics 响应基础结构 + */ +export interface MetricResponse { + name: string; + description?: string; + baseUnit?: string; + measurements: Measurement[]; + availableTags?: Array<{ + tag: string; + values: string[]; + }>; +} + +/** + * JVM 内存信息 + */ +export interface JvmMemoryInfo { + heapUsed: number; // 堆内存使用量(字节) + heapMax: number; // 堆内存最大值(字节) + heapPercent: number; // 堆内存使用百分比 + nonHeapUsed: number; // 非堆内存使用量(字节) + nonHeapMax: number; // 非堆内存最大值(字节) + nonHeapPercent: number; // 非堆内存使用百分比 +} + +/** + * CPU 使用信息 + */ +export interface CpuUsageInfo { + systemCpu: number; // 系统CPU使用率 (0-1) + processCpu: number; // 进程CPU使用率 (0-1) +} + +/** + * 线程信息 + */ +export interface ThreadInfo { + live: number; // 活跃线程数 + daemon: number; // 守护线程数 + peak: number; // 峰值线程数 +} + +/** + * 数据库连接池信息 + */ +export interface HikariInfo { + active: number; // 活跃连接 + idle: number; // 空闲连接 + max: number; // 最大连接 + min: number; // 最小连接 + pending: number; // 等待连接 + total: number; // 总连接数 + usagePercent: number; // 使用率 +} + +/** + * GC 统计信息 + */ +export interface GcInfo { + count: number; // GC 次数 + totalTime: number; // GC 总耗时(秒) + maxTime: number; // 最长GC时间(秒) + avgTime: number; // 平均GC时间(毫秒) +} + +/** + * 线程详情 + */ +export interface ThreadDetail { + threadName: string; + threadId: number; + threadState: 'RUNNABLE' | 'WAITING' | 'TIMED_WAITING' | 'BLOCKED' | 'NEW' | 'TERMINATED'; + blockedTime: number; + blockedCount: number; + waitedTime: number; + waitedCount: number; + lockInfo: any; + lockedMonitors: any[]; + lockedSynchronizers: any[]; + stackTrace: any[]; +} + +/** + * 线程转储响应 + */ +export interface ThreadDumpResponse { + threads: ThreadDetail[]; +} + +/** + * 线程状态统计 + */ +export interface ThreadStateCount { + RUNNABLE: number; + WAITING: number; + TIMED_WAITING: number; + BLOCKED: number; + NEW: number; + TERMINATED: number; +} + +/** + * 系统指标数据(汇总) + */ +export interface SystemMetrics { + health: HealthResponse; + memory: JvmMemoryInfo; + cpu: CpuUsageInfo; + threads: ThreadInfo; + hikari: HikariInfo; + gc: GcInfo; + threadStates: ThreadStateCount; + timestamp: number; // 采集时间戳 +} + +/** + * 告警阈值配置 + */ +export interface ThresholdConfig { + cpu: { + warning: number; + danger: number; + }; + memory: { + warning: number; + danger: number; + }; + threadBlocked: { + warning: number; + danger: number; + }; + dbConnections: { + warning: number; + danger: number; + }; +} + +/** + * 历史数据点(用于趋势图) + */ +export interface MetricHistoryPoint { + timestamp: number; + heapUsed: number; + heapMax: number; + cpuUsage: number; +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 24f06d91..152bab28 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -83,6 +83,19 @@ export default defineConfig(({ mode }) => { console.log('🔌 WebSocket代理:', req.url, '→', proxyTarget); }); } + }, + '/actuator': { + target: proxyTarget, + changeOrigin: true, + // 打印代理信息 + configure: (proxy, _options) => { + proxy.on('error', (err, _req, _res) => { + console.log('Actuator 代理错误', err); + }); + proxy.on('proxyReq', (proxyReq, req, _res) => { + console.log('📊 Actuator 代理请求:', req.method, req.url, '→', proxyTarget); + }); + } } }, fs: {