refactor(metrics): Comprehensive metrics system redesign with improved performance tracking

- Refactor Collector struct to use more efficient atomic counters and data structures
- Implement robust bandwidth tracking with sliding window mechanism
- Enhance path metrics with atomic operations and improved type safety
- Add data consistency checker for metrics integrity
- Optimize request logging with a more memory-efficient queue
- Simplify metrics collection and reporting logic
- Improve frontend metrics display with more detailed status code visualization
This commit is contained in:
wood chen 2025-02-17 07:46:58 +08:00
parent ff24191146
commit d0d752712e
5 changed files with 347 additions and 274 deletions

View File

@ -17,35 +17,25 @@ import (
// Collector 指标收集器 // Collector 指标收集器
type Collector struct { type Collector struct {
startTime time.Time startTime time.Time
activeRequests int64 activeRequests int64
totalRequests int64 totalBytes int64
totalErrors int64 latencySum int64
totalBytes int64 maxLatency int64 // 最大响应时间
latencySum int64 minLatency int64 // 最小响应时间
maxLatency int64 // 最大响应时间 pathStats sync.Map
minLatency int64 // 最小响应时间 statusCodeStats sync.Map
clientErrors int64 // 4xx错误 latencyBuckets sync.Map // 响应时间分布
serverErrors int64 // 5xx错误 bandwidthStats struct {
pathStats sync.Map sync.RWMutex
statusCodeStats sync.Map window time.Duration
latencyBuckets sync.Map // 响应时间分布 lastUpdate time.Time
bandwidthStats sync.Map // 带宽统计 current int64
errorTypes sync.Map // 错误类型统计 history map[string]int64
recentRequests []models.RequestLog }
recentRequestsMutex sync.RWMutex recentRequests *models.RequestQueue
pathStatsMutex sync.RWMutex pathStatsMutex sync.RWMutex
config *config.Config config *config.Config
lastMinute time.Time // 用于计算每分钟带宽
minuteBytes int64 // 当前分钟的字节数
// 新增:时间段统计
lastStatsTime time.Time
intervalRequests int64
intervalErrors int64
intervalBytes int64
intervalLatencySum int64
intervalStatusCodes sync.Map
} }
var ( var (
@ -58,19 +48,25 @@ func InitCollector(cfg *config.Config) error {
once.Do(func() { once.Do(func() {
instance = &Collector{ instance = &Collector{
startTime: time.Now(), startTime: time.Now(),
lastMinute: time.Now(), recentRequests: models.NewRequestQueue(100),
lastStatsTime: time.Now(),
recentRequests: make([]models.RequestLog, 0, 1000),
config: cfg, config: cfg,
minLatency: math.MaxInt64, // 初始化为最大值 minLatency: math.MaxInt64,
} }
// 初始化带宽统计
instance.bandwidthStats.window = time.Minute
instance.bandwidthStats.lastUpdate = time.Now()
instance.bandwidthStats.history = make(map[string]int64)
// 初始化延迟分布桶 // 初始化延迟分布桶
instance.latencyBuckets.Store("<10ms", new(int64)) instance.latencyBuckets.Store("<10ms", new(int64))
instance.latencyBuckets.Store("10-50ms", new(int64)) instance.latencyBuckets.Store("10-50ms", new(int64))
instance.latencyBuckets.Store("50-200ms", new(int64)) instance.latencyBuckets.Store("50-200ms", new(int64))
instance.latencyBuckets.Store("200-1000ms", new(int64)) instance.latencyBuckets.Store("200-1000ms", new(int64))
instance.latencyBuckets.Store(">1s", new(int64)) instance.latencyBuckets.Store(">1s", new(int64))
// 启动数据一致性检查器
instance.startConsistencyChecker()
}) })
return nil return nil
} }
@ -92,41 +88,22 @@ func (c *Collector) EndRequest() {
// RecordRequest 记录请求 // RecordRequest 记录请求
func (c *Collector) RecordRequest(path string, status int, latency time.Duration, bytes int64, clientIP string, r *http.Request) { func (c *Collector) RecordRequest(path string, status int, latency time.Duration, bytes int64, clientIP string, r *http.Request) {
// 更新总体统计 // 更新状态码统计
atomic.AddInt64(&c.totalRequests, 1)
atomic.AddInt64(&c.totalBytes, bytes)
atomic.AddInt64(&c.latencySum, int64(latency))
// 更新时间段统计
atomic.AddInt64(&c.intervalRequests, 1)
atomic.AddInt64(&c.intervalBytes, bytes)
atomic.AddInt64(&c.intervalLatencySum, int64(latency))
if status >= 400 {
atomic.AddInt64(&c.intervalErrors, 1)
}
// 更新带宽统计
atomic.AddInt64(&c.minuteBytes, bytes)
now := time.Now()
if now.Sub(c.lastMinute) >= time.Minute {
currentMinute := now.Format("15:04")
counter := new(int64)
*counter = atomic.SwapInt64(&c.minuteBytes, 0)
c.bandwidthStats.Store(currentMinute, counter)
c.lastMinute = now
}
// 更新时间段状态码统计
statusKey := fmt.Sprintf("%d", status) statusKey := fmt.Sprintf("%d", status)
if counter, ok := c.intervalStatusCodes.Load(statusKey); ok { if counter, ok := c.statusCodeStats.Load(statusKey); ok {
atomic.AddInt64(counter.(*int64), 1) atomic.AddInt64(counter.(*int64), 1)
} else { } else {
counter := new(int64) counter := new(int64)
*counter = 1 *counter = 1
c.intervalStatusCodes.Store(statusKey, counter) c.statusCodeStats.Store(statusKey, counter)
} }
// 更新最小和最大响应时间 // 更新总字节数和带宽统计
atomic.AddInt64(&c.totalBytes, bytes)
c.updateBandwidthStats(bytes)
// 更新延迟统计
atomic.AddInt64(&c.latencySum, int64(latency))
latencyNanos := int64(latency) latencyNanos := int64(latency)
for { for {
oldMin := atomic.LoadInt64(&c.minLatency) oldMin := atomic.LoadInt64(&c.minLatency)
@ -170,80 +147,39 @@ func (c *Collector) RecordRequest(path string, status int, latency time.Duration
c.latencyBuckets.Store(bucketKey, counter) c.latencyBuckets.Store(bucketKey, counter)
} }
// 更新错误统计
if status >= 400 {
atomic.AddInt64(&c.totalErrors, 1)
if status >= 500 {
atomic.AddInt64(&c.serverErrors, 1)
} else {
atomic.AddInt64(&c.clientErrors, 1)
}
errKey := fmt.Sprintf("%d %s", status, http.StatusText(status))
if counter, ok := c.errorTypes.Load(errKey); ok {
atomic.AddInt64(counter.(*int64), 1)
} else {
counter := new(int64)
*counter = 1
c.errorTypes.Store(errKey, counter)
}
}
// 收集状态码统计
statusCodeStats := make(map[string]int64)
c.statusCodeStats.Range(func(key, value interface{}) bool {
if counter, ok := value.(*int64); ok {
statusCodeStats[key.(string)] = atomic.LoadInt64(counter)
} else {
statusCodeStats[key.(string)] = value.(int64)
}
return true
})
// 更新状态码统计
statusKey = fmt.Sprintf("%d", status)
if counter, ok := c.statusCodeStats.Load(statusKey); ok {
atomic.AddInt64(counter.(*int64), 1)
} else {
counter := new(int64)
*counter = 1
c.statusCodeStats.Store(statusKey, counter)
}
// 更新路径统计 // 更新路径统计
c.pathStatsMutex.Lock() c.pathStatsMutex.Lock()
if value, ok := c.pathStats.Load(path); ok { if value, ok := c.pathStats.Load(path); ok {
stat := value.(*models.PathMetrics) stat := value.(*models.PathMetrics)
atomic.AddInt64(&stat.RequestCount, 1) stat.AddRequest()
if status >= 400 { if status >= 400 {
atomic.AddInt64(&stat.ErrorCount, 1) stat.AddError()
} }
atomic.AddInt64(&stat.TotalLatency, int64(latency)) stat.AddLatency(int64(latency))
atomic.AddInt64(&stat.BytesTransferred, bytes) stat.AddBytes(bytes)
} else { } else {
c.pathStats.Store(path, &models.PathMetrics{ newStat := &models.PathMetrics{
Path: path, Path: path,
RequestCount: 1, }
ErrorCount: map[bool]int64{true: 1, false: 0}[status >= 400], newStat.AddRequest()
TotalLatency: int64(latency), if status >= 400 {
BytesTransferred: bytes, newStat.AddError()
}) }
newStat.AddLatency(int64(latency))
newStat.AddBytes(bytes)
c.pathStats.Store(path, newStat)
} }
c.pathStatsMutex.Unlock() c.pathStatsMutex.Unlock()
// 更新最近请求记录 // 更新最近请求记录
c.recentRequestsMutex.Lock() c.recentRequests.Push(models.RequestLog{
c.recentRequests = append([]models.RequestLog{{
Time: time.Now(), Time: time.Now(),
Path: path, Path: path,
Status: status, Status: status,
Latency: int64(latency), Latency: int64(latency),
BytesSent: bytes, BytesSent: bytes,
ClientIP: clientIP, ClientIP: clientIP,
}}, c.recentRequests...) })
if len(c.recentRequests) > 100 { // 只保留最近100条记录
c.recentRequests = c.recentRequests[:100]
}
c.recentRequestsMutex.Unlock()
} }
// FormatUptime 格式化运行时间 // FormatUptime 格式化运行时间
@ -267,54 +203,58 @@ func FormatUptime(d time.Duration) string {
// GetStats 获取统计数据 // GetStats 获取统计数据
func (c *Collector) GetStats() map[string]interface{} { func (c *Collector) GetStats() map[string]interface{} {
// 获取统计数据
var mem runtime.MemStats var mem runtime.MemStats
runtime.ReadMemStats(&mem) runtime.ReadMemStats(&mem)
now := time.Now() now := time.Now()
interval := now.Sub(c.lastStatsTime) totalRuntime := now.Sub(c.startTime)
c.lastStatsTime = now
// 获取并重置时间段统计 // 计算总请求数和平均延迟
intervalRequests := atomic.SwapInt64(&c.intervalRequests, 0) var totalRequests int64
intervalBytes := atomic.SwapInt64(&c.intervalBytes, 0) c.statusCodeStats.Range(func(key, value interface{}) bool {
intervalLatencySum := atomic.SwapInt64(&c.intervalLatencySum, 0) if counter, ok := value.(*int64); ok {
intervalErrors := atomic.SwapInt64(&c.intervalErrors, 0) totalRequests += atomic.LoadInt64(counter)
}
// 计算时间段平均延迟
avgLatency := float64(0)
if intervalRequests > 0 {
avgLatency = float64(intervalLatencySum) / float64(intervalRequests)
}
// 收集并重置时间段状态码统计
intervalStatusStats := make(map[string]int64)
c.intervalStatusCodes.Range(func(key, value interface{}) bool {
intervalStatusStats[key.(string)] = atomic.SwapInt64(value.(*int64), 0)
return true return true
}) })
avgLatency := float64(0)
if totalRequests > 0 {
avgLatency = float64(atomic.LoadInt64(&c.latencySum)) / float64(totalRequests)
}
// 计算总体平均每秒请求数
requestsPerSecond := float64(totalRequests) / totalRuntime.Seconds()
// 收集状态码统计 // 收集状态码统计
statusCodeStats := make(map[string]int64) statusCodeStats := make(map[string]int64)
c.statusCodeStats.Range(func(key, value interface{}) bool { c.statusCodeStats.Range(func(key, value interface{}) bool {
statusCodeStats[key.(string)] = atomic.LoadInt64(value.(*int64)) if counter, ok := value.(*int64); ok {
statusCodeStats[key.(string)] = atomic.LoadInt64(counter)
} else {
statusCodeStats[key.(string)] = value.(int64)
}
return true return true
}) })
// 收集路径统计 // 收集路径统计
var pathMetrics []models.PathMetrics var pathMetrics []*models.PathMetrics
c.pathStats.Range(func(key, value interface{}) bool { c.pathStats.Range(func(key, value interface{}) bool {
stats := value.(*models.PathMetrics) stats := value.(*models.PathMetrics)
if stats.RequestCount > 0 { requestCount := stats.GetRequestCount()
avgLatencyMs := float64(stats.TotalLatency) / float64(stats.RequestCount) / float64(time.Millisecond) if requestCount > 0 {
totalLatency := stats.GetTotalLatency()
avgLatencyMs := float64(totalLatency) / float64(requestCount) / float64(time.Millisecond)
stats.AvgLatency = fmt.Sprintf("%.2fms", avgLatencyMs) stats.AvgLatency = fmt.Sprintf("%.2fms", avgLatencyMs)
} }
pathMetrics = append(pathMetrics, *stats) pathMetrics = append(pathMetrics, stats)
return true return true
}) })
// 按请求数降序排序 // 按请求数降序排序
sort.Slice(pathMetrics, func(i, j int) bool { sort.Slice(pathMetrics, func(i, j int) bool {
return pathMetrics[i].RequestCount > pathMetrics[j].RequestCount return pathMetrics[i].GetRequestCount() > pathMetrics[j].GetRequestCount()
}) })
// 只保留前10个 // 只保留前10个
@ -333,37 +273,8 @@ func (c *Collector) GetStats() map[string]interface{} {
return true return true
}) })
// 收集错误类型统计 // 获取最近请求记录(使用读锁)
errorTypeStats := make(map[string]int64) recentRequests := c.recentRequests.GetAll()
c.errorTypes.Range(func(key, value interface{}) bool {
if counter, ok := value.(*int64); ok {
errorTypeStats[key.(string)] = atomic.LoadInt64(counter)
} else {
errorTypeStats[key.(string)] = value.(int64)
}
return true
})
// 收集最近5分钟的带宽统计
bandwidthHistory := make(map[string]string)
var times []string
c.bandwidthStats.Range(func(key, value interface{}) bool {
times = append(times, key.(string))
return true
})
sort.Strings(times)
if len(times) > 5 {
times = times[len(times)-5:]
}
for _, t := range times {
if bytes, ok := c.bandwidthStats.Load(t); ok {
if counter, ok := bytes.(*int64); ok {
bandwidthHistory[t] = utils.FormatBytes(atomic.LoadInt64(counter)) + "/min"
} else {
bandwidthHistory[t] = utils.FormatBytes(bytes.(int64)) + "/min"
}
}
}
// 获取最小和最大响应时间 // 获取最小和最大响应时间
minLatency := atomic.LoadInt64(&c.minLatency) minLatency := atomic.LoadInt64(&c.minLatency)
@ -372,25 +283,19 @@ func (c *Collector) GetStats() map[string]interface{} {
minLatency = 0 minLatency = 0
} }
// 获取最近请求记录(加锁) // 收集带宽历史记录
c.recentRequestsMutex.RLock() bandwidthHistory := c.getBandwidthHistory()
recentRequests := make([]models.RequestLog, len(c.recentRequests))
copy(recentRequests, c.recentRequests)
c.recentRequestsMutex.RUnlock()
return map[string]interface{}{ return map[string]interface{}{
"uptime": FormatUptime(time.Since(c.startTime)), "uptime": FormatUptime(totalRuntime),
"active_requests": atomic.LoadInt64(&c.activeRequests), "active_requests": atomic.LoadInt64(&c.activeRequests),
"total_requests": atomic.LoadInt64(&c.totalRequests),
"total_errors": atomic.LoadInt64(&c.totalErrors),
"interval_errors": intervalErrors,
"total_bytes": atomic.LoadInt64(&c.totalBytes), "total_bytes": atomic.LoadInt64(&c.totalBytes),
"num_goroutine": runtime.NumGoroutine(), "num_goroutine": runtime.NumGoroutine(),
"memory_usage": utils.FormatBytes(int64(mem.Alloc)), "memory_usage": utils.FormatBytes(int64(mem.Alloc)),
"avg_response_time": fmt.Sprintf("%.2fms", avgLatency/float64(time.Millisecond)), "avg_response_time": fmt.Sprintf("%.2fms", avgLatency/float64(time.Millisecond)),
"requests_per_second": float64(intervalRequests) / interval.Seconds(), "requests_per_second": requestsPerSecond,
"bytes_per_second": float64(intervalBytes) / interval.Seconds(), "bytes_per_second": float64(atomic.LoadInt64(&c.totalBytes)) / totalRuntime.Seconds(),
"status_code_stats": intervalStatusStats, "status_code_stats": statusCodeStats,
"top_paths": pathMetrics, "top_paths": pathMetrics,
"recent_requests": recentRequests, "recent_requests": recentRequests,
"latency_stats": map[string]interface{}{ "latency_stats": map[string]interface{}{
@ -398,13 +303,8 @@ func (c *Collector) GetStats() map[string]interface{} {
"max": fmt.Sprintf("%.2fms", float64(maxLatency)/float64(time.Millisecond)), "max": fmt.Sprintf("%.2fms", float64(maxLatency)/float64(time.Millisecond)),
"distribution": latencyDistribution, "distribution": latencyDistribution,
}, },
"error_stats": map[string]interface{}{
"client_errors": atomic.LoadInt64(&c.clientErrors),
"server_errors": atomic.LoadInt64(&c.serverErrors),
"types": errorTypeStats,
},
"bandwidth_history": bandwidthHistory, "bandwidth_history": bandwidthHistory,
"current_bandwidth": utils.FormatBytes(atomic.LoadInt64(&c.minuteBytes)) + "/min", "current_bandwidth": utils.FormatBytes(int64(c.getCurrentBandwidth())) + "/s",
} }
} }
@ -429,36 +329,41 @@ func (c *Collector) LoadRecentStats() error {
// validateLoadedData 验证当前数据的有效性 // validateLoadedData 验证当前数据的有效性
func (c *Collector) validateLoadedData() error { func (c *Collector) validateLoadedData() error {
// 验证基础指标 // 验证基础指标
if c.totalRequests < 0 || if c.totalBytes < 0 ||
c.totalErrors < 0 || c.activeRequests < 0 {
c.totalBytes < 0 { return fmt.Errorf("invalid negative stats values")
return fmt.Errorf("invalid stats values")
}
// 验证错误数不能大于总请求数
if c.totalErrors > c.totalRequests {
return fmt.Errorf("total errors exceeds total requests")
} }
// 验证状态码统计 // 验证状态码统计
var statusCodeTotal int64
c.statusCodeStats.Range(func(key, value interface{}) bool { c.statusCodeStats.Range(func(key, value interface{}) bool {
return value.(int64) >= 0 count := atomic.LoadInt64(value.(*int64))
if count < 0 {
return false
}
statusCodeTotal += count
return true
}) })
// 验证路径统计 // 验证路径统计
var totalPathRequests int64 var totalPathRequests int64
c.pathStats.Range(func(_, value interface{}) bool { c.pathStats.Range(func(_, value interface{}) bool {
stats := value.(*models.PathMetrics) stats := value.(*models.PathMetrics)
if stats.RequestCount < 0 || stats.ErrorCount < 0 { requestCount := stats.GetRequestCount()
errorCount := stats.GetErrorCount()
if requestCount < 0 || errorCount < 0 {
return false return false
} }
totalPathRequests += stats.RequestCount if errorCount > requestCount {
return false
}
totalPathRequests += requestCount
return true return true
}) })
// 验证总数一致性 if totalPathRequests != statusCodeTotal {
if totalPathRequests > c.totalRequests { return fmt.Errorf("path stats total (%d) does not match status code total (%d)",
return fmt.Errorf("path stats total exceeds total requests") totalPathRequests, statusCodeTotal)
} }
return nil return nil
@ -474,8 +379,81 @@ func (c *Collector) GetLastSaveTime() time.Time {
// CheckDataConsistency 实现 interfaces.MetricsCollector 接口 // CheckDataConsistency 实现 interfaces.MetricsCollector 接口
func (c *Collector) CheckDataConsistency() error { func (c *Collector) CheckDataConsistency() error {
// 简单的数据验证 // 简单的数据验证
if c.totalErrors > c.totalRequests { if err := c.validateLoadedData(); err != nil {
return fmt.Errorf("total errors exceeds total requests") return err
} }
return nil return nil
} }
// 添加定期检查数据一致性的功能
func (c *Collector) startConsistencyChecker() {
go func() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
if err := c.validateLoadedData(); err != nil {
log.Printf("[Metrics] Data consistency check failed: %v", err)
// 可以在这里添加修复逻辑或报警通知
}
}
}()
}
// updateBandwidthStats 更新带宽统计
func (c *Collector) updateBandwidthStats(bytes int64) {
c.bandwidthStats.Lock()
defer c.bandwidthStats.Unlock()
now := time.Now()
if now.Sub(c.bandwidthStats.lastUpdate) >= c.bandwidthStats.window {
// 保存当前时间窗口的数据
key := c.bandwidthStats.lastUpdate.Format("01-02 15:04")
c.bandwidthStats.history[key] = c.bandwidthStats.current
// 清理旧数据保留最近5个时间窗口
if len(c.bandwidthStats.history) > 5 {
var oldestTime time.Time
var oldestKey string
for k := range c.bandwidthStats.history {
t, _ := time.Parse("01-02 15:04", k)
if oldestTime.IsZero() || t.Before(oldestTime) {
oldestTime = t
oldestKey = k
}
}
delete(c.bandwidthStats.history, oldestKey)
}
// 重置当前窗口
c.bandwidthStats.current = bytes
c.bandwidthStats.lastUpdate = now
} else {
c.bandwidthStats.current += bytes
}
}
// getCurrentBandwidth 获取当前带宽
func (c *Collector) getCurrentBandwidth() float64 {
c.bandwidthStats.RLock()
defer c.bandwidthStats.RUnlock()
now := time.Now()
duration := now.Sub(c.bandwidthStats.lastUpdate).Seconds()
if duration == 0 {
return 0
}
return float64(c.bandwidthStats.current) / duration
}
// getBandwidthHistory 获取带宽历史记录
func (c *Collector) getBandwidthHistory() map[string]string {
c.bandwidthStats.RLock()
defer c.bandwidthStats.RUnlock()
history := make(map[string]string)
for k, v := range c.bandwidthStats.history {
history[k] = utils.FormatBytes(v) + "/min"
}
return history
}

View File

@ -11,14 +11,54 @@ type PathStats struct {
LatencySum atomic.Int64 LatencySum atomic.Int64
} }
// PathMetrics 路径指标 // PathMetrics 路径统计信息
type PathMetrics struct { type PathMetrics struct {
Path string `json:"path"` Path string `json:"path"`
RequestCount int64 `json:"request_count"` RequestCount atomic.Int64 `json:"request_count"`
ErrorCount int64 `json:"error_count"` ErrorCount atomic.Int64 `json:"error_count"`
TotalLatency int64 `json:"total_latency"` TotalLatency atomic.Int64 `json:"-"`
BytesTransferred int64 `json:"bytes_transferred"` BytesTransferred atomic.Int64 `json:"bytes_transferred"`
AvgLatency string `json:"avg_latency"` AvgLatency string `json:"avg_latency"`
}
// GetRequestCount 获取请求数
func (p *PathMetrics) GetRequestCount() int64 {
return p.RequestCount.Load()
}
// GetErrorCount 获取错误数
func (p *PathMetrics) GetErrorCount() int64 {
return p.ErrorCount.Load()
}
// GetTotalLatency 获取总延迟
func (p *PathMetrics) GetTotalLatency() int64 {
return p.TotalLatency.Load()
}
// GetBytesTransferred 获取传输字节数
func (p *PathMetrics) GetBytesTransferred() int64 {
return p.BytesTransferred.Load()
}
// AddRequest 增加请求数
func (p *PathMetrics) AddRequest() {
p.RequestCount.Add(1)
}
// AddError 增加错误数
func (p *PathMetrics) AddError() {
p.ErrorCount.Add(1)
}
// AddLatency 增加延迟
func (p *PathMetrics) AddLatency(latency int64) {
p.TotalLatency.Add(latency)
}
// AddBytes 增加传输字节数
func (p *PathMetrics) AddBytes(bytes int64) {
p.BytesTransferred.Add(bytes)
} }
type HistoricalMetrics struct { type HistoricalMetrics struct {

View File

@ -7,12 +7,12 @@ import (
// RequestLog 请求日志 // RequestLog 请求日志
type RequestLog struct { type RequestLog struct {
Time time.Time `json:"Time"` Time time.Time `json:"time"`
Path string `json:"Path"` Path string `json:"path"`
Status int `json:"Status"` Status int `json:"status"`
Latency int64 `json:"Latency"` Latency int64 `json:"latency"`
BytesSent int64 `json:"BytesSent"` BytesSent int64 `json:"bytes_sent"`
ClientIP string `json:"ClientIP"` ClientIP string `json:"client_ip"`
} }
// RequestQueue 请求队列 // RequestQueue 请求队列

View File

@ -1,6 +1,6 @@
"use client" "use client"
import { useEffect, useState, useCallback } from "react" import { useEffect, useState, useCallback, useRef } from "react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { useToast } from "@/components/ui/use-toast" import { useToast } from "@/components/ui/use-toast"
@ -70,6 +70,9 @@ export default function ConfigPage() {
const { toast } = useToast() const { toast } = useToast()
const router = useRouter() const router = useRouter()
// 使用 ref 来保存滚动位置
const scrollPositionRef = useRef(0)
// 对话框状态 // 对话框状态
const [pathDialogOpen, setPathDialogOpen] = useState(false) const [pathDialogOpen, setPathDialogOpen] = useState(false)
const [newPathData, setNewPathData] = useState({ const [newPathData, setNewPathData] = useState({
@ -191,40 +194,60 @@ export default function ConfigPage() {
} }
} }
const handlePathDialogOpenChange = (open: boolean) => { // 处理对话框打开和关闭时的滚动位置
setPathDialogOpen(open) const handleDialogOpenChange = useCallback((open: boolean, handler: (open: boolean) => void) => {
if (!open) { if (open) {
setEditingPathData(null) // 对话框打开时,保存当前滚动位置
setNewPathData({ scrollPositionRef.current = window.scrollY
path: "", } else {
defaultTarget: "", // 对话框关闭时,恢复滚动位置
extensionMap: {}, handler(open)
sizeThreshold: 0, requestAnimationFrame(() => {
sizeUnit: 'MB', window.scrollTo(0, scrollPositionRef.current)
}) })
} }
} }, [])
const handleFixedPathDialogOpenChange = (open: boolean) => { const handlePathDialogOpenChange = useCallback((open: boolean) => {
setFixedPathDialogOpen(open) handleDialogOpenChange(open, (isOpen) => {
if (!open) { setPathDialogOpen(isOpen)
setEditingFixedPath(null) if (!isOpen) {
setNewFixedPath({ setEditingPathData(null)
Path: "", setNewPathData({
TargetHost: "", path: "",
TargetURL: "", defaultTarget: "",
}) extensionMap: {},
} sizeThreshold: 0,
} sizeUnit: 'MB',
})
}
})
}, [handleDialogOpenChange])
const handleExtensionMapDialogOpenChange = (open: boolean) => { const handleFixedPathDialogOpenChange = useCallback((open: boolean) => {
setExtensionMapDialogOpen(open) handleDialogOpenChange(open, (isOpen) => {
if (!open) { setFixedPathDialogOpen(isOpen)
setEditingPath(null) if (!isOpen) {
setEditingExtension(null) setEditingFixedPath(null)
setNewExtension({ ext: "", target: "" }) setNewFixedPath({
} Path: "",
} TargetHost: "",
TargetURL: "",
})
}
})
}, [handleDialogOpenChange])
const handleExtensionMapDialogOpenChange = useCallback((open: boolean) => {
handleDialogOpenChange(open, (isOpen) => {
setExtensionMapDialogOpen(isOpen)
if (!isOpen) {
setEditingPath(null)
setEditingExtension(null)
setNewExtension({ ext: "", target: "" })
}
})
}, [handleDialogOpenChange])
const addOrUpdatePath = () => { const addOrUpdatePath = () => {
if (!config) return if (!config) return
@ -663,6 +686,18 @@ export default function ConfigPage() {
setPathDialogOpen(true) setPathDialogOpen(true)
} }
// 处理删除对话框的滚动位置
const handleDeleteDialogOpenChange = useCallback((open: boolean, setter: (value: null) => void) => {
if (open) {
scrollPositionRef.current = window.scrollY
} else {
setter(null)
requestAnimationFrame(() => {
window.scrollTo(0, scrollPositionRef.current)
})
}
}, [])
if (loading) { if (loading) {
return ( return (
<div className="flex h-[calc(100vh-4rem)] items-center justify-center"> <div className="flex h-[calc(100vh-4rem)] items-center justify-center">
@ -1090,7 +1125,10 @@ export default function ConfigPage() {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<AlertDialog open={!!deletingPath} onOpenChange={(open) => !open && setDeletingPath(null)}> <AlertDialog
open={!!deletingPath}
onOpenChange={(open) => handleDeleteDialogOpenChange(open, setDeletingPath)}
>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle> <AlertDialogTitle></AlertDialogTitle>
@ -1105,7 +1143,10 @@ export default function ConfigPage() {
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
<AlertDialog open={!!deletingFixedPath} onOpenChange={(open) => !open && setDeletingFixedPath(null)}> <AlertDialog
open={!!deletingFixedPath}
onOpenChange={(open) => handleDeleteDialogOpenChange(open, setDeletingFixedPath)}
>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle> <AlertDialogTitle></AlertDialogTitle>
@ -1120,7 +1161,10 @@ export default function ConfigPage() {
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
<AlertDialog open={!!deletingExtension} onOpenChange={(open) => !open && setDeletingExtension(null)}> <AlertDialog
open={!!deletingExtension}
onOpenChange={(open) => handleDeleteDialogOpenChange(open, setDeletingExtension)}
>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle> <AlertDialogTitle></AlertDialogTitle>

View File

@ -148,14 +148,6 @@ export default function DashboardPage() {
<div className="text-sm font-medium text-gray-500"></div> <div className="text-sm font-medium text-gray-500"></div>
<div className="text-lg font-semibold">{metrics.active_requests}</div> <div className="text-lg font-semibold">{metrics.active_requests}</div>
</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> <div>
<div className="text-sm font-medium text-gray-500"></div> <div className="text-sm font-medium text-gray-500"></div>
<div className="text-lg font-semibold">{formatBytes(metrics.total_bytes)}</div> <div className="text-lg font-semibold">{formatBytes(metrics.total_bytes)}</div>
@ -187,7 +179,7 @@ export default function DashboardPage() {
<div className="text-lg font-semibold">{metrics.avg_response_time}</div> <div className="text-lg font-semibold">{metrics.avg_response_time}</div>
</div> </div>
<div> <div>
<div className="text-sm font-medium text-gray-500"></div> <div className="text-sm font-medium text-gray-500"></div>
<div className="text-lg font-semibold"> <div className="text-lg font-semibold">
{metrics.requests_per_second.toFixed(2)} {metrics.requests_per_second.toFixed(2)}
</div> </div>
@ -205,17 +197,36 @@ export default function DashboardPage() {
<div className="grid grid-cols-2 md:grid-cols-5 gap-4"> <div className="grid grid-cols-2 md:grid-cols-5 gap-4">
{Object.entries(metrics.status_code_stats || {}) {Object.entries(metrics.status_code_stats || {})
.sort((a, b) => a[0].localeCompare(b[0])) .sort((a, b) => a[0].localeCompare(b[0]))
.map(([status, count]) => ( .map(([status, count]) => {
<div const statusNum = parseInt(status);
key={status} let colorClass = "text-green-600";
className="p-4 rounded-lg border bg-card text-card-foreground shadow-sm" if (statusNum >= 500) {
> colorClass = "text-red-600";
<div className="text-sm font-medium text-gray-500"> } else if (statusNum >= 400) {
{status} colorClass = "text-yellow-600";
} else if (statusNum >= 300) {
colorClass = "text-blue-600";
}
// 计算总请求数
const totalRequests = Object.values(metrics.status_code_stats || {}).reduce((a, b) => a + (b as number), 0);
return (
<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 ${colorClass}`}>{count}</div>
<div className="text-sm text-gray-500 mt-1">
{totalRequests ?
((count as number / totalRequests) * 100).toFixed(1) : 0}%
</div>
</div> </div>
<div className="text-lg font-semibold">{count}</div> );
</div> })}
))}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>