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
@ -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
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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 请求队列
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user