mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-19 00:41:55 +08:00
- Integrate Recharts library for interactive data visualization - Add detailed latency distribution charts and error type analysis - Implement bandwidth and performance metrics graphs - Update metrics collection to support more granular statistical tracking - Modify frontend API routes to remove /admin prefix - Improve metrics display with responsive and informative charts
484 lines
16 KiB
TypeScript
484 lines
16 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState } from "react"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { useToast } from "@/components/ui/use-toast"
|
|
import { useRouter } from "next/navigation"
|
|
import {
|
|
BarChart,
|
|
Bar,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
Legend,
|
|
ResponsiveContainer,
|
|
PieChart,
|
|
Pie,
|
|
Cell,
|
|
LineChart,
|
|
Line,
|
|
} from "recharts"
|
|
|
|
interface Metrics {
|
|
uptime: string
|
|
active_requests: number
|
|
total_requests: number
|
|
total_errors: number
|
|
num_goroutine: number
|
|
memory_usage: string
|
|
avg_response_time: string
|
|
requests_per_second: number
|
|
status_code_stats: Record<string, number>
|
|
top_paths: Array<{
|
|
path: string
|
|
request_count: number
|
|
error_count: number
|
|
avg_latency: string
|
|
bytes_transferred: number
|
|
}>
|
|
recent_requests: Array<{
|
|
Time: string
|
|
Path: string
|
|
Status: number
|
|
Latency: number
|
|
BytesSent: number
|
|
ClientIP: string
|
|
}>
|
|
latency_stats: {
|
|
min: string
|
|
max: string
|
|
distribution: Record<string, number>
|
|
}
|
|
error_stats: {
|
|
client_errors: number
|
|
server_errors: number
|
|
types: Record<string, number>
|
|
}
|
|
bandwidth_history: Record<string, string>
|
|
current_bandwidth: string
|
|
total_bytes: number
|
|
}
|
|
|
|
// 颜色常量
|
|
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']
|
|
|
|
export default function DashboardPage() {
|
|
const [metrics, setMetrics] = useState<Metrics | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const { toast } = useToast()
|
|
const router = useRouter()
|
|
|
|
const fetchMetrics = async () => {
|
|
try {
|
|
const token = localStorage.getItem("token")
|
|
if (!token) {
|
|
router.push("/login")
|
|
return
|
|
}
|
|
|
|
const response = await fetch("/admin/api/metrics", {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
})
|
|
|
|
if (response.status === 401) {
|
|
localStorage.removeItem("token")
|
|
router.push("/login")
|
|
return
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error("加载监控数据失败")
|
|
}
|
|
|
|
const data = await response.json()
|
|
setMetrics(data)
|
|
setError(null)
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "加载监控数据失败"
|
|
setError(message)
|
|
toast({
|
|
title: "错误",
|
|
description: message,
|
|
variant: "destructive",
|
|
})
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
// 立即获取一次数据
|
|
fetchMetrics()
|
|
|
|
// 设置定时刷新
|
|
const interval = setInterval(fetchMetrics, 5000)
|
|
return () => clearInterval(interval)
|
|
}, [])
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex h-[calc(100vh-4rem)] items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="text-lg font-medium">加载中...</div>
|
|
<div className="text-sm text-gray-500 mt-1">正在获取监控数据</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (error || !metrics) {
|
|
return (
|
|
<div className="flex h-[calc(100vh-4rem)] items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="text-lg font-medium text-red-600">
|
|
{error || "暂无数据"}
|
|
</div>
|
|
<div className="text-sm text-gray-500 mt-1">
|
|
请检查后端服务是否正常运行
|
|
</div>
|
|
<button
|
|
onClick={fetchMetrics}
|
|
className="mt-4 px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
|
|
>
|
|
重试
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>基础指标</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<div className="text-sm font-medium text-gray-500">运行时间</div>
|
|
<div className="text-lg font-semibold">{metrics.uptime}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-medium text-gray-500">当前活跃请求</div>
|
|
<div className="text-lg font-semibold">{metrics.active_requests}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-medium text-gray-500">总请求数</div>
|
|
<div className="text-lg font-semibold">{metrics.total_requests}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-medium text-gray-500">错误数</div>
|
|
<div className="text-lg font-semibold">{metrics.total_errors}</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>系统指标</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<div className="text-sm font-medium text-gray-500">Goroutine数量</div>
|
|
<div className="text-lg font-semibold">{metrics.num_goroutine}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-medium text-gray-500">内存使用</div>
|
|
<div className="text-lg font-semibold">{metrics.memory_usage}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-medium text-gray-500">平均响应时间</div>
|
|
<div className="text-lg font-semibold">{metrics.avg_response_time}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-medium text-gray-500">每秒请求数</div>
|
|
<div className="text-lg font-semibold">
|
|
{metrics.requests_per_second.toFixed(2)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>状态码统计</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
|
{Object.entries(metrics.status_code_stats || {})
|
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
.map(([status, count]) => (
|
|
<div
|
|
key={status}
|
|
className="p-4 rounded-lg border bg-card text-card-foreground shadow-sm"
|
|
>
|
|
<div className="text-sm font-medium text-gray-500">
|
|
状态码 {status}
|
|
</div>
|
|
<div className="text-lg font-semibold">{count}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>热门路径 (Top 10)</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="border-b">
|
|
<th className="text-left p-2">路径</th>
|
|
<th className="text-left p-2">请求数</th>
|
|
<th className="text-left p-2">错误数</th>
|
|
<th className="text-left p-2">平均延迟</th>
|
|
<th className="text-left p-2">传输大小</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{(metrics.top_paths || []).map((path, index) => (
|
|
<tr key={index} className="border-b">
|
|
<td className="p-2">{path.path}</td>
|
|
<td className="p-2">{path.request_count}</td>
|
|
<td className="p-2">{path.error_count}</td>
|
|
<td className="p-2">{path.avg_latency}</td>
|
|
<td className="p-2">{formatBytes(path.bytes_transferred)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>最近请求</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="border-b">
|
|
<th className="text-left p-2">时间</th>
|
|
<th className="text-left p-2">路径</th>
|
|
<th className="text-left p-2">状态</th>
|
|
<th className="text-left p-2">延迟</th>
|
|
<th className="text-left p-2">大小</th>
|
|
<th className="text-left p-2">客户端IP</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{(metrics.recent_requests || [])
|
|
.slice(0, 20) // 只显示最近20条记录
|
|
.map((req, index) => (
|
|
<tr key={index} className="border-b hover:bg-gray-50">
|
|
<td className="p-2">{formatDate(req.Time)}</td>
|
|
<td className="p-2 max-w-xs truncate">{req.Path}</td>
|
|
<td className="p-2">
|
|
<span
|
|
className={`px-2 py-1 rounded-full text-xs ${getStatusColor(
|
|
req.Status
|
|
)}`}
|
|
>
|
|
{req.Status}
|
|
</span>
|
|
</td>
|
|
<td className="p-2">{formatLatency(req.Latency)}</td>
|
|
<td className="p-2">{formatBytes(req.BytesSent)}</td>
|
|
<td className="p-2">{req.ClientIP}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>延迟统计</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className="space-y-2">
|
|
<div className="text-sm font-medium text-gray-500">最小响应时间</div>
|
|
<div className="text-lg font-semibold">{metrics?.latency_stats?.min}</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="text-sm font-medium text-gray-500">最大响应时间</div>
|
|
<div className="text-lg font-semibold">{metrics?.latency_stats?.max}</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="text-sm font-medium text-gray-500">平均响应时间</div>
|
|
<div className="text-lg font-semibold">{metrics?.avg_response_time}</div>
|
|
</div>
|
|
</div>
|
|
<div className="h-80 mt-6">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart
|
|
data={Object.entries(metrics?.latency_stats?.distribution || {}).map(([name, value]) => ({
|
|
name,
|
|
value,
|
|
}))}
|
|
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
|
|
>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="name" />
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Legend />
|
|
<Bar dataKey="value" name="请求数" fill="#8884d8" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>错误分布</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-80">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<PieChart>
|
|
<Pie
|
|
data={[
|
|
{ name: '客户端错误', value: metrics?.error_stats?.client_errors || 0 },
|
|
{ name: '服务器错误', value: metrics?.error_stats?.server_errors || 0 },
|
|
]}
|
|
cx="50%"
|
|
cy="50%"
|
|
labelLine={false}
|
|
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
|
|
outerRadius={80}
|
|
fill="#8884d8"
|
|
dataKey="value"
|
|
>
|
|
{[0, 1].map((entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip />
|
|
<Legend />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>错误类型统计</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-80">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart
|
|
data={Object.entries(metrics?.error_stats?.types || {}).map(([name, value]) => ({
|
|
name,
|
|
value,
|
|
}))}
|
|
layout="vertical"
|
|
margin={{ top: 5, right: 30, left: 100, bottom: 5 }}
|
|
>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis type="number" />
|
|
<YAxis type="category" dataKey="name" />
|
|
<Tooltip />
|
|
<Bar dataKey="value" fill="#FF8042" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>带宽统计</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
<div className="space-y-2">
|
|
<div className="text-sm font-medium text-gray-500">当前带宽</div>
|
|
<div className="text-lg font-semibold">{metrics?.current_bandwidth}</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="text-sm font-medium text-gray-500">总传输数据</div>
|
|
<div className="text-lg font-semibold">{formatBytes(metrics?.total_bytes || 0)}</div>
|
|
</div>
|
|
</div>
|
|
<div className="h-80">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<LineChart
|
|
data={Object.entries(metrics?.bandwidth_history || {}).map(([time, value]) => ({
|
|
time,
|
|
value: parseFloat(value.split(' ')[0]),
|
|
unit: value.split(' ')[1],
|
|
}))}
|
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
|
>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="time" />
|
|
<YAxis />
|
|
<Tooltip formatter={(value, name, props) => [`${value} ${props.payload.unit}`, '带宽']} />
|
|
<Legend />
|
|
<Line type="monotone" dataKey="value" name="带宽" stroke="#8884d8" />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function formatBytes(bytes: number) {
|
|
if (!bytes || isNaN(bytes)) return "0 B"
|
|
const k = 1024
|
|
const sizes = ["B", "KB", "MB", "GB"]
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
|
|
}
|
|
|
|
function formatDate(dateStr: string) {
|
|
if (!dateStr) return "-"
|
|
const date = new Date(dateStr)
|
|
return date.toLocaleString()
|
|
}
|
|
|
|
function formatLatency(nanoseconds: number) {
|
|
if (!nanoseconds || isNaN(nanoseconds)) return "-"
|
|
if (nanoseconds < 1000) {
|
|
return nanoseconds + " ns"
|
|
} else if (nanoseconds < 1000000) {
|
|
return (nanoseconds / 1000).toFixed(2) + " µs"
|
|
} else if (nanoseconds < 1000000000) {
|
|
return (nanoseconds / 1000000).toFixed(2) + " ms"
|
|
} else {
|
|
return (nanoseconds / 1000000000).toFixed(2) + " s"
|
|
}
|
|
}
|
|
|
|
function getStatusColor(status: number) {
|
|
if (status >= 500) return "bg-red-100 text-red-800"
|
|
if (status >= 400) return "bg-yellow-100 text-yellow-800"
|
|
if (status >= 300) return "bg-blue-100 text-blue-800"
|
|
return "bg-green-100 text-green-800"
|
|
}
|