mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-18 08:31:55 +08:00
feat(metrics): 增强指标展示和统计功能
- 在指标结构中新增延迟统计、错误统计和引用来源统计字段 - 更新前端仪表盘,添加延迟、带宽、错误和引用来源统计卡片 - 优化指标收集器,支持引用来源和错误类型统计 - 在工具函数中新增字符串转整数解析方法 - 简化引用来源URL处理,提取域名信息
This commit is contained in:
parent
f0c806292b
commit
b6b77b03ed
@ -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"`
|
||||
|
||||
// 延迟统计
|
||||
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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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() {
|
||||
@ -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>
|
||||
@ -324,6 +509,7 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user