From b6b77b03ed99d6b2636c9e18fda37ef1c510e8c1 Mon Sep 17 00:00:00 2001 From: wood chen Date: Sun, 9 Mar 2025 11:24:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(metrics):=20=E5=A2=9E=E5=BC=BA=E6=8C=87?= =?UTF-8?q?=E6=A0=87=E5=B1=95=E7=A4=BA=E5=92=8C=E7=BB=9F=E8=AE=A1=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在指标结构中新增延迟统计、错误统计和引用来源统计字段 - 更新前端仪表盘,添加延迟、带宽、错误和引用来源统计卡片 - 优化指标收集器,支持引用来源和错误类型统计 - 在工具函数中新增字符串转整数解析方法 - 简化引用来源URL处理,提取域名信息 --- internal/handler/metrics.go | 108 +++++++++++++-- internal/metrics/collector.go | 83 ++++++++++++ internal/utils/utils.go | 10 ++ web/app/dashboard/page.tsx | 240 ++++++++++++++++++++++++++++++---- 4 files changed, 404 insertions(+), 37 deletions(-) diff --git a/internal/handler/metrics.go b/internal/handler/metrics.go index cbf3c55..cdffe9b 100644 --- a/internal/handler/metrics.go +++ b/internal/handler/metrics.go @@ -11,7 +11,7 @@ import ( "time" ) -// Metrics 定义指标结构 +// Metrics 定义指标结构,与前端期望的数据结构保持一致 type Metrics struct { // 基础指标 Uptime string `json:"uptime"` @@ -28,13 +28,39 @@ type Metrics struct { AverageResponseTime string `json:"avg_response_time"` RequestsPerSecond float64 `json:"requests_per_second"` - // 新增字段 - TotalBytes int64 `json:"total_bytes"` - BytesPerSecond float64 `json:"bytes_per_second"` - StatusCodeStats map[string]int64 `json:"status_code_stats"` - TopPaths []models.PathMetricsJSON `json:"top_paths"` - RecentRequests []models.RequestLog `json:"recent_requests"` - TopReferers []models.PathMetricsJSON `json:"top_referers"` + // 传输指标 + TotalBytes int64 `json:"total_bytes"` + BytesPerSecond float64 `json:"bytes_per_second"` + + // 状态码统计 + StatusCodeStats map[string]int64 `json:"status_code_stats"` + + // 路径统计 + TopPaths []models.PathMetricsJSON `json:"top_paths"` + + // 最近请求 + RecentRequests []models.RequestLog `json:"recent_requests"` + + // 引用来源统计 + TopReferers []models.PathMetricsJSON `json:"top_referers"` + + // 延迟统计 + LatencyStats struct { + Min string `json:"min"` + Max string `json:"max"` + Distribution map[string]int64 `json:"distribution"` + } `json:"latency_stats"` + + // 错误统计 + ErrorStats struct { + ClientErrors int64 `json:"client_errors"` + ServerErrors int64 `json:"server_errors"` + Types map[string]int64 `json:"types"` + } `json:"error_stats"` + + // 带宽统计 + BandwidthHistory map[string]string `json:"bandwidth_history"` + CurrentBandwidth string `json:"current_bandwidth"` } // MetricsHandler 处理指标请求 @@ -57,10 +83,16 @@ func (h *ProxyHandler) MetricsHandler(w http.ResponseWriter, r *http.Request) { "bytes_per_second": float64(0), "requests_per_second": float64(0), "status_code_stats": make(map[string]int64), - "latency_percentiles": make([]float64, 0), "top_paths": make([]models.PathMetrics, 0), "recent_requests": make([]models.RequestLog, 0), "top_referers": make([]models.PathMetrics, 0), + "latency_stats": map[string]interface{}{ + "min": "0ms", + "max": "0ms", + "distribution": make(map[string]int64), + }, + "bandwidth_history": make(map[string]string), + "current_bandwidth": "0 B/s", } } @@ -69,6 +101,41 @@ func (h *ProxyHandler) MetricsHandler(w http.ResponseWriter, r *http.Request) { totalBytes := utils.SafeInt64(stats["total_bytes"]) uptimeSeconds := uptime.Seconds() + // 处理延迟统计数据 + latencyStats := make(map[string]interface{}) + if stats["latency_stats"] != nil { + latencyStats = stats["latency_stats"].(map[string]interface{}) + } + + // 处理带宽历史数据 + bandwidthHistory := make(map[string]string) + if stats["bandwidth_history"] != nil { + for k, v := range stats["bandwidth_history"].(map[string]string) { + bandwidthHistory[k] = v + } + } + + // 计算客户端错误和服务器错误数量 + var clientErrors, serverErrors int64 + statusCodeStats := models.SafeStatusCodeStats(stats["status_code_stats"]) + for code, count := range statusCodeStats { + codeInt := utils.ParseInt(code, 0) + if codeInt >= 400 && codeInt < 500 { + clientErrors += count + } else if codeInt >= 500 { + serverErrors += count + } + } + + // 创建错误类型统计 + errorTypes := make(map[string]int64) + if clientErrors > 0 { + errorTypes["客户端错误"] = clientErrors + } + if serverErrors > 0 { + errorTypes["服务器错误"] = serverErrors + } + metrics := Metrics{ Uptime: metrics.FormatUptime(uptime), ActiveRequests: utils.SafeInt64(stats["active_requests"]), @@ -81,12 +148,33 @@ func (h *ProxyHandler) MetricsHandler(w http.ResponseWriter, r *http.Request) { TotalBytes: totalBytes, BytesPerSecond: float64(totalBytes) / utils.MaxFloat64(uptimeSeconds, 1), RequestsPerSecond: float64(totalRequests) / utils.MaxFloat64(uptimeSeconds, 1), - StatusCodeStats: models.SafeStatusCodeStats(stats["status_code_stats"]), + StatusCodeStats: statusCodeStats, TopPaths: models.SafePathMetrics(stats["top_paths"]), RecentRequests: models.SafeRequestLogs(stats["recent_requests"]), TopReferers: models.SafePathMetrics(stats["top_referers"]), + BandwidthHistory: bandwidthHistory, + CurrentBandwidth: utils.SafeString(stats["current_bandwidth"], "0 B/s"), } + // 填充延迟统计数据 + metrics.LatencyStats.Min = utils.SafeString(latencyStats["min"], "0ms") + metrics.LatencyStats.Max = utils.SafeString(latencyStats["max"], "0ms") + + // 处理分布数据 + if distribution, ok := latencyStats["distribution"].(map[string]interface{}); ok { + metrics.LatencyStats.Distribution = make(map[string]int64) + for k, v := range distribution { + if intValue, ok := v.(float64); ok { + metrics.LatencyStats.Distribution[k] = int64(intValue) + } + } + } + + // 填充错误统计数据 + metrics.ErrorStats.ClientErrors = clientErrors + metrics.ErrorStats.ServerErrors = serverErrors + metrics.ErrorStats.Types = errorTypes + w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(metrics); err != nil { log.Printf("Error encoding metrics: %v", err) diff --git a/internal/metrics/collector.go b/internal/metrics/collector.go index e09dc24..27fcf10 100644 --- a/internal/metrics/collector.go +++ b/internal/metrics/collector.go @@ -10,6 +10,7 @@ import ( "proxy-go/internal/utils" "runtime" "sort" + "strings" "sync" "sync/atomic" "time" @@ -26,6 +27,7 @@ type Collector struct { pathStats sync.Map statusCodeStats sync.Map latencyBuckets sync.Map // 响应时间分布 + refererStats sync.Map // 引用来源统计 bandwidthStats struct { sync.RWMutex window time.Duration @@ -171,6 +173,36 @@ func (c *Collector) RecordRequest(path string, status int, latency time.Duration } c.pathStatsMutex.Unlock() + // 更新引用来源统计 + if r != nil { + referer := r.Header.Get("Referer") + if referer != "" { + // 简化引用来源,只保留域名部分 + referer = simplifyReferer(referer) + + if value, ok := c.refererStats.Load(referer); ok { + stat := value.(*models.PathMetrics) + stat.AddRequest() + if status >= 400 { + stat.AddError() + } + stat.AddLatency(int64(latency)) + stat.AddBytes(bytes) + } else { + newStat := &models.PathMetrics{ + Path: referer, + } + newStat.RequestCount.Store(1) + if status >= 400 { + newStat.ErrorCount.Store(1) + } + newStat.TotalLatency.Store(int64(latency)) + newStat.BytesTransferred.Store(bytes) + c.refererStats.Store(referer, newStat) + } + } + } + // 更新最近请求记录 c.recentRequests.Push(models.RequestLog{ Time: time.Now(), @@ -275,6 +307,41 @@ func (c *Collector) GetStats() map[string]interface{} { pathMetricsValues[i] = metric.ToJSON() } + // 收集引用来源统计 + var refererMetrics []*models.PathMetrics + c.refererStats.Range(func(key, value interface{}) bool { + stats := value.(*models.PathMetrics) + requestCount := stats.GetRequestCount() + if requestCount > 0 { + totalLatency := stats.GetTotalLatency() + avgLatencyMs := float64(totalLatency) / float64(requestCount) / float64(time.Millisecond) + stats.AvgLatency = fmt.Sprintf("%.2fms", avgLatencyMs) + refererMetrics = append(refererMetrics, stats) + } + return true + }) + + // 按请求数降序排序,请求数相同时按引用来源字典序排序 + sort.Slice(refererMetrics, func(i, j int) bool { + countI := refererMetrics[i].GetRequestCount() + countJ := refererMetrics[j].GetRequestCount() + if countI != countJ { + return countI > countJ + } + return refererMetrics[i].Path < refererMetrics[j].Path + }) + + // 只保留前10个 + if len(refererMetrics) > 10 { + refererMetrics = refererMetrics[:10] + } + + // 转换为值切片 + refererMetricsValues := make([]models.PathMetricsJSON, len(refererMetrics)) + for i, metric := range refererMetrics { + refererMetricsValues[i] = metric.ToJSON() + } + // 收集延迟分布 latencyDistribution := make(map[string]int64) c.latencyBuckets.Range(func(key, value interface{}) bool { @@ -310,6 +377,7 @@ func (c *Collector) GetStats() map[string]interface{} { "bytes_per_second": float64(atomic.LoadInt64(&c.totalBytes)) / totalRuntime.Seconds(), "status_code_stats": statusCodeStats, "top_paths": pathMetricsValues, + "top_referers": refererMetricsValues, "recent_requests": recentRequests, "latency_stats": map[string]interface{}{ "min": fmt.Sprintf("%.2fms", float64(minLatency)/float64(time.Millisecond)), @@ -476,3 +544,18 @@ func (c *Collector) getBandwidthHistory() map[string]string { } return history } + +// simplifyReferer 简化引用来源URL,只保留域名部分 +func simplifyReferer(referer string) string { + // 移除协议部分 + if idx := strings.Index(referer, "://"); idx != -1 { + referer = referer[idx+3:] + } + + // 只保留域名部分 + if idx := strings.Index(referer, "/"); idx != -1 { + referer = referer[:idx] + } + + return referer +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 55a7181..5725c7a 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -358,3 +358,13 @@ func MaxFloat64(a, b float64) float64 { } return b } + +// ParseInt 将字符串解析为整数,如果解析失败则返回默认值 +func ParseInt(s string, defaultValue int) int { + var result int + _, err := fmt.Sscanf(s, "%d", &result) + if err != nil { + return defaultValue + } + return result +} diff --git a/web/app/dashboard/page.tsx b/web/app/dashboard/page.tsx index 74dbd61..c0babf7 100644 --- a/web/app/dashboard/page.tsx +++ b/web/app/dashboard/page.tsx @@ -15,6 +15,7 @@ interface Metrics { avg_response_time: string requests_per_second: number bytes_per_second: number + error_rate: number status_code_stats: Record top_paths: Array<{ path: string @@ -44,6 +45,13 @@ interface Metrics { bandwidth_history: Record current_bandwidth: string total_bytes: number + top_referers: Array<{ + path: string + request_count: number + error_count: number + avg_latency: string + bytes_transferred: number + }> } export default function DashboardPage() { @@ -221,7 +229,7 @@ export default function DashboardPage() {
{count}
- {totalRequests ? + {totalRequests ? ((count as number / totalRequests) * 100).toFixed(1) : 0}%
@@ -231,6 +239,183 @@ export default function DashboardPage() { + + {/* 新增:延迟统计卡片 */} +
+ + + 延迟统计 + + +
+
+
+
最小响应时间
+
{metrics.latency_stats?.min || "0ms"}
+
+
+
最大响应时间
+
{metrics.latency_stats?.max || "0ms"}
+
+
+ +
+
响应时间分布
+
+ {metrics.latency_stats?.distribution && + Object.entries(metrics.latency_stats.distribution) + .sort((a, b) => { + // 按照延迟范围排序 + const order = ["<10ms", "10-50ms", "50-200ms", "200-1000ms", ">1s"]; + return order.indexOf(a[0]) - order.indexOf(b[0]); + }) + .map(([range, count]) => ( +
+
{range}
+
{count}
+
+ {Object.values(metrics.latency_stats?.distribution || {}).reduce((sum, val) => sum + val, 0) > 0 + ? ((count / Object.values(metrics.latency_stats?.distribution || {}).reduce((sum, val) => sum + val, 0)) * 100).toFixed(1) + : 0}% +
+
+ )) + } +
+
+
+
+
+ + + + 带宽统计 + + +
+
+
当前带宽
+
{metrics.current_bandwidth || "0 B/s"}
+
+ +
+
带宽历史
+
+ {metrics.bandwidth_history && + Object.entries(metrics.bandwidth_history) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([time, bandwidth]) => ( +
+
{time}
+
{bandwidth}
+
+ )) + } +
+
+
+
+
+
+ + {/* 错误统计卡片 */} + + + 错误统计 + + +
+
+
客户端错误 (4xx)
+
+ {metrics.error_stats?.client_errors || 0} +
+
+ 占总请求的 {metrics.total_requests ? + ((metrics.error_stats?.client_errors || 0) / metrics.total_requests * 100).toFixed(2) : 0}% +
+
+ +
+
服务器错误 (5xx)
+
+ {metrics.error_stats?.server_errors || 0} +
+
+ 占总请求的 {metrics.total_requests ? + ((metrics.error_stats?.server_errors || 0) / metrics.total_requests * 100).toFixed(2) : 0}% +
+
+ +
+
总错误率
+
+ {(metrics.error_rate * 100).toFixed(2)}% +
+
+ 总错误数: {metrics.total_errors || 0} +
+
+
+ + {metrics.error_stats?.types && Object.keys(metrics.error_stats.types).length > 0 && ( +
+
错误类型分布
+
+ {Object.entries(metrics.error_stats.types).map(([type, count]) => ( +
+
{type}
+
{count}
+
+ {metrics.total_errors ? ((count / metrics.total_errors) * 100).toFixed(1) : 0}% +
+
+ ))} +
+
+ )} +
+
+ + {/* 引用来源统计卡片 */} + {metrics.top_referers && metrics.top_referers.length > 0 && ( + + + 引用来源统计 (Top {metrics.top_referers.length}) + + +
+ + + + + + + + + + + + {metrics.top_referers.map((referer, index) => ( + + + + + + + + ))} + +
来源域名请求数错误数平均延迟传输大小
+ + {referer.path} + + {referer.request_count}{referer.error_count}{referer.avg_latency}{formatBytes(referer.bytes_transferred)}
+
+
+
+ )} + 热门路径 (Top 10) @@ -293,37 +478,38 @@ export default function DashboardPage() { {(metrics.recent_requests || []) .slice(0, 20) // 只显示最近20条记录 .map((req, index) => ( - - {formatDate(req.Time)} - - - {req.Path} - - - - - {req.Status} - - - {formatLatency(req.Latency)} - {formatBytes(req.BytesSent)} - {req.ClientIP} - - ))} + + {formatDate(req.Time)} + + + {req.Path} + + + + + {req.Status} + + + {formatLatency(req.Latency)} + {formatBytes(req.BytesSent)} + {req.ClientIP} + + ))} + ) }