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 = ` +