增加系统版本维护页面
This commit is contained in:
parent
a0d38e1d00
commit
f77f01689f
@ -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<ThreadDetailDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const [threads, setThreads] = useState<ThreadDetail[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [selectedState, setSelectedState] = useState<string | null>(null);
|
||||
const [sortField, setSortField] = useState<SortField>('id');
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>('asc');
|
||||
const [expandedThreads, setExpandedThreads] = useState<Set<number>>(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<string, number>);
|
||||
|
||||
// 复制线程详情(当前筛选的线程 + 完整堆栈)
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-6xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>线程详情 ({threads.length} 个线程)</span>
|
||||
<div className="flex items-center gap-3">
|
||||
{!loading && (
|
||||
<div className="flex items-center gap-2 text-sm font-normal">
|
||||
{Object.entries(stateStats).map(([state, count]) => (
|
||||
<Badge
|
||||
key={state}
|
||||
variant={getStateColor(state)}
|
||||
className={`cursor-pointer transition-all ${
|
||||
selectedState === state
|
||||
? 'ring-2 ring-offset-2 ring-primary'
|
||||
: 'hover:opacity-80'
|
||||
}`}
|
||||
onClick={() => handleStateClick(state)}
|
||||
>
|
||||
{state}: {count}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant={showProblemsOnly ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setShowProblemsOnly(!showProblemsOnly)}
|
||||
disabled={loading}
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4 mr-2" />
|
||||
{showProblemsOnly ? '显示全部' : '仅问题线程'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCopyThreadDetails}
|
||||
disabled={loading || threads.length === 0}
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
复制线程详情
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogBody className="space-y-4">
|
||||
{/* 搜索框 */}
|
||||
<div className="space-y-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索线程名称或状态..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
{selectedState && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-muted-foreground">当前筛选:</span>
|
||||
<Badge variant={getStateColor(selectedState)}>
|
||||
{selectedState}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedState(null)}
|
||||
className="h-6 px-2 text-xs"
|
||||
>
|
||||
清除筛选
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 线程列表 */}
|
||||
{loading ? (
|
||||
<div className="space-y-2">
|
||||
{[1, 2, 3, 4, 5].map(i => (
|
||||
<Skeleton key={i} className="h-16 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="border rounded-lg">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[40px]"></TableHead>
|
||||
<TableHead
|
||||
className="w-[50px] cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => handleSort('id')}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
ID
|
||||
{sortField === 'id' && <ArrowUpDown className="h-3 w-3" />}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="w-[200px]">线程名称</TableHead>
|
||||
<TableHead className="w-[110px]">状态</TableHead>
|
||||
<TableHead
|
||||
className="w-[90px] cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => handleSort('blocked')}
|
||||
>
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
阻塞次数
|
||||
{sortField === 'blocked' && <ArrowUpDown className="h-3 w-3" />}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="w-[90px] cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => handleSort('waited')}
|
||||
>
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
等待次数
|
||||
{sortField === 'waited' && <ArrowUpDown className="h-3 w-3" />}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="w-[80px]">锁信息</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredThreads.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
|
||||
{searchKeyword ? '未找到匹配的线程' : '暂无线程数据'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredThreads.map((thread) => {
|
||||
const isExpanded = expandedThreads.has(thread.threadId);
|
||||
const hasProblem = isProblemThread(thread);
|
||||
|
||||
return (
|
||||
<React.Fragment key={thread.threadId}>
|
||||
<TableRow className={hasProblem ? 'bg-red-50 dark:bg-red-950/10' : ''}>
|
||||
<TableCell className="text-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={() => toggleThreadExpand(thread.threadId)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{thread.threadId}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs max-w-[200px]">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{hasProblem && (
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-red-600 flex-shrink-0" />
|
||||
)}
|
||||
<span className="truncate" title={thread.threadName}>
|
||||
{thread.threadName}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={getStateColor(thread.threadState)}>
|
||||
{thread.threadState}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{thread.blockedCount > 0 ? (
|
||||
<span className={`font-medium ${
|
||||
thread.blockedCount > 5
|
||||
? 'text-red-600'
|
||||
: 'text-orange-600'
|
||||
}`}>
|
||||
{thread.blockedCount}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">0</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span className={thread.waitedCount > 100 ? 'text-orange-600 font-medium' : 'text-muted-foreground'}>
|
||||
{thread.waitedCount}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{thread.lockInfo ? (
|
||||
<span className="text-orange-600">持有锁</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{isExpanded && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="bg-muted/30 p-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-medium text-sm">线程堆栈信息</h4>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const stack = thread.stackTrace
|
||||
.map(s => ` at ${s.className}.${s.methodName}(${s.fileName}:${s.lineNumber})`)
|
||||
.join('\n');
|
||||
navigator.clipboard.writeText(`线程 #${thread.threadId}: ${thread.threadName}\n${stack}`);
|
||||
toast({ title: '已复制堆栈信息' });
|
||||
}}
|
||||
>
|
||||
<Copy className="h-3 w-3 mr-1" />
|
||||
复制堆栈
|
||||
</Button>
|
||||
</div>
|
||||
<div className="bg-background rounded border p-3 font-mono text-xs overflow-x-auto max-h-64 overflow-y-auto">
|
||||
{thread.stackTrace && thread.stackTrace.length > 0 ? (
|
||||
thread.stackTrace.map((stack, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`py-0.5 ${
|
||||
stack.className?.includes('Lock') ||
|
||||
stack.methodName?.includes('lock') ||
|
||||
stack.methodName?.includes('wait')
|
||||
? 'text-orange-600 font-medium'
|
||||
: 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
at {stack.className}.{stack.methodName}
|
||||
({stack.fileName}:{stack.lineNumber})
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-muted-foreground">无堆栈信息</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部统计 */}
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
显示 {filteredThreads.length} / {threads.length} 个线程
|
||||
</div>
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
597
frontend/src/pages/System/Metrics/Dashboard/index.tsx
Normal file
597
frontend/src/pages/System/Metrics/Dashboard/index.tsx
Normal file
@ -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<string, string> = {
|
||||
RUNNABLE: '#10b981', // 绿色
|
||||
WAITING: '#3b82f6', // 蓝色
|
||||
TIMED_WAITING: '#8b5cf6', // 紫色
|
||||
BLOCKED: '#ef4444', // 红色
|
||||
NEW: '#6b7280', // 灰色
|
||||
TERMINATED: '#9ca3af', // 浅灰色
|
||||
};
|
||||
|
||||
const MetricsDashboard: React.FC = () => {
|
||||
const { toast } = useToast();
|
||||
const [metrics, setMetrics] = useState<SystemMetrics | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [history, setHistory] = useState<MetricHistoryPoint[]>([]);
|
||||
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 <CheckCircle className="h-5 w-5 text-green-600" />;
|
||||
case 'DOWN':
|
||||
return <XCircle className="h-5 w-5 text-red-600" />;
|
||||
default:
|
||||
return <HelpCircle className="h-5 w-5 text-gray-600" />;
|
||||
}
|
||||
};
|
||||
|
||||
// 导出数据
|
||||
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 (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">系统指标监控</h1>
|
||||
<p className="text-muted-foreground mt-1">实时监控系统运行状态和性能指标</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
{[1, 2, 3, 4].map(i => (
|
||||
<Card key={i}>
|
||||
<CardContent className="p-6">
|
||||
<Skeleton className="h-20" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 标题栏 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">系统指标监控</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
实时监控系统运行状态和性能指标 ·
|
||||
{autoRefresh && <span className="text-green-600 ml-2">自动刷新 (30秒)</span>}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setAutoRefresh(!autoRefresh)}
|
||||
>
|
||||
{autoRefresh ? '停止刷新' : '开启刷新'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => loadMetrics()}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleExport}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
导出
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
{/* 健康状态 */}
|
||||
<Card className="bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-950/20 dark:to-indigo-950/20 border-blue-200 dark:border-blue-900">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{getHealthIcon(metrics.health.status)}
|
||||
<span className="text-xs font-medium text-muted-foreground">健康状态</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">
|
||||
{metrics.health.status === 'UP' ? '正常' :
|
||||
metrics.health.status === 'DOWN' ? '异常' : '未知'}
|
||||
</div>
|
||||
<Badge
|
||||
variant={metrics.health.status === 'UP' ? 'default' : 'destructive'}
|
||||
>
|
||||
{metrics.health.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<Activity className="h-10 w-10 text-blue-600 dark:text-blue-400 opacity-50" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* CPU 使用率 */}
|
||||
<Card className="bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 border-green-200 dark:border-green-900">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Cpu className="h-4 w-4" />
|
||||
<span className="text-xs font-medium text-muted-foreground">CPU 使用率</span>
|
||||
</div>
|
||||
<div className={`text-2xl font-bold ${getStatusColor(cpuPercent, THRESHOLDS.cpu)}`}>
|
||||
{formatPercent(cpuPercent)}%
|
||||
</div>
|
||||
<Progress value={cpuPercent} className="h-2" />
|
||||
{cpuPercent >= THRESHOLDS.cpu.warning && (
|
||||
<div className="flex items-center gap-1 text-xs text-yellow-600">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
CPU 使用率偏高
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 内存使用 */}
|
||||
<Card className="bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 border-purple-200 dark:border-purple-900">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<MemoryStick className="h-4 w-4" />
|
||||
<span className="text-xs font-medium text-muted-foreground">JVM 堆内存</span>
|
||||
</div>
|
||||
<div className={`text-2xl font-bold ${getStatusColor(memoryPercent, THRESHOLDS.memory)}`}>
|
||||
{formatBytes(metrics.memory.heapUsed)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
/ {formatBytes(metrics.memory.heapMax)}
|
||||
</div>
|
||||
<Progress value={memoryPercent} className="h-2" />
|
||||
{memoryPercent >= THRESHOLDS.memory.warning && (
|
||||
<div className="flex items-center gap-1 text-xs text-yellow-600">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
内存使用率偏高
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 线程数 */}
|
||||
<Card
|
||||
className="bg-gradient-to-br from-orange-50 to-amber-50 dark:from-orange-950/20 dark:to-amber-950/20 border-orange-200 dark:border-orange-900 cursor-pointer hover:shadow-lg transition-shadow"
|
||||
onClick={() => setThreadDialogOpen(true)}
|
||||
>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="h-4 w-4" />
|
||||
<span className="text-xs font-medium text-muted-foreground">活跃线程</span>
|
||||
<span className="text-xs text-blue-600 dark:text-blue-400">点击查看详情</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-orange-600 dark:text-orange-400">
|
||||
{metrics.threads.live}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground space-y-0.5">
|
||||
<div>守护线程: {metrics.threads.daemon}</div>
|
||||
<div>峰值: {metrics.threads.peak}</div>
|
||||
</div>
|
||||
{blockedThreads >= THRESHOLDS.threadBlocked.warning && (
|
||||
<div className="flex items-center gap-1 text-xs text-red-600">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
{blockedThreads} 个线程阻塞
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<GitBranch className="h-10 w-10 text-orange-600 dark:text-orange-400 opacity-50" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* JVM 内存趋势图 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>JVM 内存趋势</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={history}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tickFormatter={(value) =>
|
||||
new Date(value).toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={(value: number) => `${value.toFixed(1)}G`}
|
||||
/>
|
||||
<Tooltip
|
||||
labelFormatter={(value) =>
|
||||
new Date(value).toLocaleTimeString('zh-CN')
|
||||
}
|
||||
formatter={(value: number) => `${value.toFixed(2)} GB`}
|
||||
/>
|
||||
<Legend />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="heapUsed"
|
||||
stroke="#8b5cf6"
|
||||
strokeWidth={2}
|
||||
name="堆内存使用"
|
||||
dot={false}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="heapMax"
|
||||
stroke="#94a3b8"
|
||||
strokeWidth={1}
|
||||
strokeDasharray="5 5"
|
||||
name="堆内存最大值"
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 数据库连接池 & 线程状态 */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{/* 数据库连接池 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5" />
|
||||
数据库连接池
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">活跃连接</div>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{metrics.hikari.active}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">空闲连接</div>
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{metrics.hikari.idle}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">等待连接</div>
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{metrics.hikari.pending}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">最大连接</div>
|
||||
<div className="text-2xl font-bold text-gray-600">
|
||||
{metrics.hikari.max}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium">连接池使用率</span>
|
||||
<span className={`text-sm font-bold ${getStatusColor(dbUsagePercent, THRESHOLDS.dbConnections)}`}>
|
||||
{formatPercent(dbUsagePercent)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={dbUsagePercent} className="h-3" />
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
总计: {metrics.hikari.total} / {metrics.hikari.max}
|
||||
</div>
|
||||
{dbUsagePercent >= THRESHOLDS.dbConnections.warning && (
|
||||
<div className="flex items-center gap-1 text-xs text-yellow-600 mt-2">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
连接池使用率偏高,建议增加连接数
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 线程状态分布 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>线程状态分布</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={threadStateData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ name, value }) => `${name}: ${value}`}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{threadStateData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={THREAD_STATE_COLORS[entry.name] || '#6b7280'}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
{blockedThreads >= THRESHOLDS.threadBlocked.danger && (
|
||||
<div className="flex items-center gap-2 text-sm text-red-600 bg-red-50 dark:bg-red-950/20 p-3 rounded-lg mt-4">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<span>检测到 {blockedThreads} 个线程处于阻塞状态,可能存在死锁或性能问题</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* GC 统计 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>垃圾回收 (GC) 统计</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="text-center p-4 bg-muted/30 rounded-lg">
|
||||
<div className="text-sm text-muted-foreground mb-1">GC 次数</div>
|
||||
<div className="text-2xl font-bold">{metrics.gc.count}</div>
|
||||
<div className="text-xs text-muted-foreground">次</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-muted/30 rounded-lg">
|
||||
<div className="text-sm text-muted-foreground mb-1">总耗时</div>
|
||||
<div className="text-2xl font-bold">{metrics.gc.totalTime.toFixed(3)}</div>
|
||||
<div className="text-xs text-muted-foreground">秒</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-muted/30 rounded-lg">
|
||||
<div className="text-sm text-muted-foreground mb-1">平均耗时</div>
|
||||
<div className="text-2xl font-bold">{metrics.gc.avgTime.toFixed(2)}</div>
|
||||
<div className="text-xs text-muted-foreground">毫秒</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-muted/30 rounded-lg">
|
||||
<div className="text-sm text-muted-foreground mb-1">最长耗时</div>
|
||||
<div className="text-2xl font-bold">{(metrics.gc.maxTime * 1000).toFixed(2)}</div>
|
||||
<div className="text-xs text-muted-foreground">毫秒</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 底部时间戳 */}
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
最后更新: {new Date(metrics.timestamp).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
|
||||
{/* 线程详情对话框 */}
|
||||
<ThreadDetailDialog
|
||||
open={threadDialogOpen}
|
||||
onOpenChange={setThreadDialogOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricsDashboard;
|
||||
297
frontend/src/pages/System/Metrics/Dashboard/service.ts
Normal file
297
frontend/src/pages/System/Metrics/Dashboard/service.ts
Normal file
@ -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 <T>(url: string): Promise<T> => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取健康检查信息
|
||||
*/
|
||||
export const getHealth = () =>
|
||||
fetchActuator<HealthResponse>(`${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<MetricResponse>(url);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取 JVM 内存信息
|
||||
*/
|
||||
export const getJvmMemory = async (): Promise<JvmMemoryInfo> => {
|
||||
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<CpuUsageInfo> => {
|
||||
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<ThreadInfo> => {
|
||||
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<HikariInfo> => {
|
||||
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<GcInfo> => {
|
||||
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<ThreadDumpResponse>(`${ACTUATOR_BASE_URL}/threaddump`);
|
||||
|
||||
/**
|
||||
* 获取线程状态统计
|
||||
*/
|
||||
export const getThreadStateCount = async (): Promise<ThreadStateCount> => {
|
||||
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<SystemMetrics> => {
|
||||
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`);
|
||||
184
frontend/src/pages/System/Metrics/Dashboard/types.ts
Normal file
184
frontend/src/pages/System/Metrics/Dashboard/types.ts
Normal file
@ -0,0 +1,184 @@
|
||||
/**
|
||||
* 系统指标监控相关类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 健康检查响应
|
||||
*/
|
||||
export interface HealthResponse {
|
||||
status: 'UP' | 'DOWN' | 'UNKNOWN';
|
||||
components?: {
|
||||
db?: {
|
||||
status: string;
|
||||
details?: Record<string, any>;
|
||||
};
|
||||
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;
|
||||
}
|
||||
@ -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: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user