mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-18 08:31:55 +08:00
feat(metrics): 增强指标展示和统计功能
- 在指标结构中新增延迟统计、错误统计和引用来源统计字段 - 更新前端仪表盘,添加延迟、带宽、错误和引用来源统计卡片 - 优化指标收集器,支持引用来源和错误类型统计 - 在工具函数中新增字符串转整数解析方法 - 简化引用来源URL处理,提取域名信息
This commit is contained in:
parent
f0c806292b
commit
b6b77b03ed
@ -11,7 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Metrics 定义指标结构
|
// Metrics 定义指标结构,与前端期望的数据结构保持一致
|
||||||
type Metrics struct {
|
type Metrics struct {
|
||||||
// 基础指标
|
// 基础指标
|
||||||
Uptime string `json:"uptime"`
|
Uptime string `json:"uptime"`
|
||||||
@ -28,13 +28,39 @@ type Metrics struct {
|
|||||||
AverageResponseTime string `json:"avg_response_time"`
|
AverageResponseTime string `json:"avg_response_time"`
|
||||||
RequestsPerSecond float64 `json:"requests_per_second"`
|
RequestsPerSecond float64 `json:"requests_per_second"`
|
||||||
|
|
||||||
// 新增字段
|
// 传输指标
|
||||||
TotalBytes int64 `json:"total_bytes"`
|
TotalBytes int64 `json:"total_bytes"`
|
||||||
BytesPerSecond float64 `json:"bytes_per_second"`
|
BytesPerSecond float64 `json:"bytes_per_second"`
|
||||||
StatusCodeStats map[string]int64 `json:"status_code_stats"`
|
|
||||||
TopPaths []models.PathMetricsJSON `json:"top_paths"`
|
// 状态码统计
|
||||||
RecentRequests []models.RequestLog `json:"recent_requests"`
|
StatusCodeStats map[string]int64 `json:"status_code_stats"`
|
||||||
TopReferers []models.PathMetricsJSON `json:"top_referers"`
|
|
||||||
|
// 路径统计
|
||||||
|
TopPaths []models.PathMetricsJSON `json:"top_paths"`
|
||||||
|
|
||||||
|
// 最近请求
|
||||||
|
RecentRequests []models.RequestLog `json:"recent_requests"`
|
||||||
|
|
||||||
|
// 引用来源统计
|
||||||
|
TopReferers []models.PathMetricsJSON `json:"top_referers"`
|
||||||
|
|
||||||
|
// 延迟统计
|
||||||
|
LatencyStats struct {
|
||||||
|
Min string `json:"min"`
|
||||||
|
Max string `json:"max"`
|
||||||
|
Distribution map[string]int64 `json:"distribution"`
|
||||||
|
} `json:"latency_stats"`
|
||||||
|
|
||||||
|
// 错误统计
|
||||||
|
ErrorStats struct {
|
||||||
|
ClientErrors int64 `json:"client_errors"`
|
||||||
|
ServerErrors int64 `json:"server_errors"`
|
||||||
|
Types map[string]int64 `json:"types"`
|
||||||
|
} `json:"error_stats"`
|
||||||
|
|
||||||
|
// 带宽统计
|
||||||
|
BandwidthHistory map[string]string `json:"bandwidth_history"`
|
||||||
|
CurrentBandwidth string `json:"current_bandwidth"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetricsHandler 处理指标请求
|
// MetricsHandler 处理指标请求
|
||||||
@ -57,10 +83,16 @@ func (h *ProxyHandler) MetricsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"bytes_per_second": float64(0),
|
"bytes_per_second": float64(0),
|
||||||
"requests_per_second": float64(0),
|
"requests_per_second": float64(0),
|
||||||
"status_code_stats": make(map[string]int64),
|
"status_code_stats": make(map[string]int64),
|
||||||
"latency_percentiles": make([]float64, 0),
|
|
||||||
"top_paths": make([]models.PathMetrics, 0),
|
"top_paths": make([]models.PathMetrics, 0),
|
||||||
"recent_requests": make([]models.RequestLog, 0),
|
"recent_requests": make([]models.RequestLog, 0),
|
||||||
"top_referers": make([]models.PathMetrics, 0),
|
"top_referers": make([]models.PathMetrics, 0),
|
||||||
|
"latency_stats": map[string]interface{}{
|
||||||
|
"min": "0ms",
|
||||||
|
"max": "0ms",
|
||||||
|
"distribution": make(map[string]int64),
|
||||||
|
},
|
||||||
|
"bandwidth_history": make(map[string]string),
|
||||||
|
"current_bandwidth": "0 B/s",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +101,41 @@ func (h *ProxyHandler) MetricsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
totalBytes := utils.SafeInt64(stats["total_bytes"])
|
totalBytes := utils.SafeInt64(stats["total_bytes"])
|
||||||
uptimeSeconds := uptime.Seconds()
|
uptimeSeconds := uptime.Seconds()
|
||||||
|
|
||||||
|
// 处理延迟统计数据
|
||||||
|
latencyStats := make(map[string]interface{})
|
||||||
|
if stats["latency_stats"] != nil {
|
||||||
|
latencyStats = stats["latency_stats"].(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理带宽历史数据
|
||||||
|
bandwidthHistory := make(map[string]string)
|
||||||
|
if stats["bandwidth_history"] != nil {
|
||||||
|
for k, v := range stats["bandwidth_history"].(map[string]string) {
|
||||||
|
bandwidthHistory[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算客户端错误和服务器错误数量
|
||||||
|
var clientErrors, serverErrors int64
|
||||||
|
statusCodeStats := models.SafeStatusCodeStats(stats["status_code_stats"])
|
||||||
|
for code, count := range statusCodeStats {
|
||||||
|
codeInt := utils.ParseInt(code, 0)
|
||||||
|
if codeInt >= 400 && codeInt < 500 {
|
||||||
|
clientErrors += count
|
||||||
|
} else if codeInt >= 500 {
|
||||||
|
serverErrors += count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建错误类型统计
|
||||||
|
errorTypes := make(map[string]int64)
|
||||||
|
if clientErrors > 0 {
|
||||||
|
errorTypes["客户端错误"] = clientErrors
|
||||||
|
}
|
||||||
|
if serverErrors > 0 {
|
||||||
|
errorTypes["服务器错误"] = serverErrors
|
||||||
|
}
|
||||||
|
|
||||||
metrics := Metrics{
|
metrics := Metrics{
|
||||||
Uptime: metrics.FormatUptime(uptime),
|
Uptime: metrics.FormatUptime(uptime),
|
||||||
ActiveRequests: utils.SafeInt64(stats["active_requests"]),
|
ActiveRequests: utils.SafeInt64(stats["active_requests"]),
|
||||||
@ -81,12 +148,33 @@ func (h *ProxyHandler) MetricsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
TotalBytes: totalBytes,
|
TotalBytes: totalBytes,
|
||||||
BytesPerSecond: float64(totalBytes) / utils.MaxFloat64(uptimeSeconds, 1),
|
BytesPerSecond: float64(totalBytes) / utils.MaxFloat64(uptimeSeconds, 1),
|
||||||
RequestsPerSecond: float64(totalRequests) / utils.MaxFloat64(uptimeSeconds, 1),
|
RequestsPerSecond: float64(totalRequests) / utils.MaxFloat64(uptimeSeconds, 1),
|
||||||
StatusCodeStats: models.SafeStatusCodeStats(stats["status_code_stats"]),
|
StatusCodeStats: statusCodeStats,
|
||||||
TopPaths: models.SafePathMetrics(stats["top_paths"]),
|
TopPaths: models.SafePathMetrics(stats["top_paths"]),
|
||||||
RecentRequests: models.SafeRequestLogs(stats["recent_requests"]),
|
RecentRequests: models.SafeRequestLogs(stats["recent_requests"]),
|
||||||
TopReferers: models.SafePathMetrics(stats["top_referers"]),
|
TopReferers: models.SafePathMetrics(stats["top_referers"]),
|
||||||
|
BandwidthHistory: bandwidthHistory,
|
||||||
|
CurrentBandwidth: utils.SafeString(stats["current_bandwidth"], "0 B/s"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 填充延迟统计数据
|
||||||
|
metrics.LatencyStats.Min = utils.SafeString(latencyStats["min"], "0ms")
|
||||||
|
metrics.LatencyStats.Max = utils.SafeString(latencyStats["max"], "0ms")
|
||||||
|
|
||||||
|
// 处理分布数据
|
||||||
|
if distribution, ok := latencyStats["distribution"].(map[string]interface{}); ok {
|
||||||
|
metrics.LatencyStats.Distribution = make(map[string]int64)
|
||||||
|
for k, v := range distribution {
|
||||||
|
if intValue, ok := v.(float64); ok {
|
||||||
|
metrics.LatencyStats.Distribution[k] = int64(intValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充错误统计数据
|
||||||
|
metrics.ErrorStats.ClientErrors = clientErrors
|
||||||
|
metrics.ErrorStats.ServerErrors = serverErrors
|
||||||
|
metrics.ErrorStats.Types = errorTypes
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
if err := json.NewEncoder(w).Encode(metrics); err != nil {
|
if err := json.NewEncoder(w).Encode(metrics); err != nil {
|
||||||
log.Printf("Error encoding metrics: %v", err)
|
log.Printf("Error encoding metrics: %v", err)
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"proxy-go/internal/utils"
|
"proxy-go/internal/utils"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@ -26,6 +27,7 @@ type Collector struct {
|
|||||||
pathStats sync.Map
|
pathStats sync.Map
|
||||||
statusCodeStats sync.Map
|
statusCodeStats sync.Map
|
||||||
latencyBuckets sync.Map // 响应时间分布
|
latencyBuckets sync.Map // 响应时间分布
|
||||||
|
refererStats sync.Map // 引用来源统计
|
||||||
bandwidthStats struct {
|
bandwidthStats struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
window time.Duration
|
window time.Duration
|
||||||
@ -171,6 +173,36 @@ func (c *Collector) RecordRequest(path string, status int, latency time.Duration
|
|||||||
}
|
}
|
||||||
c.pathStatsMutex.Unlock()
|
c.pathStatsMutex.Unlock()
|
||||||
|
|
||||||
|
// 更新引用来源统计
|
||||||
|
if r != nil {
|
||||||
|
referer := r.Header.Get("Referer")
|
||||||
|
if referer != "" {
|
||||||
|
// 简化引用来源,只保留域名部分
|
||||||
|
referer = simplifyReferer(referer)
|
||||||
|
|
||||||
|
if value, ok := c.refererStats.Load(referer); ok {
|
||||||
|
stat := value.(*models.PathMetrics)
|
||||||
|
stat.AddRequest()
|
||||||
|
if status >= 400 {
|
||||||
|
stat.AddError()
|
||||||
|
}
|
||||||
|
stat.AddLatency(int64(latency))
|
||||||
|
stat.AddBytes(bytes)
|
||||||
|
} else {
|
||||||
|
newStat := &models.PathMetrics{
|
||||||
|
Path: referer,
|
||||||
|
}
|
||||||
|
newStat.RequestCount.Store(1)
|
||||||
|
if status >= 400 {
|
||||||
|
newStat.ErrorCount.Store(1)
|
||||||
|
}
|
||||||
|
newStat.TotalLatency.Store(int64(latency))
|
||||||
|
newStat.BytesTransferred.Store(bytes)
|
||||||
|
c.refererStats.Store(referer, newStat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 更新最近请求记录
|
// 更新最近请求记录
|
||||||
c.recentRequests.Push(models.RequestLog{
|
c.recentRequests.Push(models.RequestLog{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
@ -275,6 +307,41 @@ func (c *Collector) GetStats() map[string]interface{} {
|
|||||||
pathMetricsValues[i] = metric.ToJSON()
|
pathMetricsValues[i] = metric.ToJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 收集引用来源统计
|
||||||
|
var refererMetrics []*models.PathMetrics
|
||||||
|
c.refererStats.Range(func(key, value interface{}) bool {
|
||||||
|
stats := value.(*models.PathMetrics)
|
||||||
|
requestCount := stats.GetRequestCount()
|
||||||
|
if requestCount > 0 {
|
||||||
|
totalLatency := stats.GetTotalLatency()
|
||||||
|
avgLatencyMs := float64(totalLatency) / float64(requestCount) / float64(time.Millisecond)
|
||||||
|
stats.AvgLatency = fmt.Sprintf("%.2fms", avgLatencyMs)
|
||||||
|
refererMetrics = append(refererMetrics, stats)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 按请求数降序排序,请求数相同时按引用来源字典序排序
|
||||||
|
sort.Slice(refererMetrics, func(i, j int) bool {
|
||||||
|
countI := refererMetrics[i].GetRequestCount()
|
||||||
|
countJ := refererMetrics[j].GetRequestCount()
|
||||||
|
if countI != countJ {
|
||||||
|
return countI > countJ
|
||||||
|
}
|
||||||
|
return refererMetrics[i].Path < refererMetrics[j].Path
|
||||||
|
})
|
||||||
|
|
||||||
|
// 只保留前10个
|
||||||
|
if len(refererMetrics) > 10 {
|
||||||
|
refererMetrics = refererMetrics[:10]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为值切片
|
||||||
|
refererMetricsValues := make([]models.PathMetricsJSON, len(refererMetrics))
|
||||||
|
for i, metric := range refererMetrics {
|
||||||
|
refererMetricsValues[i] = metric.ToJSON()
|
||||||
|
}
|
||||||
|
|
||||||
// 收集延迟分布
|
// 收集延迟分布
|
||||||
latencyDistribution := make(map[string]int64)
|
latencyDistribution := make(map[string]int64)
|
||||||
c.latencyBuckets.Range(func(key, value interface{}) bool {
|
c.latencyBuckets.Range(func(key, value interface{}) bool {
|
||||||
@ -310,6 +377,7 @@ func (c *Collector) GetStats() map[string]interface{} {
|
|||||||
"bytes_per_second": float64(atomic.LoadInt64(&c.totalBytes)) / totalRuntime.Seconds(),
|
"bytes_per_second": float64(atomic.LoadInt64(&c.totalBytes)) / totalRuntime.Seconds(),
|
||||||
"status_code_stats": statusCodeStats,
|
"status_code_stats": statusCodeStats,
|
||||||
"top_paths": pathMetricsValues,
|
"top_paths": pathMetricsValues,
|
||||||
|
"top_referers": refererMetricsValues,
|
||||||
"recent_requests": recentRequests,
|
"recent_requests": recentRequests,
|
||||||
"latency_stats": map[string]interface{}{
|
"latency_stats": map[string]interface{}{
|
||||||
"min": fmt.Sprintf("%.2fms", float64(minLatency)/float64(time.Millisecond)),
|
"min": fmt.Sprintf("%.2fms", float64(minLatency)/float64(time.Millisecond)),
|
||||||
@ -476,3 +544,18 @@ func (c *Collector) getBandwidthHistory() map[string]string {
|
|||||||
}
|
}
|
||||||
return history
|
return history
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// simplifyReferer 简化引用来源URL,只保留域名部分
|
||||||
|
func simplifyReferer(referer string) string {
|
||||||
|
// 移除协议部分
|
||||||
|
if idx := strings.Index(referer, "://"); idx != -1 {
|
||||||
|
referer = referer[idx+3:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只保留域名部分
|
||||||
|
if idx := strings.Index(referer, "/"); idx != -1 {
|
||||||
|
referer = referer[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
return referer
|
||||||
|
}
|
||||||
|
@ -358,3 +358,13 @@ func MaxFloat64(a, b float64) float64 {
|
|||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseInt 将字符串解析为整数,如果解析失败则返回默认值
|
||||||
|
func ParseInt(s string, defaultValue int) int {
|
||||||
|
var result int
|
||||||
|
_, err := fmt.Sscanf(s, "%d", &result)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
@ -15,6 +15,7 @@ interface Metrics {
|
|||||||
avg_response_time: string
|
avg_response_time: string
|
||||||
requests_per_second: number
|
requests_per_second: number
|
||||||
bytes_per_second: number
|
bytes_per_second: number
|
||||||
|
error_rate: number
|
||||||
status_code_stats: Record<string, number>
|
status_code_stats: Record<string, number>
|
||||||
top_paths: Array<{
|
top_paths: Array<{
|
||||||
path: string
|
path: string
|
||||||
@ -44,6 +45,13 @@ interface Metrics {
|
|||||||
bandwidth_history: Record<string, string>
|
bandwidth_history: Record<string, string>
|
||||||
current_bandwidth: string
|
current_bandwidth: string
|
||||||
total_bytes: number
|
total_bytes: number
|
||||||
|
top_referers: Array<{
|
||||||
|
path: string
|
||||||
|
request_count: number
|
||||||
|
error_count: number
|
||||||
|
avg_latency: string
|
||||||
|
bytes_transferred: number
|
||||||
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
@ -221,7 +229,7 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className={`text-lg font-semibold ${colorClass}`}>{count}</div>
|
<div className={`text-lg font-semibold ${colorClass}`}>{count}</div>
|
||||||
<div className="text-sm text-gray-500 mt-1">
|
<div className="text-sm text-gray-500 mt-1">
|
||||||
{totalRequests ?
|
{totalRequests ?
|
||||||
((count as number / totalRequests) * 100).toFixed(1) : 0}%
|
((count as number / totalRequests) * 100).toFixed(1) : 0}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -231,6 +239,183 @@ export default function DashboardPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
||||||
|
{/* 新增:延迟统计卡片 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>延迟统计</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<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.latency_stats?.min || "0ms"}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-500">最大响应时间</div>
|
||||||
|
<div className="text-lg font-semibold">{metrics.latency_stats?.max || "0ms"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-500 mb-2">响应时间分布</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||||
|
{metrics.latency_stats?.distribution &&
|
||||||
|
Object.entries(metrics.latency_stats.distribution)
|
||||||
|
.sort((a, b) => {
|
||||||
|
// 按照延迟范围排序
|
||||||
|
const order = ["<10ms", "10-50ms", "50-200ms", "200-1000ms", ">1s"];
|
||||||
|
return order.indexOf(a[0]) - order.indexOf(b[0]);
|
||||||
|
})
|
||||||
|
.map(([range, count]) => (
|
||||||
|
<div key={range} className="p-3 rounded-lg border bg-card text-card-foreground shadow-sm">
|
||||||
|
<div className="text-sm font-medium text-gray-500">{range}</div>
|
||||||
|
<div className="text-lg font-semibold">{count}</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
|
{Object.values(metrics.latency_stats?.distribution || {}).reduce((sum, val) => sum + val, 0) > 0
|
||||||
|
? ((count / Object.values(metrics.latency_stats?.distribution || {}).reduce((sum, val) => sum + val, 0)) * 100).toFixed(1)
|
||||||
|
: 0}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>带宽统计</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-500">当前带宽</div>
|
||||||
|
<div className="text-lg font-semibold">{metrics.current_bandwidth || "0 B/s"}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-500 mb-2">带宽历史</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||||
|
{metrics.bandwidth_history &&
|
||||||
|
Object.entries(metrics.bandwidth_history)
|
||||||
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
|
.map(([time, bandwidth]) => (
|
||||||
|
<div key={time} className="p-3 rounded-lg border bg-card text-card-foreground shadow-sm">
|
||||||
|
<div className="text-sm font-medium text-gray-500">{time}</div>
|
||||||
|
<div className="text-lg font-semibold">{bandwidth}</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 错误统计卡片 */}
|
||||||
|
<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">客户端错误 (4xx)</div>
|
||||||
|
<div className="text-2xl font-semibold text-yellow-600">
|
||||||
|
{metrics.error_stats?.client_errors || 0}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
占总请求的 {metrics.total_requests ?
|
||||||
|
((metrics.error_stats?.client_errors || 0) / metrics.total_requests * 100).toFixed(2) : 0}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium text-gray-500">服务器错误 (5xx)</div>
|
||||||
|
<div className="text-2xl font-semibold text-red-600">
|
||||||
|
{metrics.error_stats?.server_errors || 0}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
占总请求的 {metrics.total_requests ?
|
||||||
|
((metrics.error_stats?.server_errors || 0) / metrics.total_requests * 100).toFixed(2) : 0}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium text-gray-500">总错误率</div>
|
||||||
|
<div className="text-2xl font-semibold">
|
||||||
|
{(metrics.error_rate * 100).toFixed(2)}%
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
总错误数: {metrics.total_errors || 0}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{metrics.error_stats?.types && Object.keys(metrics.error_stats.types).length > 0 && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="text-sm font-medium text-gray-500 mb-2">错误类型分布</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{Object.entries(metrics.error_stats.types).map(([type, count]) => (
|
||||||
|
<div key={type} className="p-3 rounded-lg border bg-card text-card-foreground shadow-sm">
|
||||||
|
<div className="text-sm font-medium text-gray-500">{type}</div>
|
||||||
|
<div className="text-lg font-semibold">{count}</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
|
{metrics.total_errors ? ((count / metrics.total_errors) * 100).toFixed(1) : 0}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 引用来源统计卡片 */}
|
||||||
|
{metrics.top_referers && metrics.top_referers.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>引用来源统计 (Top {metrics.top_referers.length})</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_referers.map((referer, index) => (
|
||||||
|
<tr key={index} className="border-b">
|
||||||
|
<td className="p-2">
|
||||||
|
<span className="text-blue-600">
|
||||||
|
{referer.path}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-2">{referer.request_count}</td>
|
||||||
|
<td className="p-2">{referer.error_count}</td>
|
||||||
|
<td className="p-2">{referer.avg_latency}</td>
|
||||||
|
<td className="p-2">{formatBytes(referer.bytes_transferred)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>热门路径 (Top 10)</CardTitle>
|
<CardTitle>热门路径 (Top 10)</CardTitle>
|
||||||
@ -293,37 +478,38 @@ export default function DashboardPage() {
|
|||||||
{(metrics.recent_requests || [])
|
{(metrics.recent_requests || [])
|
||||||
.slice(0, 20) // 只显示最近20条记录
|
.slice(0, 20) // 只显示最近20条记录
|
||||||
.map((req, index) => (
|
.map((req, index) => (
|
||||||
<tr key={index} className="border-b hover:bg-gray-50">
|
<tr key={index} className="border-b hover:bg-gray-50">
|
||||||
<td className="p-2">{formatDate(req.Time)}</td>
|
<td className="p-2">{formatDate(req.Time)}</td>
|
||||||
<td className="p-2 max-w-xs truncate">
|
<td className="p-2 max-w-xs truncate">
|
||||||
<a
|
<a
|
||||||
href={req.Path}
|
href={req.Path}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-blue-600 hover:text-blue-800 hover:underline"
|
className="text-blue-600 hover:text-blue-800 hover:underline"
|
||||||
>
|
>
|
||||||
{req.Path}
|
{req.Path}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 rounded-full text-xs ${getStatusColor(
|
className={`px-2 py-1 rounded-full text-xs ${getStatusColor(
|
||||||
req.Status
|
req.Status
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{req.Status}
|
{req.Status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2">{formatLatency(req.Latency)}</td>
|
<td className="p-2">{formatLatency(req.Latency)}</td>
|
||||||
<td className="p-2">{formatBytes(req.BytesSent)}</td>
|
<td className="p-2">{formatBytes(req.BytesSent)}</td>
|
||||||
<td className="p-2">{req.ClientIP}</td>
|
<td className="p-2">{req.ClientIP}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user