wood chen 3e5950e3f6 feat(metrics,web): Enhance dashboard with advanced metrics visualization
- 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
2025-02-15 16:23:20 +08:00

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"
}