feat(metrics): 增强指标展示和统计功能

- 在指标结构中新增延迟统计、错误统计和引用来源统计字段
- 更新前端仪表盘,添加延迟、带宽、错误和引用来源统计卡片
- 优化指标收集器,支持引用来源和错误类型统计
- 在工具函数中新增字符串转整数解析方法
- 简化引用来源URL处理,提取域名信息
This commit is contained in:
wood chen 2025-03-09 11:24:46 +08:00
parent f0c806292b
commit b6b77b03ed
4 changed files with 404 additions and 37 deletions

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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<string, number>
top_paths: Array<{
path: string
@ -44,6 +45,13 @@ interface Metrics {
bandwidth_history: Record<string, string>
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() {
</div>
<div className={`text-lg font-semibold ${colorClass}`}>{count}</div>
<div className="text-sm text-gray-500 mt-1">
{totalRequests ?
{totalRequests ?
((count as number / totalRequests) * 100).toFixed(1) : 0}%
</div>
</div>
@ -231,6 +239,183 @@ export default function DashboardPage() {
</CardContent>
</Card>
{/* 新增:延迟统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-sm font-medium text-gray-500"></div>
<div className="text-lg font-semibold">{metrics.latency_stats?.min || "0ms"}</div>
</div>
<div>
<div className="text-sm font-medium text-gray-500"></div>
<div className="text-lg font-semibold">{metrics.latency_stats?.max || "0ms"}</div>
</div>
</div>
<div>
<div className="text-sm font-medium text-gray-500 mb-2"></div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{metrics.latency_stats?.distribution &&
Object.entries(metrics.latency_stats.distribution)
.sort((a, b) => {
// 按照延迟范围排序
const order = ["<10ms", "10-50ms", "50-200ms", "200-1000ms", ">1s"];
return order.indexOf(a[0]) - order.indexOf(b[0]);
})
.map(([range, count]) => (
<div key={range} className="p-3 rounded-lg border bg-card text-card-foreground shadow-sm">
<div className="text-sm font-medium text-gray-500">{range}</div>
<div className="text-lg font-semibold">{count}</div>
<div className="text-xs text-gray-500 mt-1">
{Object.values(metrics.latency_stats?.distribution || {}).reduce((sum, val) => sum + val, 0) > 0
? ((count / Object.values(metrics.latency_stats?.distribution || {}).reduce((sum, val) => sum + val, 0)) * 100).toFixed(1)
: 0}%
</div>
</div>
))
}
</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<div className="text-sm font-medium text-gray-500"></div>
<div className="text-lg font-semibold">{metrics.current_bandwidth || "0 B/s"}</div>
</div>
<div>
<div className="text-sm font-medium text-gray-500 mb-2"></div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{metrics.bandwidth_history &&
Object.entries(metrics.bandwidth_history)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([time, bandwidth]) => (
<div key={time} className="p-3 rounded-lg border bg-card text-card-foreground shadow-sm">
<div className="text-sm font-medium text-gray-500">{time}</div>
<div className="text-lg font-semibold">{bandwidth}</div>
</div>
))
}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* 错误统计卡片 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
<div className="text-sm font-medium text-gray-500"> (4xx)</div>
<div className="text-2xl font-semibold text-yellow-600">
{metrics.error_stats?.client_errors || 0}
</div>
<div className="text-sm text-gray-500">
{metrics.total_requests ?
((metrics.error_stats?.client_errors || 0) / metrics.total_requests * 100).toFixed(2) : 0}%
</div>
</div>
<div className="space-y-2">
<div className="text-sm font-medium text-gray-500"> (5xx)</div>
<div className="text-2xl font-semibold text-red-600">
{metrics.error_stats?.server_errors || 0}
</div>
<div className="text-sm text-gray-500">
{metrics.total_requests ?
((metrics.error_stats?.server_errors || 0) / metrics.total_requests * 100).toFixed(2) : 0}%
</div>
</div>
<div className="space-y-2">
<div className="text-sm font-medium text-gray-500"></div>
<div className="text-2xl font-semibold">
{(metrics.error_rate * 100).toFixed(2)}%
</div>
<div className="text-sm text-gray-500">
: {metrics.total_errors || 0}
</div>
</div>
</div>
{metrics.error_stats?.types && Object.keys(metrics.error_stats.types).length > 0 && (
<div className="mt-6">
<div className="text-sm font-medium text-gray-500 mb-2"></div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{Object.entries(metrics.error_stats.types).map(([type, count]) => (
<div key={type} className="p-3 rounded-lg border bg-card text-card-foreground shadow-sm">
<div className="text-sm font-medium text-gray-500">{type}</div>
<div className="text-lg font-semibold">{count}</div>
<div className="text-xs text-gray-500 mt-1">
{metrics.total_errors ? ((count / metrics.total_errors) * 100).toFixed(1) : 0}%
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
{/* 引用来源统计卡片 */}
{metrics.top_referers && metrics.top_referers.length > 0 && (
<Card>
<CardHeader>
<CardTitle> (Top {metrics.top_referers.length})</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b">
<th className="text-left p-2"></th>
<th className="text-left p-2"></th>
<th className="text-left p-2"></th>
<th className="text-left p-2"></th>
<th className="text-left p-2"></th>
</tr>
</thead>
<tbody>
{metrics.top_referers.map((referer, index) => (
<tr key={index} className="border-b">
<td className="p-2">
<span className="text-blue-600">
{referer.path}
</span>
</td>
<td className="p-2">{referer.request_count}</td>
<td className="p-2">{referer.error_count}</td>
<td className="p-2">{referer.avg_latency}</td>
<td className="p-2">{formatBytes(referer.bytes_transferred)}</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle> (Top 10)</CardTitle>
@ -293,37 +478,38 @@ export default function DashboardPage() {
{(metrics.recent_requests || [])
.slice(0, 20) // 只显示最近20条记录
.map((req, index) => (
<tr key={index} className="border-b hover:bg-gray-50">
<td className="p-2">{formatDate(req.Time)}</td>
<td className="p-2 max-w-xs truncate">
<a
href={req.Path}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 hover:underline"
>
{req.Path}
</a>
</td>
<td className="p-2">
<span
className={`px-2 py-1 rounded-full text-xs ${getStatusColor(
req.Status
)}`}
>
{req.Status}
</span>
</td>
<td className="p-2">{formatLatency(req.Latency)}</td>
<td className="p-2">{formatBytes(req.BytesSent)}</td>
<td className="p-2">{req.ClientIP}</td>
</tr>
))}
<tr key={index} className="border-b hover:bg-gray-50">
<td className="p-2">{formatDate(req.Time)}</td>
<td className="p-2 max-w-xs truncate">
<a
href={req.Path}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 hover:underline"
>
{req.Path}
</a>
</td>
<td className="p-2">
<span
className={`px-2 py-1 rounded-full text-xs ${getStatusColor(
req.Status
)}`}
>
{req.Status}
</span>
</td>
<td className="p-2">{formatLatency(req.Latency)}</td>
<td className="p-2">{formatBytes(req.BytesSent)}</td>
<td className="p-2">{req.ClientIP}</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
)
}