mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-18 08:31:55 +08:00
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:
parent
ff24191146
commit
d0d752712e
@ -19,33 +19,23 @@ import (
|
||||
type Collector struct {
|
||||
startTime time.Time
|
||||
activeRequests int64
|
||||
totalRequests int64
|
||||
totalErrors int64
|
||||
totalBytes int64
|
||||
latencySum int64
|
||||
maxLatency int64 // 最大响应时间
|
||||
minLatency int64 // 最小响应时间
|
||||
clientErrors int64 // 4xx错误
|
||||
serverErrors int64 // 5xx错误
|
||||
pathStats sync.Map
|
||||
statusCodeStats sync.Map
|
||||
latencyBuckets sync.Map // 响应时间分布
|
||||
bandwidthStats sync.Map // 带宽统计
|
||||
errorTypes sync.Map // 错误类型统计
|
||||
recentRequests []models.RequestLog
|
||||
recentRequestsMutex sync.RWMutex
|
||||
bandwidthStats struct {
|
||||
sync.RWMutex
|
||||
window time.Duration
|
||||
lastUpdate time.Time
|
||||
current int64
|
||||
history map[string]int64
|
||||
}
|
||||
recentRequests *models.RequestQueue
|
||||
pathStatsMutex sync.RWMutex
|
||||
config *config.Config
|
||||
lastMinute time.Time // 用于计算每分钟带宽
|
||||
minuteBytes int64 // 当前分钟的字节数
|
||||
|
||||
// 新增:时间段统计
|
||||
lastStatsTime time.Time
|
||||
intervalRequests int64
|
||||
intervalErrors int64
|
||||
intervalBytes int64
|
||||
intervalLatencySum int64
|
||||
intervalStatusCodes sync.Map
|
||||
}
|
||||
|
||||
var (
|
||||
@ -58,19 +48,25 @@ func InitCollector(cfg *config.Config) error {
|
||||
once.Do(func() {
|
||||
instance = &Collector{
|
||||
startTime: time.Now(),
|
||||
lastMinute: time.Now(),
|
||||
lastStatsTime: time.Now(),
|
||||
recentRequests: make([]models.RequestLog, 0, 1000),
|
||||
recentRequests: models.NewRequestQueue(100),
|
||||
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("10-50ms", new(int64))
|
||||
instance.latencyBuckets.Store("50-200ms", new(int64))
|
||||
instance.latencyBuckets.Store("200-1000ms", new(int64))
|
||||
instance.latencyBuckets.Store(">1s", new(int64))
|
||||
|
||||
// 启动数据一致性检查器
|
||||
instance.startConsistencyChecker()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@ -92,41 +88,22 @@ func (c *Collector) EndRequest() {
|
||||
|
||||
// RecordRequest 记录请求
|
||||
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)
|
||||
if counter, ok := c.intervalStatusCodes.Load(statusKey); ok {
|
||||
if counter, ok := c.statusCodeStats.Load(statusKey); ok {
|
||||
atomic.AddInt64(counter.(*int64), 1)
|
||||
} else {
|
||||
counter := new(int64)
|
||||
*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)
|
||||
for {
|
||||
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)
|
||||
}
|
||||
|
||||
// 更新错误统计
|
||||
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()
|
||||
if value, ok := c.pathStats.Load(path); ok {
|
||||
stat := value.(*models.PathMetrics)
|
||||
atomic.AddInt64(&stat.RequestCount, 1)
|
||||
stat.AddRequest()
|
||||
if status >= 400 {
|
||||
atomic.AddInt64(&stat.ErrorCount, 1)
|
||||
stat.AddError()
|
||||
}
|
||||
atomic.AddInt64(&stat.TotalLatency, int64(latency))
|
||||
atomic.AddInt64(&stat.BytesTransferred, bytes)
|
||||
stat.AddLatency(int64(latency))
|
||||
stat.AddBytes(bytes)
|
||||
} else {
|
||||
c.pathStats.Store(path, &models.PathMetrics{
|
||||
newStat := &models.PathMetrics{
|
||||
Path: path,
|
||||
RequestCount: 1,
|
||||
ErrorCount: map[bool]int64{true: 1, false: 0}[status >= 400],
|
||||
TotalLatency: int64(latency),
|
||||
BytesTransferred: bytes,
|
||||
})
|
||||
}
|
||||
newStat.AddRequest()
|
||||
if status >= 400 {
|
||||
newStat.AddError()
|
||||
}
|
||||
newStat.AddLatency(int64(latency))
|
||||
newStat.AddBytes(bytes)
|
||||
c.pathStats.Store(path, newStat)
|
||||
}
|
||||
c.pathStatsMutex.Unlock()
|
||||
|
||||
// 更新最近请求记录
|
||||
c.recentRequestsMutex.Lock()
|
||||
c.recentRequests = append([]models.RequestLog{{
|
||||
c.recentRequests.Push(models.RequestLog{
|
||||
Time: time.Now(),
|
||||
Path: path,
|
||||
Status: status,
|
||||
Latency: int64(latency),
|
||||
BytesSent: bytes,
|
||||
ClientIP: clientIP,
|
||||
}}, c.recentRequests...)
|
||||
if len(c.recentRequests) > 100 { // 只保留最近100条记录
|
||||
c.recentRequests = c.recentRequests[:100]
|
||||
}
|
||||
c.recentRequestsMutex.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
// FormatUptime 格式化运行时间
|
||||
@ -267,54 +203,58 @@ func FormatUptime(d time.Duration) string {
|
||||
|
||||
// GetStats 获取统计数据
|
||||
func (c *Collector) GetStats() map[string]interface{} {
|
||||
// 获取统计数据
|
||||
var mem runtime.MemStats
|
||||
runtime.ReadMemStats(&mem)
|
||||
|
||||
now := time.Now()
|
||||
interval := now.Sub(c.lastStatsTime)
|
||||
c.lastStatsTime = now
|
||||
totalRuntime := now.Sub(c.startTime)
|
||||
|
||||
// 获取并重置时间段统计
|
||||
intervalRequests := atomic.SwapInt64(&c.intervalRequests, 0)
|
||||
intervalBytes := atomic.SwapInt64(&c.intervalBytes, 0)
|
||||
intervalLatencySum := atomic.SwapInt64(&c.intervalLatencySum, 0)
|
||||
intervalErrors := atomic.SwapInt64(&c.intervalErrors, 0)
|
||||
|
||||
// 计算时间段平均延迟
|
||||
avgLatency := float64(0)
|
||||
if intervalRequests > 0 {
|
||||
avgLatency = float64(intervalLatencySum) / float64(intervalRequests)
|
||||
// 计算总请求数和平均延迟
|
||||
var totalRequests int64
|
||||
c.statusCodeStats.Range(func(key, value interface{}) bool {
|
||||
if counter, ok := value.(*int64); ok {
|
||||
totalRequests += atomic.LoadInt64(counter)
|
||||
}
|
||||
|
||||
// 收集并重置时间段状态码统计
|
||||
intervalStatusStats := make(map[string]int64)
|
||||
c.intervalStatusCodes.Range(func(key, value interface{}) bool {
|
||||
intervalStatusStats[key.(string)] = atomic.SwapInt64(value.(*int64), 0)
|
||||
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)
|
||||
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
|
||||
})
|
||||
|
||||
// 收集路径统计
|
||||
var pathMetrics []models.PathMetrics
|
||||
var pathMetrics []*models.PathMetrics
|
||||
c.pathStats.Range(func(key, value interface{}) bool {
|
||||
stats := value.(*models.PathMetrics)
|
||||
if stats.RequestCount > 0 {
|
||||
avgLatencyMs := float64(stats.TotalLatency) / float64(stats.RequestCount) / float64(time.Millisecond)
|
||||
requestCount := stats.GetRequestCount()
|
||||
if requestCount > 0 {
|
||||
totalLatency := stats.GetTotalLatency()
|
||||
avgLatencyMs := float64(totalLatency) / float64(requestCount) / float64(time.Millisecond)
|
||||
stats.AvgLatency = fmt.Sprintf("%.2fms", avgLatencyMs)
|
||||
}
|
||||
pathMetrics = append(pathMetrics, *stats)
|
||||
pathMetrics = append(pathMetrics, stats)
|
||||
return true
|
||||
})
|
||||
|
||||
// 按请求数降序排序
|
||||
sort.Slice(pathMetrics, func(i, j int) bool {
|
||||
return pathMetrics[i].RequestCount > pathMetrics[j].RequestCount
|
||||
return pathMetrics[i].GetRequestCount() > pathMetrics[j].GetRequestCount()
|
||||
})
|
||||
|
||||
// 只保留前10个
|
||||
@ -333,37 +273,8 @@ func (c *Collector) GetStats() map[string]interface{} {
|
||||
return true
|
||||
})
|
||||
|
||||
// 收集错误类型统计
|
||||
errorTypeStats := make(map[string]int64)
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
// 获取最近请求记录(使用读锁)
|
||||
recentRequests := c.recentRequests.GetAll()
|
||||
|
||||
// 获取最小和最大响应时间
|
||||
minLatency := atomic.LoadInt64(&c.minLatency)
|
||||
@ -372,25 +283,19 @@ func (c *Collector) GetStats() map[string]interface{} {
|
||||
minLatency = 0
|
||||
}
|
||||
|
||||
// 获取最近请求记录(加锁)
|
||||
c.recentRequestsMutex.RLock()
|
||||
recentRequests := make([]models.RequestLog, len(c.recentRequests))
|
||||
copy(recentRequests, c.recentRequests)
|
||||
c.recentRequestsMutex.RUnlock()
|
||||
// 收集带宽历史记录
|
||||
bandwidthHistory := c.getBandwidthHistory()
|
||||
|
||||
return map[string]interface{}{
|
||||
"uptime": FormatUptime(time.Since(c.startTime)),
|
||||
"uptime": FormatUptime(totalRuntime),
|
||||
"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),
|
||||
"num_goroutine": runtime.NumGoroutine(),
|
||||
"memory_usage": utils.FormatBytes(int64(mem.Alloc)),
|
||||
"avg_response_time": fmt.Sprintf("%.2fms", avgLatency/float64(time.Millisecond)),
|
||||
"requests_per_second": float64(intervalRequests) / interval.Seconds(),
|
||||
"bytes_per_second": float64(intervalBytes) / interval.Seconds(),
|
||||
"status_code_stats": intervalStatusStats,
|
||||
"requests_per_second": requestsPerSecond,
|
||||
"bytes_per_second": float64(atomic.LoadInt64(&c.totalBytes)) / totalRuntime.Seconds(),
|
||||
"status_code_stats": statusCodeStats,
|
||||
"top_paths": pathMetrics,
|
||||
"recent_requests": recentRequests,
|
||||
"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)),
|
||||
"distribution": latencyDistribution,
|
||||
},
|
||||
"error_stats": map[string]interface{}{
|
||||
"client_errors": atomic.LoadInt64(&c.clientErrors),
|
||||
"server_errors": atomic.LoadInt64(&c.serverErrors),
|
||||
"types": errorTypeStats,
|
||||
},
|
||||
"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 验证当前数据的有效性
|
||||
func (c *Collector) validateLoadedData() error {
|
||||
// 验证基础指标
|
||||
if c.totalRequests < 0 ||
|
||||
c.totalErrors < 0 ||
|
||||
c.totalBytes < 0 {
|
||||
return fmt.Errorf("invalid stats values")
|
||||
}
|
||||
|
||||
// 验证错误数不能大于总请求数
|
||||
if c.totalErrors > c.totalRequests {
|
||||
return fmt.Errorf("total errors exceeds total requests")
|
||||
if c.totalBytes < 0 ||
|
||||
c.activeRequests < 0 {
|
||||
return fmt.Errorf("invalid negative stats values")
|
||||
}
|
||||
|
||||
// 验证状态码统计
|
||||
var statusCodeTotal int64
|
||||
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
|
||||
c.pathStats.Range(func(_, value interface{}) bool {
|
||||
stats := value.(*models.PathMetrics)
|
||||
if stats.RequestCount < 0 || stats.ErrorCount < 0 {
|
||||
requestCount := stats.GetRequestCount()
|
||||
errorCount := stats.GetErrorCount()
|
||||
if requestCount < 0 || errorCount < 0 {
|
||||
return false
|
||||
}
|
||||
totalPathRequests += stats.RequestCount
|
||||
if errorCount > requestCount {
|
||||
return false
|
||||
}
|
||||
totalPathRequests += requestCount
|
||||
return true
|
||||
})
|
||||
|
||||
// 验证总数一致性
|
||||
if totalPathRequests > c.totalRequests {
|
||||
return fmt.Errorf("path stats total exceeds total requests")
|
||||
if totalPathRequests != statusCodeTotal {
|
||||
return fmt.Errorf("path stats total (%d) does not match status code total (%d)",
|
||||
totalPathRequests, statusCodeTotal)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -474,8 +379,81 @@ func (c *Collector) GetLastSaveTime() time.Time {
|
||||
// CheckDataConsistency 实现 interfaces.MetricsCollector 接口
|
||||
func (c *Collector) CheckDataConsistency() error {
|
||||
// 简单的数据验证
|
||||
if c.totalErrors > c.totalRequests {
|
||||
return fmt.Errorf("total errors exceeds total requests")
|
||||
if err := c.validateLoadedData(); err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -11,16 +11,56 @@ type PathStats struct {
|
||||
LatencySum atomic.Int64
|
||||
}
|
||||
|
||||
// PathMetrics 路径指标
|
||||
// PathMetrics 路径统计信息
|
||||
type PathMetrics struct {
|
||||
Path string `json:"path"`
|
||||
RequestCount int64 `json:"request_count"`
|
||||
ErrorCount int64 `json:"error_count"`
|
||||
TotalLatency int64 `json:"total_latency"`
|
||||
BytesTransferred int64 `json:"bytes_transferred"`
|
||||
RequestCount atomic.Int64 `json:"request_count"`
|
||||
ErrorCount atomic.Int64 `json:"error_count"`
|
||||
TotalLatency atomic.Int64 `json:"-"`
|
||||
BytesTransferred atomic.Int64 `json:"bytes_transferred"`
|
||||
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 {
|
||||
Timestamp string `json:"timestamp"`
|
||||
TotalRequests int64 `json:"total_requests"`
|
||||
|
@ -7,12 +7,12 @@ import (
|
||||
|
||||
// RequestLog 请求日志
|
||||
type RequestLog struct {
|
||||
Time time.Time `json:"Time"`
|
||||
Path string `json:"Path"`
|
||||
Status int `json:"Status"`
|
||||
Latency int64 `json:"Latency"`
|
||||
BytesSent int64 `json:"BytesSent"`
|
||||
ClientIP string `json:"ClientIP"`
|
||||
Time time.Time `json:"time"`
|
||||
Path string `json:"path"`
|
||||
Status int `json:"status"`
|
||||
Latency int64 `json:"latency"`
|
||||
BytesSent int64 `json:"bytes_sent"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
}
|
||||
|
||||
// RequestQueue 请求队列
|
||||
|
@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState, useCallback } from "react"
|
||||
import { useEffect, useState, useCallback, useRef } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
@ -70,6 +70,9 @@ export default function ConfigPage() {
|
||||
const { toast } = useToast()
|
||||
const router = useRouter()
|
||||
|
||||
// 使用 ref 来保存滚动位置
|
||||
const scrollPositionRef = useRef(0)
|
||||
|
||||
// 对话框状态
|
||||
const [pathDialogOpen, setPathDialogOpen] = useState(false)
|
||||
const [newPathData, setNewPathData] = useState({
|
||||
@ -191,9 +194,24 @@ export default function ConfigPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handlePathDialogOpenChange = (open: boolean) => {
|
||||
setPathDialogOpen(open)
|
||||
if (!open) {
|
||||
// 处理对话框打开和关闭时的滚动位置
|
||||
const handleDialogOpenChange = useCallback((open: boolean, handler: (open: boolean) => void) => {
|
||||
if (open) {
|
||||
// 对话框打开时,保存当前滚动位置
|
||||
scrollPositionRef.current = window.scrollY
|
||||
} else {
|
||||
// 对话框关闭时,恢复滚动位置
|
||||
handler(open)
|
||||
requestAnimationFrame(() => {
|
||||
window.scrollTo(0, scrollPositionRef.current)
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handlePathDialogOpenChange = useCallback((open: boolean) => {
|
||||
handleDialogOpenChange(open, (isOpen) => {
|
||||
setPathDialogOpen(isOpen)
|
||||
if (!isOpen) {
|
||||
setEditingPathData(null)
|
||||
setNewPathData({
|
||||
path: "",
|
||||
@ -203,11 +221,13 @@ export default function ConfigPage() {
|
||||
sizeUnit: 'MB',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [handleDialogOpenChange])
|
||||
|
||||
const handleFixedPathDialogOpenChange = (open: boolean) => {
|
||||
setFixedPathDialogOpen(open)
|
||||
if (!open) {
|
||||
const handleFixedPathDialogOpenChange = useCallback((open: boolean) => {
|
||||
handleDialogOpenChange(open, (isOpen) => {
|
||||
setFixedPathDialogOpen(isOpen)
|
||||
if (!isOpen) {
|
||||
setEditingFixedPath(null)
|
||||
setNewFixedPath({
|
||||
Path: "",
|
||||
@ -215,16 +235,19 @@ export default function ConfigPage() {
|
||||
TargetURL: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [handleDialogOpenChange])
|
||||
|
||||
const handleExtensionMapDialogOpenChange = (open: boolean) => {
|
||||
setExtensionMapDialogOpen(open)
|
||||
if (!open) {
|
||||
const handleExtensionMapDialogOpenChange = useCallback((open: boolean) => {
|
||||
handleDialogOpenChange(open, (isOpen) => {
|
||||
setExtensionMapDialogOpen(isOpen)
|
||||
if (!isOpen) {
|
||||
setEditingPath(null)
|
||||
setEditingExtension(null)
|
||||
setNewExtension({ ext: "", target: "" })
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [handleDialogOpenChange])
|
||||
|
||||
const addOrUpdatePath = () => {
|
||||
if (!config) return
|
||||
@ -663,6 +686,18 @@ export default function ConfigPage() {
|
||||
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) {
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)] items-center justify-center">
|
||||
@ -1090,7 +1125,10 @@ export default function ConfigPage() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog open={!!deletingPath} onOpenChange={(open) => !open && setDeletingPath(null)}>
|
||||
<AlertDialog
|
||||
open={!!deletingPath}
|
||||
onOpenChange={(open) => handleDeleteDialogOpenChange(open, setDeletingPath)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
@ -1105,7 +1143,10 @@ export default function ConfigPage() {
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog open={!!deletingFixedPath} onOpenChange={(open) => !open && setDeletingFixedPath(null)}>
|
||||
<AlertDialog
|
||||
open={!!deletingFixedPath}
|
||||
onOpenChange={(open) => handleDeleteDialogOpenChange(open, setDeletingFixedPath)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
@ -1120,7 +1161,10 @@ export default function ConfigPage() {
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog open={!!deletingExtension} onOpenChange={(open) => !open && setDeletingExtension(null)}>
|
||||
<AlertDialog
|
||||
open={!!deletingExtension}
|
||||
onOpenChange={(open) => handleDeleteDialogOpenChange(open, setDeletingExtension)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
|
@ -148,14 +148,6 @@ export default function DashboardPage() {
|
||||
<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>
|
||||
<div className="text-sm font-medium text-gray-500">总传输数据</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>
|
||||
<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">
|
||||
{metrics.requests_per_second.toFixed(2)}
|
||||
</div>
|
||||
@ -205,7 +197,21 @@ export default function DashboardPage() {
|
||||
<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]) => (
|
||||
.map(([status, count]) => {
|
||||
const statusNum = parseInt(status);
|
||||
let colorClass = "text-green-600";
|
||||
if (statusNum >= 500) {
|
||||
colorClass = "text-red-600";
|
||||
} else if (statusNum >= 400) {
|
||||
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"
|
||||
@ -213,9 +219,14 @@ export default function DashboardPage() {
|
||||
<div className="text-sm font-medium text-gray-500">
|
||||
状态码 {status}
|
||||
</div>
|
||||
<div className="text-lg font-semibold">{count}</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
Loading…
x
Reference in New Issue
Block a user