增加系统版本维护页面
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);
|
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: {
|
fs: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user