From 1e77085e1004143853bd019063cb21d81024d987 Mon Sep 17 00:00:00 2001 From: wood chen Date: Sun, 13 Jul 2025 03:57:25 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E7=BC=93=E5=AD=98=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=99=A8=E7=9A=84=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=BC=93=E5=AD=98=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=99=A8=E4=BB=A5=E6=94=AF=E6=8C=81=E5=9B=BE=E7=89=87=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E7=9A=84=E6=99=BA=E8=83=BD=E6=A0=BC=E5=BC=8F=E5=9B=9E?= =?UTF-8?q?=E9=80=80=E5=92=8C=E7=BB=9F=E8=AE=A1=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=BC=93=E5=AD=98=E5=91=BD=E4=B8=AD=E7=8E=87?= =?UTF-8?q?=E7=9A=84=E8=AE=B0=E5=BD=95=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E4=BB=AA=E8=A1=A8=E6=9D=BF=E7=9A=84=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E5=B1=95=E7=A4=BA=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/cache/manager.go | 337 +++++++++++++++++++++---------- internal/cache/manager_test.go | 64 ------ internal/handler/mirror_proxy.go | 40 ++-- internal/handler/proxy.go | 52 +++-- internal/metrics/collector.go | 17 +- web/app/dashboard/cache/page.tsx | 76 +++++++ web/app/dashboard/page.tsx | 26 ++- 7 files changed, 386 insertions(+), 226 deletions(-) delete mode 100644 internal/cache/manager_test.go diff --git a/internal/cache/manager.go b/internal/cache/manager.go index 05da703..2d5c3b7 100644 --- a/internal/cache/manager.go +++ b/internal/cache/manager.go @@ -228,13 +228,16 @@ type CacheItem struct { // CacheStats 缓存统计信息 type CacheStats struct { - TotalItems int `json:"total_items"` // 缓存项数量 - TotalSize int64 `json:"total_size"` // 总大小 - HitCount int64 `json:"hit_count"` // 命中次数 - MissCount int64 `json:"miss_count"` // 未命中次数 - HitRate float64 `json:"hit_rate"` // 命中率 - BytesSaved int64 `json:"bytes_saved"` // 节省的带宽 - Enabled bool `json:"enabled"` // 缓存开关状态 + TotalItems int `json:"total_items"` // 缓存项数量 + TotalSize int64 `json:"total_size"` // 总大小 + HitCount int64 `json:"hit_count"` // 命中次数 + MissCount int64 `json:"miss_count"` // 未命中次数 + HitRate float64 `json:"hit_rate"` // 命中率 + BytesSaved int64 `json:"bytes_saved"` // 节省的带宽 + Enabled bool `json:"enabled"` // 缓存开关状态 + FormatFallbackHit int64 `json:"format_fallback_hit"` // 格式回退命中次数 + ImageCacheHit int64 `json:"image_cache_hit"` // 图片缓存命中次数 + RegularCacheHit int64 `json:"regular_cache_hit"` // 常规缓存命中次数 } // CacheManager 缓存管理器 @@ -252,11 +255,13 @@ type CacheManager struct { cleanupTimer *time.Ticker // 添加清理定时器 stopCleanup chan struct{} // 添加停止信号通道 + // 新增:格式回退统计 + formatFallbackHit atomic.Int64 // 格式回退命中次数 + imageCacheHit atomic.Int64 // 图片缓存命中次数 + regularCacheHit atomic.Int64 // 常规缓存命中次数 + // ExtensionMatcher缓存 extensionMatcherCache *ExtensionMatcherCache - - // 缓存预热 - prewarming atomic.Bool } // NewCacheManager 创建新的缓存管理器 @@ -292,9 +297,6 @@ func NewCacheManager(cacheDir string) (*CacheManager, error) { // 启动清理协程 cm.startCleanup() - // 启动缓存预热 - go cm.prewarmCache() - return cm, nil } @@ -311,10 +313,79 @@ func (cm *CacheManager) GenerateCacheKey(r *http.Request) CacheKey { } sort.Strings(varyHeaders) + url := r.URL.String() + acceptHeaders := r.Header.Get("Accept") + userAgent := r.Header.Get("User-Agent") + + // 🎯 针对图片请求进行智能缓存键优化 + if utils.IsImageRequest(r.URL.Path) { + // 解析Accept头中的图片格式偏好 + imageFormat := cm.parseImageFormatPreference(acceptHeaders) + + // 为图片请求生成格式感知的缓存键 + return CacheKey{ + URL: url, + AcceptHeaders: imageFormat, // 使用标准化的图片格式 + UserAgent: cm.normalizeUserAgent(userAgent), // 标准化UserAgent + } + } + return CacheKey{ - URL: r.URL.String(), - AcceptHeaders: r.Header.Get("Accept"), - UserAgent: r.Header.Get("User-Agent"), + URL: url, + AcceptHeaders: acceptHeaders, + UserAgent: userAgent, + } +} + +// parseImageFormatPreference 解析图片格式偏好,返回标准化的格式标识 +func (cm *CacheManager) parseImageFormatPreference(accept string) string { + if accept == "" { + return "image/jpeg" // 默认格式 + } + + accept = strings.ToLower(accept) + + // 按优先级检查现代图片格式 + switch { + case strings.Contains(accept, "image/avif"): + return "image/avif" + case strings.Contains(accept, "image/webp"): + return "image/webp" + case strings.Contains(accept, "image/jpeg") || strings.Contains(accept, "image/jpg"): + return "image/jpeg" + case strings.Contains(accept, "image/png"): + return "image/png" + case strings.Contains(accept, "image/gif"): + return "image/gif" + case strings.Contains(accept, "image/*"): + return "image/auto" // 自动格式 + default: + return "image/jpeg" // 默认格式 + } +} + +// normalizeUserAgent 标准化UserAgent,减少缓存键的变化 +func (cm *CacheManager) normalizeUserAgent(ua string) string { + if ua == "" { + return "default" + } + + ua = strings.ToLower(ua) + + // 根据主要浏览器类型进行分类 + switch { + case strings.Contains(ua, "chrome") && !strings.Contains(ua, "edge"): + return "chrome" + case strings.Contains(ua, "firefox"): + return "firefox" + case strings.Contains(ua, "safari") && !strings.Contains(ua, "chrome"): + return "safari" + case strings.Contains(ua, "edge"): + return "edge" + case strings.Contains(ua, "bot") || strings.Contains(ua, "crawler"): + return "bot" + default: + return "other" } } @@ -324,6 +395,100 @@ func (cm *CacheManager) Get(key CacheKey, r *http.Request) (*CacheItem, bool, bo return nil, false, false } + // 🎯 针对图片请求实现智能格式回退 + if utils.IsImageRequest(r.URL.Path) { + return cm.getImageWithFallback(key, r) + } + + return cm.getRegularItem(key) +} + +// getImageWithFallback 获取图片缓存项,支持格式回退 +func (cm *CacheManager) getImageWithFallback(key CacheKey, r *http.Request) (*CacheItem, bool, bool) { + // 首先尝试精确匹配 + if item, found, notModified := cm.getRegularItem(key); found { + cm.imageCacheHit.Add(1) + return item, found, notModified + } + + // 如果精确匹配失败,尝试格式回退 + if item, found, notModified := cm.tryFormatFallback(key, r); found { + cm.formatFallbackHit.Add(1) + return item, found, notModified + } + + return nil, false, false +} + +// tryFormatFallback 尝试格式回退 +func (cm *CacheManager) tryFormatFallback(originalKey CacheKey, r *http.Request) (*CacheItem, bool, bool) { + requestedFormat := originalKey.AcceptHeaders + + // 定义格式回退顺序 + fallbackFormats := cm.getFormatFallbackOrder(requestedFormat) + + for _, format := range fallbackFormats { + fallbackKey := CacheKey{ + URL: originalKey.URL, + AcceptHeaders: format, + UserAgent: originalKey.UserAgent, + } + + if item, found, notModified := cm.getRegularItem(fallbackKey); found { + // 找到了兼容格式,检查是否真的兼容 + if cm.isFormatCompatible(requestedFormat, format, item.ContentType) { + log.Printf("[Cache] 格式回退: %s -> %s (%s)", requestedFormat, format, originalKey.URL) + return item, found, notModified + } + } + } + + return nil, false, false +} + +// getFormatFallbackOrder 获取格式回退顺序 +func (cm *CacheManager) getFormatFallbackOrder(requestedFormat string) []string { + switch requestedFormat { + case "image/avif": + return []string{"image/webp", "image/jpeg", "image/png"} + case "image/webp": + return []string{"image/jpeg", "image/png", "image/avif"} + case "image/jpeg": + return []string{"image/webp", "image/png", "image/avif"} + case "image/png": + return []string{"image/webp", "image/jpeg", "image/avif"} + case "image/auto": + return []string{"image/webp", "image/avif", "image/jpeg", "image/png"} + default: + return []string{"image/jpeg", "image/webp", "image/png"} + } +} + +// isFormatCompatible 检查格式是否兼容 +func (cm *CacheManager) isFormatCompatible(requestedFormat, cachedFormat, actualContentType string) bool { + // 如果是自动格式,接受任何现代格式 + if requestedFormat == "image/auto" { + return true + } + + // 现代浏览器通常可以处理多种格式 + modernFormats := map[string]bool{ + "image/webp": true, + "image/avif": true, + "image/jpeg": true, + "image/png": true, + } + + // 检查实际内容类型是否为现代格式 + if actualContentType != "" { + return modernFormats[strings.ToLower(actualContentType)] + } + + return modernFormats[cachedFormat] +} + +// getRegularItem 获取常规缓存项(原有逻辑) +func (cm *CacheManager) getRegularItem(key CacheKey) (*CacheItem, bool, bool) { // 检查LRU缓存 if item, found := cm.lruCache.Get(key); found { // 检查LRU缓存项是否过期 @@ -336,6 +501,7 @@ func (cm *CacheManager) Get(key CacheKey, r *http.Request) (*CacheItem, bool, bo item.LastAccess = time.Now() atomic.AddInt64(&item.AccessCount, 1) cm.hitCount.Add(1) + cm.regularCacheHit.Add(1) return item, true, false } @@ -367,6 +533,7 @@ func (cm *CacheManager) Get(key CacheKey, r *http.Request) (*CacheItem, bool, bo item.LastAccess = time.Now() atomic.AddInt64(&item.AccessCount, 1) cm.hitCount.Add(1) + cm.regularCacheHit.Add(1) cm.bytesSaved.Add(item.Size) // 将缓存项添加到LRU缓存 @@ -531,13 +698,16 @@ func (cm *CacheManager) GetStats() CacheStats { } return CacheStats{ - TotalItems: totalItems, - TotalSize: totalSize, - HitCount: hitCount, - MissCount: missCount, - HitRate: hitRate, - BytesSaved: cm.bytesSaved.Load(), - Enabled: cm.enabled.Load(), + TotalItems: totalItems, + TotalSize: totalSize, + HitCount: hitCount, + MissCount: missCount, + HitRate: hitRate, + BytesSaved: cm.bytesSaved.Load(), + Enabled: cm.enabled.Load(), + FormatFallbackHit: cm.formatFallbackHit.Load(), + ImageCacheHit: cm.imageCacheHit.Load(), + RegularCacheHit: cm.regularCacheHit.Load(), } } @@ -580,6 +750,9 @@ func (cm *CacheManager) ClearCache() error { cm.hitCount.Store(0) cm.missCount.Store(0) cm.bytesSaved.Store(0) + cm.formatFallbackHit.Store(0) + cm.imageCacheHit.Store(0) + cm.regularCacheHit.Store(0) return nil } @@ -650,15 +823,41 @@ func (cm *CacheManager) Commit(key CacheKey, tempPath string, resp *http.Respons return fmt.Errorf("cache is disabled") } - // 生成最终的缓存文件名 - h := sha256.New() - h.Write([]byte(key.String())) - hashStr := hex.EncodeToString(h.Sum(nil)) - ext := filepath.Ext(key.URL) - if ext == "" { - ext = ".bin" + // 读取临时文件内容以计算哈希 + tempData, err := os.ReadFile(tempPath) + if err != nil { + os.Remove(tempPath) + return fmt.Errorf("failed to read temp file: %v", err) } - filePath := filepath.Join(cm.cacheDir, hashStr+ext) + + // 计算内容哈希,与Put方法保持一致 + contentHash := sha256.Sum256(tempData) + hashStr := hex.EncodeToString(contentHash[:]) + + // 检查是否存在相同哈希的缓存项 + var existingItem *CacheItem + cm.items.Range(func(k, v interface{}) bool { + if item := v.(*CacheItem); item.Hash == hashStr { + if _, err := os.Stat(item.FilePath); err == nil { + existingItem = item + return false + } + cm.items.Delete(k) + } + return true + }) + + if existingItem != nil { + // 删除临时文件,使用现有缓存 + os.Remove(tempPath) + cm.items.Store(key, existingItem) + log.Printf("[Cache] HIT %s %s (%s) from %s", resp.Request.Method, key.URL, formatBytes(existingItem.Size), utils.GetRequestSource(resp.Request)) + return nil + } + + // 生成最终的缓存文件名(使用内容哈希) + fileName := hashStr + filePath := filepath.Join(cm.cacheDir, fileName) // 重命名临时文件 if err := os.Rename(tempPath, filePath); err != nil { @@ -848,79 +1047,3 @@ func (cm *CacheManager) Stop() { cm.extensionMatcherCache.Stop() } } - -// prewarmCache 启动缓存预热 -func (cm *CacheManager) prewarmCache() { - // 模拟一些请求来预热缓存 - // 实际应用中,这里会从数据库或外部服务加载热点数据 - // 例如,从数据库加载最近访问频率高的URL - // 或者从外部API获取热门资源 - - // 示例:加载最近访问的URL - // 假设我们有一个数据库或文件,记录最近访问的URL和它们的哈希 - // 这里我们简单地加载一些示例URL - exampleUrls := []string{ - "https://example.com/api/data", - "https://api.github.com/repos/golang/go/releases", - "https://api.openai.com/v1/models", - "https://api.openai.com/v1/chat/completions", - } - - for _, url := range exampleUrls { - // 生成一个随机的Accept Headers和UserAgent - acceptHeaders := "application/json" - userAgent := "Mozilla/5.0 (compatible; ProxyGo/1.0)" - - // 模拟一个HTTP请求 - req, err := http.NewRequest("GET", url, nil) - if err != nil { - log.Printf("[Cache] ERR Failed to create request for prewarming: %v", err) - continue - } - req.Header.Set("Accept", acceptHeaders) - req.Header.Set("User-Agent", userAgent) - - // 生成缓存键 - cacheKey := cm.GenerateCacheKey(req) - - // 尝试从LRU缓存获取 - if _, found := cm.lruCache.Get(cacheKey); found { - log.Printf("[Cache] WARN %s (prewarmed)", cacheKey.URL) - continue - } - - // 尝试从文件缓存获取 - if _, ok := cm.items.Load(cacheKey); ok { - log.Printf("[Cache] WARN %s (prewarmed)", cacheKey.URL) - continue - } - - // 模拟一个HTTP响应 - resp := &http.Response{ - StatusCode: http.StatusOK, - Header: make(http.Header), - Request: req, - } - resp.Header.Set("Content-Type", "application/json") - resp.Header.Set("Content-Encoding", "gzip") - resp.Header.Set("X-Cache", "HIT") // 模拟缓存命中 - - // 模拟一个HTTP请求体 - body := []byte(`{"message": "Hello from prewarmed cache"}`) - - // 添加到LRU缓存 - contentHash := sha256.Sum256(body) - cm.lruCache.Put(cacheKey, &CacheItem{ - FilePath: "", // 文件缓存,这里不需要 - ContentType: "application/json", - ContentEncoding: "gzip", - Size: int64(len(body)), - LastAccess: time.Now(), - Hash: hex.EncodeToString(contentHash[:]), - CreatedAt: time.Now(), - AccessCount: 1, - }) - - log.Printf("[Cache] PREWARM %s", cacheKey.URL) - } -} diff --git a/internal/cache/manager_test.go b/internal/cache/manager_test.go deleted file mode 100644 index 2521f7f..0000000 --- a/internal/cache/manager_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package cache - -import ( - "net/http" - "os" - "testing" - "time" -) - -func TestCacheExpiry(t *testing.T) { - // 创建临时目录用于测试 - tempDir, err := os.MkdirTemp("", "cache-test-*") - if err != nil { - t.Fatal("Failed to create temp dir:", err) - } - defer os.RemoveAll(tempDir) - - // 创建缓存管理器,设置较短的过期时间(5秒)用于测试 - cm, err := NewCacheManager(tempDir) - if err != nil { - t.Fatal("Failed to create cache manager:", err) - } - cm.maxAge = 5 * time.Second - - // 创建测试请求和响应 - req, _ := http.NewRequest("GET", "http://example.com/test", nil) - resp := &http.Response{ - StatusCode: http.StatusOK, - Header: make(http.Header), - } - testData := []byte("test data") - - // 生成缓存键 - key := cm.GenerateCacheKey(req) - - // 1. 首先放入缓存 - _, err = cm.Put(key, resp, testData) - if err != nil { - t.Fatal("Failed to put item in cache:", err) - } - - // 2. 立即获取,应该能命中 - if _, hit, _ := cm.Get(key, req); !hit { - t.Error("Cache should hit immediately after putting") - } - - // 3. 等待3秒(未过期),再次访问 - time.Sleep(3 * time.Second) - if _, hit, _ := cm.Get(key, req); !hit { - t.Error("Cache should hit after 3 seconds") - } - - // 4. 再等待3秒(总共6秒,但因为上次访问重置了时间,所以应该还在有效期内) - time.Sleep(3 * time.Second) - if _, hit, _ := cm.Get(key, req); !hit { - t.Error("Cache should hit after 6 seconds because last access reset the timer") - } - - // 5. 等待6秒(超过过期时间且无访问),这次应该过期 - time.Sleep(6 * time.Second) - if _, hit, _ := cm.Get(key, req); hit { - t.Error("Cache should expire after 6 seconds of no access") - } -} diff --git a/internal/handler/mirror_proxy.go b/internal/handler/mirror_proxy.go index e81c107..2fa5d0f 100644 --- a/internal/handler/mirror_proxy.go +++ b/internal/handler/mirror_proxy.go @@ -120,6 +120,26 @@ func (h *MirrorProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { actualURL += "?" + r.URL.RawQuery } + // 早期缓存检查 - 只对GET请求进行缓存检查 + if r.Method == http.MethodGet && h.Cache != nil { + cacheKey := h.Cache.GenerateCacheKey(r) + if item, hit, notModified := h.Cache.Get(cacheKey, r); hit { + // 从缓存提供响应 + w.Header().Set("Content-Type", item.ContentType) + if item.ContentEncoding != "" { + w.Header().Set("Content-Encoding", item.ContentEncoding) + } + w.Header().Set("Proxy-Go-Cache-HIT", "1") + if notModified { + w.WriteHeader(http.StatusNotModified) + return + } + http.ServeFile(w, r, item.FilePath) + collector.RecordRequest(r.URL.Path, http.StatusOK, time.Since(startTime), item.Size, iputil.GetClientIP(r), r) + return + } + } + // 解析目标 URL 以获取 host parsedURL, err := url.Parse(actualURL) if err != nil { @@ -162,26 +182,6 @@ func (h *MirrorProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { proxyReq.Header.Set("Host", parsedURL.Host) proxyReq.Host = parsedURL.Host - // 检查是否可以使用缓存 - if r.Method == http.MethodGet && h.Cache != nil { - cacheKey := h.Cache.GenerateCacheKey(r) - if item, hit, notModified := h.Cache.Get(cacheKey, r); hit { - // 从缓存提供响应 - w.Header().Set("Content-Type", item.ContentType) - if item.ContentEncoding != "" { - w.Header().Set("Content-Encoding", item.ContentEncoding) - } - w.Header().Set("Proxy-Go-Cache-HIT", "1") - if notModified { - w.WriteHeader(http.StatusNotModified) - return - } - http.ServeFile(w, r, item.FilePath) - collector.RecordRequest(r.URL.Path, http.StatusOK, time.Since(startTime), item.Size, iputil.GetClientIP(r), r) - return - } - } - // 发送请求 resp, err := h.client.Do(proxyReq) if err != nil { diff --git a/internal/handler/proxy.go b/internal/handler/proxy.go index 90cb3b4..cd8b227 100644 --- a/internal/handler/proxy.go +++ b/internal/handler/proxy.go @@ -262,6 +262,28 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + // 早期缓存检查 - 只对GET请求进行缓存检查 + if r.Method == http.MethodGet && h.Cache != nil { + cacheKey := h.Cache.GenerateCacheKey(r) + if item, hit, notModified := h.Cache.Get(cacheKey, r); hit { + // 从缓存提供响应 + w.Header().Set("Content-Type", item.ContentType) + if item.ContentEncoding != "" { + w.Header().Set("Content-Encoding", item.ContentEncoding) + } + w.Header().Set("Proxy-Go-Cache-HIT", "1") + w.Header().Set("Proxy-Go-AltTarget", "0") // 缓存命中时设为0 + + if notModified { + w.WriteHeader(http.StatusNotModified) + return + } + http.ServeFile(w, r, item.FilePath) + collector.RecordRequest(r.URL.Path, http.StatusOK, time.Since(start), item.Size, iputil.GetClientIP(r), r) + return + } + } + // 构建目标 URL targetPath := strings.TrimPrefix(r.URL.Path, matchedPrefix) @@ -384,33 +406,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } - // 检查是否可以使用缓存 - if r.Method == http.MethodGet && h.Cache != nil { - cacheKey := h.Cache.GenerateCacheKey(r) - if item, hit, notModified := h.Cache.Get(cacheKey, r); hit { - // 从缓存提供响应 - w.Header().Set("Content-Type", item.ContentType) - if item.ContentEncoding != "" { - w.Header().Set("Content-Encoding", item.ContentEncoding) - } - w.Header().Set("Proxy-Go-Cache-HIT", "1") - - // 如果使用了扩展名映射的备用目标,添加标记响应头 - if usedAltTarget { - w.Header().Set("Proxy-Go-AltTarget", "1") - } - w.Header().Set("Proxy-Go-AltTarget", "0") - - if notModified { - w.WriteHeader(http.StatusNotModified) - return - } - http.ServeFile(w, r, item.FilePath) - collector.RecordRequest(r.URL.Path, http.StatusOK, time.Since(start), item.Size, iputil.GetClientIP(r), r) - return - } - } - // 发送代理请求 resp, err := h.client.Do(proxyReq) if err != nil { @@ -432,8 +427,9 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 如果使用了扩展名映射的备用目标,添加标记响应头 if usedAltTarget { w.Header().Set("Proxy-Go-AltTarget", "1") + } else { + w.Header().Set("Proxy-Go-AltTarget", "0") } - w.Header().Set("Proxy-Go-AltTarget", "0") // 设置响应状态码 w.WriteHeader(resp.StatusCode) diff --git a/internal/metrics/collector.go b/internal/metrics/collector.go index b350d3a..ff01f18 100644 --- a/internal/metrics/collector.go +++ b/internal/metrics/collector.go @@ -10,6 +10,7 @@ import ( "proxy-go/internal/utils" "runtime" "sort" + "strconv" "sync" "sync/atomic" "time" @@ -318,9 +319,14 @@ func (c *Collector) GetStats() map[string]interface{} { // 计算总请求数和平均延迟 var totalRequests int64 + var totalErrors int64 statusCodeStats := c.statusCodeStats.GetStats() - for _, count := range statusCodeStats { + for statusCode, count := range statusCodeStats { totalRequests += count + // 计算错误数(4xx和5xx状态码) + if code, err := strconv.Atoi(statusCode); err == nil && code >= 400 { + totalErrors += count + } } avgLatency := float64(0) @@ -328,6 +334,12 @@ func (c *Collector) GetStats() map[string]interface{} { avgLatency = float64(atomic.LoadInt64(&c.latencySum)) / float64(totalRequests) } + // 计算错误率 + errorRate := float64(0) + if totalRequests > 0 { + errorRate = float64(totalErrors) / float64(totalRequests) + } + // 计算总体平均每秒请求数 requestsPerSecond := float64(totalRequests) / totalRuntime.Seconds() @@ -396,6 +408,9 @@ func (c *Collector) GetStats() map[string]interface{} { return map[string]interface{}{ "uptime": FormatUptime(totalRuntime), "active_requests": atomic.LoadInt64(&c.activeRequests), + "total_requests": totalRequests, + "total_errors": totalErrors, + "error_rate": errorRate, "total_bytes": atomic.LoadInt64(&c.totalBytes), "num_goroutine": runtime.NumGoroutine(), "memory_usage": utils.FormatBytes(int64(mem.Alloc)), diff --git a/web/app/dashboard/cache/page.tsx b/web/app/dashboard/cache/page.tsx index e1e51f9..e5fd057 100644 --- a/web/app/dashboard/cache/page.tsx +++ b/web/app/dashboard/cache/page.tsx @@ -17,6 +17,9 @@ interface CacheStats { hit_rate: number bytes_saved: number enabled: boolean + format_fallback_hit: number + image_cache_hit: number + regular_cache_hit: number } interface CacheConfig { @@ -325,6 +328,45 @@ export default function CachePage() { + {/* 智能缓存汇总 */} + + + 智能缓存汇总 + + +
+
+
+ {(stats?.proxy.regular_cache_hit ?? 0) + (stats?.mirror.regular_cache_hit ?? 0)} +
+
常规缓存命中
+
+
+
+ {(stats?.proxy.image_cache_hit ?? 0) + (stats?.mirror.image_cache_hit ?? 0)} +
+
图片精确命中
+
+
+
+ {(stats?.proxy.format_fallback_hit ?? 0) + (stats?.mirror.format_fallback_hit ?? 0)} +
+
格式回退命中
+
+
+
+ {(() => { + const totalImageRequests = (stats?.proxy.image_cache_hit ?? 0) + (stats?.mirror.image_cache_hit ?? 0) + (stats?.proxy.format_fallback_hit ?? 0) + (stats?.mirror.format_fallback_hit ?? 0) + const fallbackHits = (stats?.proxy.format_fallback_hit ?? 0) + (stats?.mirror.format_fallback_hit ?? 0) + return totalImageRequests > 0 ? ((fallbackHits / totalImageRequests) * 100).toFixed(1) : '0.0' + })()}% +
+
格式回退率
+
+
+
+
+
{/* 代理缓存 */} @@ -370,6 +412,23 @@ export default function CachePage() {
节省带宽
{formatBytes(stats?.proxy.bytes_saved ?? 0)}
+
+
智能缓存统计
+
+
+
{stats?.proxy.regular_cache_hit ?? 0}
+
常规命中
+
+
+
{stats?.proxy.image_cache_hit ?? 0}
+
图片命中
+
+
+
{stats?.proxy.format_fallback_hit ?? 0}
+
格式回退
+
+
+
{renderCacheConfig("proxy")} @@ -419,6 +478,23 @@ export default function CachePage() {
节省带宽
{formatBytes(stats?.mirror.bytes_saved ?? 0)}
+
+
智能缓存统计
+
+
+
{stats?.mirror.regular_cache_hit ?? 0}
+
常规命中
+
+
+
{stats?.mirror.image_cache_hit ?? 0}
+
图片命中
+
+
+
{stats?.mirror.format_fallback_hit ?? 0}
+
格式回退
+
+
+
{renderCacheConfig("mirror")} diff --git a/web/app/dashboard/page.tsx b/web/app/dashboard/page.tsx index d64494e..0986372 100644 --- a/web/app/dashboard/page.tsx +++ b/web/app/dashboard/page.tsx @@ -40,7 +40,7 @@ interface Metrics { error_count: number avg_latency: string bytes_transferred: number - last_access_time: number // 添加最后访问时间字段 + last_access_time: number }> } @@ -93,7 +93,7 @@ export default function DashboardPage() { useEffect(() => { fetchMetrics() - const interval = setInterval(fetchMetrics, 3000) + const interval = setInterval(fetchMetrics, 1000) return () => clearInterval(interval) }, [fetchMetrics]) @@ -146,6 +146,18 @@ export default function DashboardPage() {
当前活跃请求
{metrics.active_requests}
+
+
总请求数
+
{metrics.total_requests || Object.values(metrics.status_code_stats || {}).reduce((a, b) => a + (b as number), 0)}
+
+
+
总错误数
+
{metrics.total_errors || 0}
+
+
+
错误率
+
{((metrics.error_rate || 0) * 100).toFixed(2)}%
+
总传输数据
{formatBytes(metrics.total_bytes)}
@@ -154,6 +166,10 @@ export default function DashboardPage() {
每秒传输数据
{formatBytes(metrics.bytes_per_second)}/s
+
+
平均每秒请求数
+
{metrics.requests_per_second.toFixed(2)}
+
@@ -177,10 +193,8 @@ export default function DashboardPage() {
{metrics.avg_response_time}
-
平均每秒请求数
-
- {metrics.requests_per_second.toFixed(2)} -
+
当前带宽
+
{metrics.current_bandwidth}