diff --git a/data/config.json b/data/config.json index a337443..b10e9d8 100644 --- a/data/config.json +++ b/data/config.json @@ -5,11 +5,13 @@ "ExtensionMap": { "jpg,png,avif": "https://path1-img.com/path/path/path", "mp4,webm": "https://path1-video.com/path/path/path" - } + }, + "SizeThreshold": 204800 }, "/path2": "https://path2.com", "/path3": { - "DefaultTarget": "https://path3.com" + "DefaultTarget": "https://path3.com", + "SizeThreshold": 512000 } }, "Compression": { @@ -36,6 +38,23 @@ ], "Metrics": { "Password": "admin123", - "TokenExpiry": 86400 + "TokenExpiry": 86400, + "FeishuWebhook": "https://open.feishu.cn/open-apis/bot/v2/hook/****", + "Alert": { + "WindowSize": 12, + "WindowInterval": "5m", + "DedupeWindow": "15m", + "MinRequests": 10, + "ErrorRate": 0.5 + }, + "Latency": { + "SmallFileSize": 1048576, + "MediumFileSize": 10485760, + "LargeFileSize": 104857600, + "SmallLatency": "3s", + "MediumLatency": "8s", + "LargeLatency": "30s", + "HugeLatency": "300s" + } } } \ No newline at end of file diff --git a/go.mod b/go.mod index caca1c4..346d696 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,6 @@ go 1.23.1 require ( github.com/andybalholm/brotli v1.1.1 + github.com/mattn/go-sqlite3 v1.14.22 golang.org/x/time v0.8.0 ) diff --git a/go.sum b/go.sum index 69caaa3..d94b586 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= diff --git a/internal/cache/cache.go b/internal/cache/cache.go new file mode 100644 index 0000000..ecfa406 --- /dev/null +++ b/internal/cache/cache.go @@ -0,0 +1,82 @@ +package cache + +import ( + "proxy-go/internal/constants" + "sync" + "time" +) + +type Cache struct { + data sync.RWMutex + items map[string]*cacheItem + ttl time.Duration + maxSize int +} + +type cacheItem struct { + value interface{} + timestamp time.Time +} + +func NewCache(ttl time.Duration) *Cache { + c := &Cache{ + items: make(map[string]*cacheItem), + ttl: ttl, + maxSize: constants.MaxCacheSize, + } + go c.cleanup() + return c +} + +func (c *Cache) Set(key string, value interface{}) { + c.data.Lock() + if len(c.items) >= c.maxSize { + oldest := time.Now() + var oldestKey string + for k, v := range c.items { + if v.timestamp.Before(oldest) { + oldest = v.timestamp + oldestKey = k + } + } + delete(c.items, oldestKey) + } + c.items[key] = &cacheItem{ + value: value, + timestamp: time.Now(), + } + c.data.Unlock() +} + +func (c *Cache) Get(key string) (interface{}, bool) { + c.data.RLock() + item, exists := c.items[key] + c.data.RUnlock() + + if !exists { + return nil, false + } + + if time.Since(item.timestamp) > c.ttl { + c.data.Lock() + delete(c.items, key) + c.data.Unlock() + return nil, false + } + + return item.value, true +} + +func (c *Cache) cleanup() { + ticker := time.NewTicker(c.ttl) + for range ticker.C { + now := time.Now() + c.data.Lock() + for key, item := range c.items { + if now.Sub(item.timestamp) > c.ttl { + delete(c.items, key) + } + } + c.data.Unlock() + } +} diff --git a/internal/config/types.go b/internal/config/types.go index f4f841a..d60073d 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -3,6 +3,7 @@ package config import ( "encoding/json" "strings" + "time" ) type Config struct { @@ -15,6 +16,7 @@ type Config struct { type PathConfig struct { DefaultTarget string `json:"DefaultTarget"` // 默认回源地址 ExtensionMap map[string]string `json:"ExtensionMap"` // 特定后缀的回源地址 + SizeThreshold int64 `json:"SizeThreshold"` // 文件大小阈值(字节),超过此大小才使用ExtensionMap processedExtMap map[string]string // 内部使用,存储拆分后的映射 } @@ -35,8 +37,27 @@ type FixedPathConfig struct { } type MetricsConfig struct { - Password string `json:"Password"` - TokenExpiry int `json:"TokenExpiry"` // token有效期(秒) + Password string `json:"Password"` + TokenExpiry int `json:"TokenExpiry"` + FeishuWebhook string `json:"FeishuWebhook"` + // 监控告警配置 + Alert struct { + WindowSize int `json:"WindowSize"` // 监控窗口数量 + WindowInterval time.Duration `json:"WindowInterval"` // 每个窗口时间长度 + DedupeWindow time.Duration `json:"DedupeWindow"` // 告警去重时间窗口 + MinRequests int64 `json:"MinRequests"` // 触发告警的最小请求数 + ErrorRate float64 `json:"ErrorRate"` // 错误率告警阈值 + } `json:"Alert"` + // 延迟告警配置 + Latency struct { + SmallFileSize int64 `json:"SmallFileSize"` // 小文件阈值 + MediumFileSize int64 `json:"MediumFileSize"` // 中等文件阈值 + LargeFileSize int64 `json:"LargeFileSize"` // 大文件阈值 + SmallLatency time.Duration `json:"SmallLatency"` // 小文件最大延迟 + MediumLatency time.Duration `json:"MediumLatency"` // 中等文件最大延迟 + LargeLatency time.Duration `json:"LargeLatency"` // 大文件最大延迟 + HugeLatency time.Duration `json:"HugeLatency"` // 超大文件最大延迟 + } `json:"Latency"` } // 添加一个辅助方法来处理字符串到 PathConfig 的转换 @@ -115,3 +136,12 @@ func (p *PathConfig) GetTargetForExt(ext string) string { } return p.DefaultTarget } + +// 添加检查扩展名是否存在的方法 +func (p *PathConfig) GetExtensionTarget(ext string) (string, bool) { + if p.processedExtMap == nil { + p.ProcessExtensionMap() + } + target, exists := p.processedExtMap[ext] + return target, exists +} diff --git a/internal/constants/constants.go b/internal/constants/constants.go new file mode 100644 index 0000000..e5607ea --- /dev/null +++ b/internal/constants/constants.go @@ -0,0 +1,86 @@ +package constants + +import ( + "proxy-go/internal/config" + "time" +) + +var ( + // 缓存相关 + CacheTTL = 5 * time.Minute // 缓存过期时间 + MaxCacheSize = 10000 // 最大缓存大小 + + // 数据库相关 + CleanupInterval = 24 * time.Hour // 清理间隔 + DataRetention = 90 * 24 * time.Hour // 数据保留时间 + BatchSize = 100 // 批量写入大小 + + // 指标相关 + MetricsInterval = 5 * time.Minute // 指标收集间隔 + MaxPathsStored = 1000 // 最大存储路径数 + MaxRecentLogs = 1000 // 最大最近日志数 + + // 监控告警相关 + AlertWindowSize = 12 // 监控窗口数量 + AlertWindowInterval = 5 * time.Minute // 每个窗口时间长度 + AlertDedupeWindow = 15 * time.Minute // 告警去重时间窗口 + MinRequestsForAlert int64 = 10 // 触发告警的最小请求数 + ErrorRateThreshold = 0.5 // 错误率告警阈值 (50%) + + // 延迟告警阈值 + SmallFileSize int64 = 1 * MB // 小文件阈值 + MediumFileSize int64 = 10 * MB // 中等文件阈值 + LargeFileSize int64 = 100 * MB // 大文件阈值 + + SmallFileLatency = 3 * time.Second // 小文件最大延迟 + MediumFileLatency = 8 * time.Second // 中等文件最大延迟 + LargeFileLatency = 30 * time.Second // 大文件最大延迟 + HugeFileLatency = 300 * time.Second // 超大文件最大延迟 (5分钟) + + // 单位常量 + KB int64 = 1024 + MB int64 = 1024 * KB +) + +// UpdateFromConfig 从配置文件更新常量 +func UpdateFromConfig(cfg *config.Config) { + // 告警配置 + if cfg.Metrics.Alert.WindowSize > 0 { + AlertWindowSize = cfg.Metrics.Alert.WindowSize + } + if cfg.Metrics.Alert.WindowInterval > 0 { + AlertWindowInterval = cfg.Metrics.Alert.WindowInterval + } + if cfg.Metrics.Alert.DedupeWindow > 0 { + AlertDedupeWindow = cfg.Metrics.Alert.DedupeWindow + } + if cfg.Metrics.Alert.MinRequests > 0 { + MinRequestsForAlert = cfg.Metrics.Alert.MinRequests + } + if cfg.Metrics.Alert.ErrorRate > 0 { + ErrorRateThreshold = cfg.Metrics.Alert.ErrorRate + } + + // 延迟告警配置 + if cfg.Metrics.Latency.SmallFileSize > 0 { + SmallFileSize = cfg.Metrics.Latency.SmallFileSize + } + if cfg.Metrics.Latency.MediumFileSize > 0 { + MediumFileSize = cfg.Metrics.Latency.MediumFileSize + } + if cfg.Metrics.Latency.LargeFileSize > 0 { + LargeFileSize = cfg.Metrics.Latency.LargeFileSize + } + if cfg.Metrics.Latency.SmallLatency > 0 { + SmallFileLatency = cfg.Metrics.Latency.SmallLatency + } + if cfg.Metrics.Latency.MediumLatency > 0 { + MediumFileLatency = cfg.Metrics.Latency.MediumLatency + } + if cfg.Metrics.Latency.LargeLatency > 0 { + LargeFileLatency = cfg.Metrics.Latency.LargeLatency + } + if cfg.Metrics.Latency.HugeLatency > 0 { + HugeFileLatency = cfg.Metrics.Latency.HugeLatency + } +} diff --git a/internal/errors/errors.go b/internal/errors/errors.go new file mode 100644 index 0000000..a54c576 --- /dev/null +++ b/internal/errors/errors.go @@ -0,0 +1,23 @@ +package errors + +type ErrorCode int + +const ( + ErrDatabase ErrorCode = iota + 1 + ErrInvalidConfig + ErrRateLimit + ErrMetricsCollection +) + +type MetricsError struct { + Code ErrorCode + Message string + Err error +} + +func (e *MetricsError) Error() string { + if e.Err != nil { + return e.Message + ": " + e.Err.Error() + } + return e.Message +} diff --git a/internal/handler/metrics.go b/internal/handler/metrics.go index fa93e95..243e333 100644 --- a/internal/handler/metrics.go +++ b/internal/handler/metrics.go @@ -5,6 +5,8 @@ import ( "log" "net/http" "proxy-go/internal/metrics" + "proxy-go/internal/models" + "strconv" "strings" "time" ) @@ -26,13 +28,13 @@ type Metrics struct { 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"` - LatencyPercentiles map[string]float64 `json:"latency_percentiles"` - TopPaths []metrics.PathMetrics `json:"top_paths"` - RecentRequests []metrics.RequestLog `json:"recent_requests"` - TopReferers []metrics.PathMetrics `json:"top_referers"` + TotalBytes int64 `json:"total_bytes"` + BytesPerSecond float64 `json:"bytes_per_second"` + StatusCodeStats map[string]int64 `json:"status_code_stats"` + LatencyPercentiles map[string]float64 `json:"latency_percentiles"` + TopPaths []models.PathMetrics `json:"top_paths"` + RecentRequests []models.RequestLog `json:"recent_requests"` + TopReferers []models.PathMetrics `json:"top_referers"` } func (h *ProxyHandler) MetricsHandler(w http.ResponseWriter, r *http.Request) { @@ -58,9 +60,9 @@ func (h *ProxyHandler) MetricsHandler(w http.ResponseWriter, r *http.Request) { BytesPerSecond: float64(stats["total_bytes"].(int64)) / metrics.Max(uptime.Seconds(), 1), RequestsPerSecond: float64(stats["total_requests"].(int64)) / metrics.Max(uptime.Seconds(), 1), StatusCodeStats: stats["status_code_stats"].(map[string]int64), - TopPaths: stats["top_paths"].([]metrics.PathMetrics), - RecentRequests: stats["recent_requests"].([]metrics.RequestLog), - TopReferers: stats["top_referers"].([]metrics.PathMetrics), + TopPaths: stats["top_paths"].([]models.PathMetrics), + RecentRequests: stats["recent_requests"].([]models.RequestLog), + TopReferers: stats["top_referers"].([]models.PathMetrics), } w.Header().Set("Content-Type", "application/json") @@ -270,6 +272,19 @@ var metricsTemplate = ` .grid-container .card { margin-bottom: 0; } + .chart-container { + margin-top: 20px; + } + .chart { + height: 200px; + margin-bottom: 20px; + } + #timeRange { + padding: 8px; + border-radius: 4px; + border: 1px solid #ddd; + margin-bottom: 15px; + } @@ -388,6 +403,18 @@ var metricsTemplate = ` +
+

历史数据

+ +
+
+ @@ -516,7 +543,128 @@ var metricsTemplate = ` // 每5秒自动刷新 setInterval(refreshMetrics, 5000); + + // 添加图表相关代码 + function loadHistoryData() { + const hours = document.getElementById('timeRange').value; + fetch('/metrics/history?hours=' + hours, { + headers: { + 'Authorization': 'Bearer ' + token + } + }) + .then(response => response.json()) + .then(data => { + const labels = data.map(m => { + const date = new Date(m.timestamp); + if (hours <= 24) { + return date.toLocaleTimeString(); + } else if (hours <= 168) { + return date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); + } else { + return date.toLocaleDateString(); + } + }); + + // 清除旧图表 + document.getElementById('historyChart').innerHTML = ''; + + // 创建新图表 + document.getElementById('historyChart').innerHTML = '
' + + '

请求数

' + + '
' + + '' + + '
' + + '

错误率

' + + '
' + + '' + + '
' + + '

流量

' + + '
' + + '' + + '
' + + '
'; + + // 绘制图表 + new Chart(document.getElementById('requestsChart'), { + type: 'line', + data: { + labels: labels, + datasets: [{ + label: '总请求数', + data: data.map(m => m.total_requests), + borderColor: '#4CAF50', + fill: false + }] + }, + options: { + scales: { + x: { + ticks: { + maxRotation: 45, + minRotation: 45 + } + } + } + } + }); + + new Chart(document.getElementById('errorRateChart'), { + type: 'line', + data: { + labels: labels, + datasets: [{ + label: '错误率', + data: data.map(m => (m.error_rate * 100).toFixed(2)), + borderColor: '#dc3545', + fill: false + }] + }, + options: { + scales: { + x: { + ticks: { + maxRotation: 45, + minRotation: 45 + } + } + } + } + }); + + new Chart(document.getElementById('bytesChart'), { + type: 'line', + data: { + labels: labels, + datasets: [{ + label: '传输字节', + data: data.map(m => m.total_bytes / 1024 / 1024), // 转换为MB + borderColor: '#17a2b8', + fill: false + }] + }, + options: { + scales: { + x: { + ticks: { + maxRotation: 45, + minRotation: 45 + } + } + } + } + }); + }); + } + + // 监听时间范围变化 + document.getElementById('timeRange').addEventListener('change', loadHistoryData); + + // 初始加载历史数据 + loadHistoryData(); + + + ` @@ -548,6 +696,26 @@ func (h *ProxyHandler) MetricsPageHandler(w http.ResponseWriter, r *http.Request func (h *ProxyHandler) MetricsDashboardHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") + metricsTemplate = strings.Replace(metricsTemplate, + ` + + `, + ` + +
+

历史数据

+ +
+
+ + `, 1) + w.Write([]byte(metricsTemplate)) } @@ -578,3 +746,23 @@ func (h *ProxyHandler) MetricsAuthHandler(w http.ResponseWriter, r *http.Request "token": token, }) } + +// 添加历史数据查询接口 +func (h *ProxyHandler) MetricsHistoryHandler(w http.ResponseWriter, r *http.Request) { + hours := 24 // 默认24小时 + if h := r.URL.Query().Get("hours"); h != "" { + if parsed, err := strconv.Atoi(h); err == nil && parsed > 0 { + hours = parsed + } + } + + collector := metrics.GetCollector() + metrics, err := collector.GetDB().GetRecentMetrics(hours) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(metrics) +} diff --git a/internal/handler/proxy.go b/internal/handler/proxy.go index f536571..48e7523 100644 --- a/internal/handler/proxy.go +++ b/internal/handler/proxy.go @@ -6,7 +6,6 @@ import ( "log" "net/http" "net/url" - "path" "proxy-go/internal/config" "proxy-go/internal/metrics" "proxy-go/internal/utils" @@ -109,17 +108,8 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - // 确定标基础URL - targetBase := pathConfig.DefaultTarget - - // 检查文件扩展名 - if pathConfig.ExtensionMap != nil { - ext := strings.ToLower(path.Ext(decodedPath)) - if ext != "" { - ext = ext[1:] // 移除开头的点 - targetBase = pathConfig.GetTargetForExt(ext) - } - } + // 确定基础URL + targetBase := utils.GetTargetURL(h.client, r, pathConfig, decodedPath) // 重新编码路径,保留 '/' parts := strings.Split(decodedPath, "/") diff --git a/internal/metrics/collector.go b/internal/metrics/collector.go index 568bdea..096cbd2 100644 --- a/internal/metrics/collector.go +++ b/internal/metrics/collector.go @@ -2,7 +2,13 @@ package metrics import ( "fmt" + "log" "net/http" + "proxy-go/internal/cache" + "proxy-go/internal/config" + "proxy-go/internal/constants" + "proxy-go/internal/models" + "proxy-go/internal/monitor" "runtime" "sort" "sync" @@ -23,16 +29,62 @@ type Collector struct { latencyBuckets [10]atomic.Int64 recentRequests struct { sync.RWMutex - items [1000]*RequestLog + items [1000]*models.RequestLog cursor atomic.Int64 } + db *models.MetricsDB + cache *cache.Cache + monitor *monitor.Monitor + statsPool sync.Pool } -var globalCollector = &Collector{ - startTime: time.Now(), - pathStats: sync.Map{}, - statusStats: [6]atomic.Int64{}, - latencyBuckets: [10]atomic.Int64{}, +var globalCollector *Collector + +func InitCollector(dbPath string, config *config.Config) error { + db, err := models.NewMetricsDB(dbPath) + if err != nil { + return err + } + + globalCollector = &Collector{ + startTime: time.Now(), + pathStats: sync.Map{}, + statusStats: [6]atomic.Int64{}, + latencyBuckets: [10]atomic.Int64{}, + db: db, + } + + globalCollector.cache = cache.NewCache(constants.CacheTTL) + globalCollector.monitor = monitor.NewMonitor() + + // 如果配置了飞书webhook,则启用飞书告警 + if config.Metrics.FeishuWebhook != "" { + globalCollector.monitor.AddHandler( + monitor.NewFeishuHandler(config.Metrics.FeishuWebhook), + ) + log.Printf("Feishu alert enabled") + } + + // 启动定时保存 + go func() { + ticker := time.NewTicker(5 * time.Minute) + for range ticker.C { + stats := globalCollector.GetStats() + if err := db.SaveMetrics(stats); err != nil { + log.Printf("Error saving metrics: %v", err) + } else { + log.Printf("Metrics saved successfully") + } + } + }() + + globalCollector.statsPool = sync.Pool{ + New: func() interface{} { + return make(map[string]interface{}, 20) + }, + } + + return nil } func GetCollector() *Collector { @@ -72,37 +124,37 @@ func (c *Collector) RecordRequest(path string, status int, latency time.Duration // 更新路径统计 if stats, ok := c.pathStats.Load(path); ok { - pathStats := stats.(*PathStats) - pathStats.requests.Add(1) + pathStats := stats.(*models.PathStats) + pathStats.Requests.Add(1) if status >= 400 { - pathStats.errors.Add(1) + pathStats.Errors.Add(1) } - pathStats.bytes.Add(bytes) - pathStats.latencySum.Add(int64(latency)) + pathStats.Bytes.Add(bytes) + pathStats.LatencySum.Add(int64(latency)) } else { - newStats := &PathStats{} - newStats.requests.Add(1) + newStats := &models.PathStats{} + newStats.Requests.Add(1) if status >= 400 { - newStats.errors.Add(1) + newStats.Errors.Add(1) } - newStats.bytes.Add(bytes) - newStats.latencySum.Add(int64(latency)) + newStats.Bytes.Add(bytes) + newStats.LatencySum.Add(int64(latency)) c.pathStats.Store(path, newStats) } // 更新引用来源统计 if referer := r.Header.Get("Referer"); referer != "" { if stats, ok := c.refererStats.Load(referer); ok { - stats.(*PathStats).requests.Add(1) + stats.(*models.PathStats).Requests.Add(1) } else { - newStats := &PathStats{} - newStats.requests.Add(1) + newStats := &models.PathStats{} + newStats.Requests.Add(1) c.refererStats.Store(referer, newStats) } } // 记录最近的请求 - log := &RequestLog{ + log := &models.RequestLog{ Time: time.Now(), Path: path, Status: status, @@ -117,9 +169,32 @@ func (c *Collector) RecordRequest(path string, status int, latency time.Duration c.recentRequests.Unlock() c.latencySum.Add(int64(latency)) + + // 更新错误统计 + if status >= 400 { + c.monitor.RecordError() + } + c.monitor.RecordRequest() + + // 检查延迟 + c.monitor.CheckLatency(latency, bytes) } func (c *Collector) GetStats() map[string]interface{} { + stats := c.statsPool.Get().(map[string]interface{}) + defer func() { + // 清空map并放回池中 + for k := range stats { + delete(stats, k) + } + c.statsPool.Put(stats) + }() + + // 先查缓存 + if stats, ok := c.cache.Get("stats"); ok { + return stats.(map[string]interface{}) + } + var m runtime.MemStats runtime.ReadMemStats(&m) @@ -129,25 +204,25 @@ func (c *Collector) GetStats() map[string]interface{} { // 获取状态码统计 statusStats := make(map[string]int64) - for i, v := range c.statusStats { - statusStats[fmt.Sprintf("%dxx", i+1)] = v.Load() + for i := range c.statusStats { + statusStats[fmt.Sprintf("%dxx", i+1)] = c.statusStats[i].Load() } // 获取Top 10路径统计 - var pathMetrics []PathMetrics - var allPaths []PathMetrics + var pathMetrics []models.PathMetrics + var allPaths []models.PathMetrics c.pathStats.Range(func(key, value interface{}) bool { - stats := value.(*PathStats) - if stats.requests.Load() == 0 { + stats := value.(*models.PathStats) + if stats.Requests.Load() == 0 { return true } - allPaths = append(allPaths, PathMetrics{ + allPaths = append(allPaths, models.PathMetrics{ Path: key.(string), - RequestCount: stats.requests.Load(), - ErrorCount: stats.errors.Load(), - AvgLatency: FormatDuration(time.Duration(stats.latencySum.Load() / stats.requests.Load())), - BytesTransferred: stats.bytes.Load(), + RequestCount: stats.Requests.Load(), + ErrorCount: stats.Errors.Load(), + AvgLatency: FormatDuration(time.Duration(stats.LatencySum.Load() / stats.Requests.Load())), + BytesTransferred: stats.Bytes.Load(), }) return true }) @@ -165,16 +240,16 @@ func (c *Collector) GetStats() map[string]interface{} { } // 获取Top 10引用来源 - var refererMetrics []PathMetrics - var allReferers []PathMetrics + var refererMetrics []models.PathMetrics + var allReferers []models.PathMetrics c.refererStats.Range(func(key, value interface{}) bool { - stats := value.(*PathStats) - if stats.requests.Load() == 0 { + stats := value.(*models.PathStats) + if stats.Requests.Load() == 0 { return true } - allReferers = append(allReferers, PathMetrics{ + allReferers = append(allReferers, models.PathMetrics{ Path: key.(string), - RequestCount: stats.requests.Load(), + RequestCount: stats.Requests.Load(), }) return true }) @@ -191,7 +266,7 @@ func (c *Collector) GetStats() map[string]interface{} { refererMetrics = allReferers } - return map[string]interface{}{ + result := map[string]interface{}{ "uptime": uptime.String(), "active_requests": atomic.LoadInt64(&c.activeRequests), "total_requests": totalRequests, @@ -212,10 +287,22 @@ func (c *Collector) GetStats() map[string]interface{} { "recent_requests": c.getRecentRequests(), "top_referers": refererMetrics, } + + for k, v := range result { + stats[k] = v + } + + // 检查告警 + c.monitor.CheckMetrics(stats) + + // 写入缓存 + c.cache.Set("stats", stats) + + return stats } -func (c *Collector) getRecentRequests() []RequestLog { - var recentReqs []RequestLog +func (c *Collector) getRecentRequests() []models.RequestLog { + var recentReqs []models.RequestLog c.recentRequests.RLock() defer c.recentRequests.RUnlock() @@ -262,3 +349,7 @@ func Max(a, b float64) float64 { } return b } + +func (c *Collector) GetDB() *models.MetricsDB { + return c.db +} diff --git a/internal/metrics/types.go b/internal/metrics/types.go index 9c4b9e4..7742ec2 100644 --- a/internal/metrics/types.go +++ b/internal/metrics/types.go @@ -17,17 +17,8 @@ type RequestLog struct { // PathStats 记录路径统计信息 type PathStats struct { - requests atomic.Int64 - errors atomic.Int64 - bytes atomic.Int64 - latencySum atomic.Int64 -} - -// PathMetrics 用于API返回的路径统计信息 -type PathMetrics struct { - Path string `json:"path"` - RequestCount int64 `json:"request_count"` - ErrorCount int64 `json:"error_count"` - AvgLatency string `json:"avg_latency"` - BytesTransferred int64 `json:"bytes_transferred"` + Requests atomic.Int64 + Errors atomic.Int64 + Bytes atomic.Int64 + LatencySum atomic.Int64 } diff --git a/internal/models/metrics.go b/internal/models/metrics.go new file mode 100644 index 0000000..364ac5c --- /dev/null +++ b/internal/models/metrics.go @@ -0,0 +1,168 @@ +package models + +import ( + "database/sql" + "sync/atomic" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +type RequestLog struct { + Time time.Time + Path string + Status int + Latency time.Duration + BytesSent int64 + ClientIP string +} + +type PathStats struct { + Requests atomic.Int64 + Errors atomic.Int64 + Bytes atomic.Int64 + LatencySum atomic.Int64 +} + +type HistoricalMetrics struct { + Timestamp string `json:"timestamp"` + TotalRequests int64 `json:"total_requests"` + TotalErrors int64 `json:"total_errors"` + TotalBytes int64 `json:"total_bytes"` + ErrorRate float64 `json:"error_rate"` + AvgLatency int64 `json:"avg_latency"` +} + +type PathMetrics struct { + Path string `json:"path"` + RequestCount int64 `json:"request_count"` + ErrorCount int64 `json:"error_count"` + AvgLatency string `json:"avg_latency"` + BytesTransferred int64 `json:"bytes_transferred"` +} + +type MetricsDB struct { + DB *sql.DB +} + +func NewMetricsDB(dbPath string) (*MetricsDB, error) { + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + return nil, err + } + return &MetricsDB{DB: db}, nil +} + +func (db *MetricsDB) SaveMetrics(stats map[string]interface{}) error { + tx, err := db.DB.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + // 保存基础指标 + _, err = tx.Exec(` + INSERT INTO metrics_history ( + total_requests, total_errors, total_bytes, avg_latency + ) VALUES (?, ?, ?, ?)`, + stats["total_requests"], stats["total_errors"], + stats["total_bytes"], stats["avg_latency"], + ) + if err != nil { + return err + } + + // 保存状态码统计 + statusStats := stats["status_code_stats"].(map[string]int64) + stmt, err := tx.Prepare(` + INSERT INTO status_stats (status_group, count) + VALUES (?, ?) + `) + if err != nil { + return err + } + defer stmt.Close() + + for group, count := range statusStats { + if _, err := stmt.Exec(group, count); err != nil { + return err + } + } + + // 保存路径统计 + pathStats := stats["top_paths"].([]PathMetrics) + stmt, err = tx.Prepare(` + INSERT INTO path_stats ( + path, requests, errors, bytes, avg_latency + ) VALUES (?, ?, ?, ?, ?) + `) + if err != nil { + return err + } + + for _, p := range pathStats { + if _, err := stmt.Exec( + p.Path, p.RequestCount, p.ErrorCount, + p.BytesTransferred, p.AvgLatency, + ); err != nil { + return err + } + } + + return tx.Commit() +} + +func (db *MetricsDB) Close() error { + return db.DB.Close() +} + +func (db *MetricsDB) GetRecentMetrics(hours int) ([]HistoricalMetrics, error) { + var interval string + if hours <= 24 { + interval = "%Y-%m-%d %H:%M:00" + } else if hours <= 168 { + interval = "%Y-%m-%d %H:00:00" + } else { + interval = "%Y-%m-%d 00:00:00" + } + + rows, err := db.DB.Query(` + WITH grouped_metrics AS ( + SELECT + strftime(?1, timestamp) as group_time, + SUM(total_requests) as total_requests, + SUM(total_errors) as total_errors, + SUM(total_bytes) as total_bytes, + AVG(avg_latency) as avg_latency + FROM metrics_history + WHERE timestamp >= datetime('now', '-' || ?2 || ' hours') + GROUP BY group_time + ORDER BY group_time DESC + ) + SELECT * FROM grouped_metrics + `, interval, hours) + if err != nil { + return nil, err + } + defer rows.Close() + + var metrics []HistoricalMetrics + for rows.Next() { + var m HistoricalMetrics + err := rows.Scan( + &m.Timestamp, + &m.TotalRequests, + &m.TotalErrors, + &m.TotalBytes, + &m.AvgLatency, + ) + if err != nil { + return nil, err + } + if m.TotalRequests > 0 { + m.ErrorRate = float64(m.TotalErrors) / float64(m.TotalRequests) + } + metrics = append(metrics, m) + } + return metrics, rows.Err() +} diff --git a/internal/monitor/feishu.go b/internal/monitor/feishu.go new file mode 100644 index 0000000..e945d24 --- /dev/null +++ b/internal/monitor/feishu.go @@ -0,0 +1,81 @@ +package monitor + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "sync" + "time" +) + +type FeishuHandler struct { + webhookURL string + client *http.Client + cardPool sync.Pool +} + +func NewFeishuHandler(webhookURL string) *FeishuHandler { + h := &FeishuHandler{ + webhookURL: webhookURL, + client: &http.Client{ + Timeout: 5 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: 90 * time.Second, + }, + }, + } + h.cardPool = sync.Pool{ + New: func() interface{} { + return &FeishuCard{} + }, + } + return h +} + +type FeishuCard struct { + MsgType string `json:"msg_type"` + Card struct { + Header struct { + Title struct { + Content string `json:"content"` + Tag string `json:"tag"` + } `json:"title"` + } `json:"header"` + Elements []interface{} `json:"elements"` + } `json:"card"` +} + +func (h *FeishuHandler) HandleAlert(alert Alert) { + card := h.cardPool.Get().(*FeishuCard) + + // 设置标题 + card.Card.Header.Title.Tag = "plain_text" + card.Card.Header.Title.Content = fmt.Sprintf("[%s] 监控告警", alert.Level) + + // 添加告警内容 + content := map[string]interface{}{ + "tag": "div", + "text": map[string]interface{}{ + "content": fmt.Sprintf("**告警时间**: %s\n**告警内容**: %s", + alert.Time.Format("2006-01-02 15:04:05"), + alert.Message), + "tag": "lark_md", + }, + } + + card.Card.Elements = []interface{}{content} + + // 发送请求 + payload, _ := json.Marshal(card) + resp, err := h.client.Post(h.webhookURL, "application/json", bytes.NewBuffer(payload)) + if err != nil { + fmt.Printf("Failed to send Feishu alert: %v\n", err) + return + } + defer resp.Body.Close() + + h.cardPool.Put(card) +} diff --git a/internal/monitor/monitor.go b/internal/monitor/monitor.go new file mode 100644 index 0000000..b207606 --- /dev/null +++ b/internal/monitor/monitor.go @@ -0,0 +1,256 @@ +package monitor + +import ( + "fmt" + "log" + "proxy-go/internal/constants" + "sync" + "sync/atomic" + "time" +) + +type AlertLevel string + +const ( + AlertLevelError AlertLevel = "ERROR" + AlertLevelWarn AlertLevel = "WARN" + AlertLevelInfo AlertLevel = "INFO" +) + +type Alert struct { + Level AlertLevel + Message string + Time time.Time +} + +type AlertHandler interface { + HandleAlert(alert Alert) +} + +// 日志告警处理器 +type LogAlertHandler struct { + logger *log.Logger +} + +type ErrorStats struct { + totalRequests atomic.Int64 + errorRequests atomic.Int64 + timestamp time.Time +} + +type TransferStats struct { + bytes atomic.Int64 + duration atomic.Int64 + timestamp time.Time +} + +type Monitor struct { + alerts chan Alert + handlers []AlertHandler + dedup sync.Map + errorWindow [12]ErrorStats // 5分钟一个窗口,保存最近1小时 + currentWindow atomic.Int32 + transferWindow [12]TransferStats // 5分钟一个窗口,保存最近1小时 + currentTWindow atomic.Int32 +} + +func NewMonitor() *Monitor { + m := &Monitor{ + alerts: make(chan Alert, 100), + handlers: make([]AlertHandler, 0), + } + + // 初始化第一个窗口 + m.errorWindow[0] = ErrorStats{timestamp: time.Now()} + m.transferWindow[0] = TransferStats{timestamp: time.Now()} + + // 添加默认的日志处理器 + m.AddHandler(&LogAlertHandler{ + logger: log.New(log.Writer(), "[ALERT] ", log.LstdFlags), + }) + + // 启动告警处理 + go m.processAlerts() + + // 启动窗口清理 + go m.cleanupWindows() + + return m +} + +func (m *Monitor) AddHandler(handler AlertHandler) { + m.handlers = append(m.handlers, handler) +} + +func (m *Monitor) processAlerts() { + for alert := range m.alerts { + // 检查是否在去重时间窗口内 + key := fmt.Sprintf("%s:%s", alert.Level, alert.Message) + if _, ok := m.dedup.LoadOrStore(key, time.Now()); ok { + continue + } + + for _, handler := range m.handlers { + handler.HandleAlert(alert) + } + } +} + +func (m *Monitor) CheckMetrics(stats map[string]interface{}) { + currentIdx := int(m.currentWindow.Load()) + window := &m.errorWindow[currentIdx] + + if time.Since(window.timestamp) >= constants.AlertWindowInterval { + // 轮转到下一个窗口 + nextIdx := (currentIdx + 1) % constants.AlertWindowSize + m.errorWindow[nextIdx] = ErrorStats{timestamp: time.Now()} + m.currentWindow.Store(int32(nextIdx)) + } + + var recentErrors, recentRequests int64 + now := time.Now() + for i := 0; i < constants.AlertWindowSize; i++ { + idx := (currentIdx - i + constants.AlertWindowSize) % constants.AlertWindowSize + w := &m.errorWindow[idx] + + if now.Sub(w.timestamp) <= constants.AlertDedupeWindow { + recentErrors += w.errorRequests.Load() + recentRequests += w.totalRequests.Load() + } + } + + // 检查错误率 + if recentRequests >= constants.MinRequestsForAlert { + errorRate := float64(recentErrors) / float64(recentRequests) + if errorRate > constants.ErrorRateThreshold { + m.alerts <- Alert{ + Level: AlertLevelError, + Message: fmt.Sprintf("最近%d分钟内错误率过高: %.2f%% (错误请求: %d, 总请求: %d)", + int(constants.AlertDedupeWindow.Minutes()), + errorRate*100, recentErrors, recentRequests), + Time: time.Now(), + } + } + } +} + +func (m *Monitor) CheckLatency(latency time.Duration, bytes int64) { + // 更新传输速率窗口 + currentIdx := int(m.currentTWindow.Load()) + window := &m.transferWindow[currentIdx] + + if time.Since(window.timestamp) >= constants.AlertWindowInterval { + // 轮转到下一个窗口 + nextIdx := (currentIdx + 1) % constants.AlertWindowSize + m.transferWindow[nextIdx] = TransferStats{timestamp: time.Now()} + m.currentTWindow.Store(int32(nextIdx)) + currentIdx = nextIdx + window = &m.transferWindow[currentIdx] + } + + window.bytes.Add(bytes) + window.duration.Add(int64(latency)) + + // 计算最近15分钟的平均传输速率 + var totalBytes, totalDuration int64 + now := time.Now() + for i := 0; i < constants.AlertWindowSize; i++ { + idx := (currentIdx - i + constants.AlertWindowSize) % constants.AlertWindowSize + w := &m.transferWindow[idx] + + if now.Sub(w.timestamp) <= constants.AlertDedupeWindow { + totalBytes += w.bytes.Load() + + totalDuration += w.duration.Load() + } + } + + if totalDuration > 0 { + avgRate := float64(totalBytes) / (float64(totalDuration) / float64(time.Second)) + + // 根据文件大小计算最小速率要求 + var ( + fileSize int64 + maxLatency time.Duration + ) + switch { + case bytes < constants.SmallFileSize: + fileSize = constants.SmallFileSize + maxLatency = constants.SmallFileLatency + case bytes < constants.MediumFileSize: + fileSize = constants.MediumFileSize + maxLatency = constants.MediumFileLatency + case bytes < constants.LargeFileSize: + fileSize = constants.LargeFileSize + maxLatency = constants.LargeFileLatency + default: + fileSize = bytes + maxLatency = constants.HugeFileLatency + } + + // 计算最小速率 = 文件大小 / 最大允许延迟 + minRate := float64(fileSize) / maxLatency.Seconds() + + // 只有当15分钟内的平均传输速率低于阈值时才告警 + if avgRate < minRate { + m.alerts <- Alert{ + Level: AlertLevelWarn, + Message: fmt.Sprintf( + "最近%d分钟内平均传输速率过低: %.2f MB/s (最低要求: %.2f MB/s, 基准文件大小: %s, 最大延迟: %s)", + int(constants.AlertDedupeWindow.Minutes()), + avgRate/float64(constants.MB), + minRate/float64(constants.MB), + formatBytes(fileSize), + maxLatency, + ), + Time: time.Now(), + } + } + } +} + +// 日志处理器实现 +func (h *LogAlertHandler) HandleAlert(alert Alert) { + h.logger.Printf("[%s] %s", alert.Level, alert.Message) +} + +func (m *Monitor) RecordRequest() { + currentIdx := int(m.currentWindow.Load()) + window := &m.errorWindow[currentIdx] + window.totalRequests.Add(1) +} + +func (m *Monitor) RecordError() { + currentIdx := int(m.currentWindow.Load()) + window := &m.errorWindow[currentIdx] + window.errorRequests.Add(1) +} + +// 格式化字节大小 +func formatBytes(bytes int64) string { + switch { + case bytes >= constants.MB: + return fmt.Sprintf("%.2f MB", float64(bytes)/float64(constants.MB)) + case bytes >= constants.KB: + return fmt.Sprintf("%.2f KB", float64(bytes)/float64(constants.KB)) + default: + return fmt.Sprintf("%d Bytes", bytes) + } +} + +// 添加窗口清理 +func (m *Monitor) cleanupWindows() { + ticker := time.NewTicker(time.Minute) + for range ticker.C { + now := time.Now() + // 清理过期的去重记录 + m.dedup.Range(func(key, value interface{}) bool { + if timestamp, ok := value.(time.Time); ok { + if now.Sub(timestamp) > constants.AlertDedupeWindow { + m.dedup.Delete(key) + } + } + return true + }) + } +} diff --git a/internal/storage/db.go b/internal/storage/db.go new file mode 100644 index 0000000..96e895d --- /dev/null +++ b/internal/storage/db.go @@ -0,0 +1,125 @@ +package storage + +import ( + "database/sql" + "log" + + _ "github.com/mattn/go-sqlite3" +) + +func InitDB(db *sql.DB) error { + // 优化SQLite配置 + _, err := db.Exec(` + PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + PRAGMA cache_size = 1000000; + PRAGMA temp_store = MEMORY; + `) + if err != nil { + return err + } + + // 创建表 + if err := initTables(db); err != nil { + return err + } + + // 启动定期清理 + go cleanupRoutine(db) + + return nil +} + +func initTables(db *sql.DB) error { + // 基础指标表 + _, err := db.Exec(` + CREATE TABLE IF NOT EXISTS metrics_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + total_requests INTEGER, + total_errors INTEGER, + total_bytes INTEGER, + avg_latency INTEGER + ) + `) + if err != nil { + return err + } + + // 状态码统计表 + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS status_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + status_group TEXT, + count INTEGER + ) + `) + if err != nil { + return err + } + + // 路径统计表 + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS path_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + path TEXT, + requests INTEGER, + errors INTEGER, + bytes INTEGER, + avg_latency INTEGER + ) + `) + if err != nil { + return err + } + + // 添加索引 + _, err = db.Exec(` + CREATE INDEX IF NOT EXISTS idx_timestamp ON metrics_history(timestamp); + CREATE INDEX IF NOT EXISTS idx_path ON path_stats(path); + `) + return err +} + +func cleanupRoutine(db *sql.DB) { + // 批量删除而不是单条删除 + tx, err := db.Begin() + if err != nil { + log.Printf("Error starting transaction: %v", err) + return + } + defer tx.Rollback() + + // 保留90天的数据 + _, err = tx.Exec(` + DELETE FROM metrics_history + WHERE timestamp < datetime('now', '-90 days') + `) + if err != nil { + log.Printf("Error cleaning old data: %v", err) + } + + // 清理状态码统计 + _, err = tx.Exec(` + DELETE FROM status_stats + WHERE timestamp < datetime('now', '-90 days') + `) + if err != nil { + log.Printf("Error cleaning old data: %v", err) + } + + // 清理路径统计 + _, err = tx.Exec(` + DELETE FROM path_stats + WHERE timestamp < datetime('now', '-90 days') + `) + if err != nil { + log.Printf("Error cleaning old data: %v", err) + } + + if err := tx.Commit(); err != nil { + log.Printf("Error committing transaction: %v", err) + } +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index b5af158..082852b 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -1,13 +1,47 @@ package utils import ( + "context" "fmt" + "log" "net" "net/http" "path/filepath" + "proxy-go/internal/config" "strings" + "sync" + "time" ) +// 文件大小缓存项 +type fileSizeCache struct { + size int64 + timestamp time.Time +} + +var ( + // 文件大小缓存,过期时间5分钟 + sizeCache sync.Map + cacheTTL = 5 * time.Minute + maxCacheSize = 10000 // 最大缓存条目数 +) + +// 清理过期缓存 +func init() { + go func() { + ticker := time.NewTicker(time.Minute) + for range ticker.C { + now := time.Now() + sizeCache.Range(func(key, value interface{}) bool { + if cache := value.(fileSizeCache); now.Sub(cache.timestamp) > cacheTTL { + sizeCache.Delete(key) + } + return true + }) + } + }() +} + func GetClientIP(r *http.Request) string { if ip := r.Header.Get("X-Real-IP"); ip != "" { return ip @@ -59,3 +93,105 @@ func IsImageRequest(path string) bool { } return imageExts[ext] } + +// GetFileSize 发送HEAD请求获取文件大小 +func GetFileSize(client *http.Client, url string) (int64, error) { + // 先查缓存 + if cache, ok := sizeCache.Load(url); ok { + cacheItem := cache.(fileSizeCache) + if time.Since(cacheItem.timestamp) < cacheTTL { + return cacheItem.size, nil + } + sizeCache.Delete(url) + } + + req, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return 0, err + } + + // 设置超时上下文 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + req = req.WithContext(ctx) + + resp, err := client.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + // 缓存结果 + if resp.ContentLength > 0 { + sizeCache.Store(url, fileSizeCache{ + size: resp.ContentLength, + timestamp: time.Now(), + }) + } + + return resp.ContentLength, nil +} + +// GetTargetURL 根据路径和配置决定目标URL +func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathConfig, path string) string { + // 默认使用默认目标 + targetBase := pathConfig.DefaultTarget + + // 如果没有设置阈值,使用默认值 200KB + threshold := pathConfig.SizeThreshold + if threshold <= 0 { + threshold = 200 * 1024 + } + + // 检查文件扩展名 + if pathConfig.ExtensionMap != nil { + ext := strings.ToLower(filepath.Ext(path)) + if ext != "" { + ext = ext[1:] // 移除开头的点 + // 先检查是否在扩展名映射中 + if altTarget, exists := pathConfig.GetExtensionTarget(ext); exists { + // 检查文件大小 + contentLength := r.ContentLength + if contentLength <= 0 { + // 如果无法获取 Content-Length,尝试发送 HEAD 请求 + if size, err := GetFileSize(client, pathConfig.DefaultTarget+path); err == nil { + contentLength = size + log.Printf("[FileSize] Path: %s, Size: %s (from %s)", + path, FormatBytes(contentLength), + func() string { + if isCacheHit(pathConfig.DefaultTarget + path) { + return "cache" + } + return "HEAD request" + }()) + } else { + log.Printf("[FileSize] Failed to get size for %s: %v", path, err) + } + } else { + log.Printf("[FileSize] Path: %s, Size: %s (from Content-Length)", + path, FormatBytes(contentLength)) + } + + // 只有当文件大于阈值时才使用扩展名映射的目标 + if contentLength > threshold { + log.Printf("[Route] %s -> %s (size: %s > %s)", + path, altTarget, FormatBytes(contentLength), FormatBytes(threshold)) + targetBase = altTarget + } else { + log.Printf("[Route] %s -> %s (size: %s <= %s)", + path, targetBase, FormatBytes(contentLength), FormatBytes(threshold)) + } + } + } + } + + return targetBase +} + +// 检查是否命中缓存 +func isCacheHit(url string) bool { + if cache, ok := sizeCache.Load(url); ok { + return time.Since(cache.(fileSizeCache).timestamp) < cacheTTL + } + return false +} diff --git a/main.go b/main.go index 82d1c8e..4b3f5cd 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,9 @@ import ( "os/signal" "proxy-go/internal/compression" "proxy-go/internal/config" + "proxy-go/internal/constants" "proxy-go/internal/handler" + "proxy-go/internal/metrics" "proxy-go/internal/middleware" "strings" "syscall" @@ -20,6 +22,14 @@ func main() { log.Fatal("Error loading config:", err) } + // 更新常量配置 + constants.UpdateFromConfig(cfg) + + // 初始化指标收集器 + if err := metrics.InitCollector("data/metrics.db", cfg); err != nil { + log.Fatal("Error initializing metrics collector:", err) + } + // 创建压缩管理器 compManager := compression.NewManager(compression.Config{ Gzip: compression.CompressorConfig(cfg.Compression.Gzip), @@ -79,6 +89,9 @@ func main() { case "/metrics/dashboard": proxyHandler.MetricsDashboardHandler(w, r) return + case "/metrics/history": + proxyHandler.AuthMiddleware(proxyHandler.MetricsHistoryHandler)(w, r) + return } // 遍历所有处理器