diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..c9ba8cf --- /dev/null +++ b/config/config.go @@ -0,0 +1,676 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/ctwj/urldb/db/entity" + "github.com/ctwj/urldb/db/repo" + "github.com/ctwj/urldb/utils" +) + +// ConfigManager 统一配置管理器 +type ConfigManager struct { + repo *repo.RepositoryManager + + // 内存缓存 + cache map[string]*ConfigItem + cacheMutex sync.RWMutex + cacheOnce sync.Once + + // 配置更新通知 + configUpdateCh chan string + watchers []chan string + watcherMutex sync.Mutex + + // 加载时间 + lastLoadTime time.Time +} + +// ConfigItem 配置项结构 +type ConfigItem struct { + Key string `json:"key"` + Value string `json:"value"` + Type string `json:"type"` + UpdatedAt time.Time `json:"updated_at"` + Group string `json:"group"` // 配置分组 + Category string `json:"category"` // 配置分类 + IsSensitive bool `json:"is_sensitive"` // 是否是敏感信息 +} + +// ConfigGroup 配置分组 +type ConfigGroup string + +const ( + GroupDatabase ConfigGroup = "database" + GroupServer ConfigGroup = "server" + GroupSecurity ConfigGroup = "security" + GroupSearch ConfigGroup = "search" + GroupTelegram ConfigGroup = "telegram" + GroupCache ConfigGroup = "cache" + GroupMeilisearch ConfigGroup = "meilisearch" + GroupSEO ConfigGroup = "seo" + GroupAutoProcess ConfigGroup = "auto_process" + GroupOther ConfigGroup = "other" +) + +// NewConfigManager 创建配置管理器 +func NewConfigManager(repoManager *repo.RepositoryManager) *ConfigManager { + cm := &ConfigManager{ + repo: repoManager, + cache: make(map[string]*ConfigItem), + configUpdateCh: make(chan string, 100), // 缓冲通道防止阻塞 + } + + // 启动配置更新监听器 + go cm.startConfigUpdateListener() + + return cm +} + +// startConfigUpdateListener 启动配置更新监听器 +func (cm *ConfigManager) startConfigUpdateListener() { + for key := range cm.configUpdateCh { + cm.notifyWatchers(key) + } +} + +// notifyWatchers 通知所有监听器配置已更新 +func (cm *ConfigManager) notifyWatchers(key string) { + cm.watcherMutex.Lock() + defer cm.watcherMutex.Unlock() + + for _, watcher := range cm.watchers { + select { + case watcher <- key: + default: + // 如果通道阻塞,跳过该监听器 + utils.Warn("配置监听器通道阻塞,跳过通知: %s", key) + } + } +} + +// AddConfigWatcher 添加配置变更监听器 +func (cm *ConfigManager) AddConfigWatcher() chan string { + cm.watcherMutex.Lock() + defer cm.watcherMutex.Unlock() + + watcher := make(chan string, 10) // 为每个监听器创建缓冲通道 + cm.watchers = append(cm.watchers, watcher) + return watcher +} + +// GetConfig 获取配置项 +func (cm *ConfigManager) GetConfig(key string) (*ConfigItem, error) { + // 先尝试从内存缓存获取 + item, exists := cm.getCachedConfig(key) + if exists { + return item, nil + } + + // 如果缓存中没有,从数据库获取 + config, err := cm.repo.SystemConfigRepository.FindByKey(key) + if err != nil { + return nil, err + } + + // 将数据库配置转换为ConfigItem并缓存 + item = &ConfigItem{ + Key: config.Key, + Value: config.Value, + Type: config.Type, + UpdatedAt: time.Now(), + } + + if group := cm.getGroupByConfigKey(key); group != "" { + item.Group = string(group) + } + + if category := cm.getCategoryByConfigKey(key); category != "" { + item.Category = category + } + + item.IsSensitive = cm.isSensitiveConfig(key) + + // 缓存配置 + cm.setCachedConfig(key, item) + + return item, nil +} + +// GetConfigValue 获取配置值 +func (cm *ConfigManager) GetConfigValue(key string) (string, error) { + item, err := cm.GetConfig(key) + if err != nil { + return "", err + } + return item.Value, nil +} + +// GetConfigBool 获取布尔值配置 +func (cm *ConfigManager) GetConfigBool(key string) (bool, error) { + value, err := cm.GetConfigValue(key) + if err != nil { + return false, err + } + + switch strings.ToLower(value) { + case "true", "1", "yes", "on": + return true, nil + case "false", "0", "no", "off", "": + return false, nil + default: + return false, fmt.Errorf("无法将配置值 '%s' 转换为布尔值", value) + } +} + +// GetConfigInt 获取整数值配置 +func (cm *ConfigManager) GetConfigInt(key string) (int, error) { + value, err := cm.GetConfigValue(key) + if err != nil { + return 0, err + } + + return strconv.Atoi(value) +} + +// GetConfigInt64 获取64位整数值配置 +func (cm *ConfigManager) GetConfigInt64(key string) (int64, error) { + value, err := cm.GetConfigValue(key) + if err != nil { + return 0, err + } + + return strconv.ParseInt(value, 10, 64) +} + +// GetConfigFloat64 获取浮点数配置 +func (cm *ConfigManager) GetConfigFloat64(key string) (float64, error) { + value, err := cm.GetConfigValue(key) + if err != nil { + return 0, err + } + + return strconv.ParseFloat(value, 64) +} + +// SetConfig 设置配置值 +func (cm *ConfigManager) SetConfig(key, value string) error { + // 更新数据库 + config := &entity.SystemConfig{ + Key: key, + Value: value, + Type: "string", // 默认类型,实际类型应该从现有配置中获取 + } + + // 获取现有配置以确定类型 + existing, err := cm.repo.SystemConfigRepository.FindByKey(key) + if err == nil { + config.Type = existing.Type + } else { + // 如果配置不存在,尝试从默认配置中获取类型 + config.Type = cm.getDefaultConfigType(key) + } + + // 保存到数据库 + err = cm.repo.SystemConfigRepository.UpsertConfigs([]entity.SystemConfig{*config}) + if err != nil { + return fmt.Errorf("保存配置失败: %v", err) + } + + // 更新缓存 + item := &ConfigItem{ + Key: config.Key, + Value: config.Value, + Type: config.Type, + UpdatedAt: time.Now(), + } + + if group := cm.getGroupByConfigKey(key); group != "" { + item.Group = string(group) + } + + if category := cm.getCategoryByConfigKey(key); category != "" { + item.Category = category + } + + item.IsSensitive = cm.isSensitiveConfig(key) + + cm.setCachedConfig(key, item) + + // 发送更新通知 + cm.configUpdateCh <- key + + utils.Info("配置已更新: %s = %s", key, value) + + return nil +} + +// SetConfigWithType 设置配置值(指定类型) +func (cm *ConfigManager) SetConfigWithType(key, value, configType string) error { + config := &entity.SystemConfig{ + Key: key, + Value: value, + Type: configType, + } + + err := cm.repo.SystemConfigRepository.UpsertConfigs([]entity.SystemConfig{*config}) + if err != nil { + return fmt.Errorf("保存配置失败: %v", err) + } + + // 更新缓存 + item := &ConfigItem{ + Key: config.Key, + Value: config.Value, + Type: config.Type, + UpdatedAt: time.Now(), + } + + if group := cm.getGroupByConfigKey(key); group != "" { + item.Group = string(group) + } + + if category := cm.getCategoryByConfigKey(key); category != "" { + item.Category = category + } + + item.IsSensitive = cm.isSensitiveConfig(key) + + cm.setCachedConfig(key, item) + + // 发送更新通知 + cm.configUpdateCh <- key + + utils.Info("配置已更新: %s = %s (type: %s)", key, value, configType) + + return nil +} + +// getGroupByConfigKey 根据配置键获取分组 +func (cm *ConfigManager) getGroupByConfigKey(key string) ConfigGroup { + switch { + case strings.HasPrefix(key, "database_"), strings.HasPrefix(key, "db_"): + return GroupDatabase + case strings.HasPrefix(key, "server_"), strings.HasPrefix(key, "port"), strings.HasPrefix(key, "host"): + return GroupServer + case strings.HasPrefix(key, "api_"), strings.HasPrefix(key, "jwt_"), strings.HasPrefix(key, "password"): + return GroupSecurity + case strings.Contains(key, "meilisearch"): + return GroupMeilisearch + case strings.Contains(key, "telegram"): + return GroupTelegram + case strings.Contains(key, "cache"), strings.Contains(key, "redis"): + return GroupCache + case strings.Contains(key, "seo"), strings.Contains(key, "title"), strings.Contains(key, "keyword"): + return GroupSEO + case strings.Contains(key, "auto_"): + return GroupAutoProcess + case strings.Contains(key, "forbidden"), strings.Contains(key, "ad_"): + return GroupOther + default: + return GroupOther + } +} + +// getCategoryByConfigKey 根据配置键获取分类 +func (cm *ConfigManager) getCategoryByConfigKey(key string) string { + switch { + case key == entity.ConfigKeySiteTitle || key == entity.ConfigKeySiteDescription: + return "basic_info" + case key == entity.ConfigKeyKeywords || key == entity.ConfigKeyAuthor: + return "seo" + case key == entity.ConfigKeyAutoProcessReadyResources || key == entity.ConfigKeyAutoProcessInterval: + return "auto_process" + case key == entity.ConfigKeyAutoTransferEnabled || key == entity.ConfigKeyAutoTransferLimitDays: + return "auto_transfer" + case key == entity.ConfigKeyMeilisearchEnabled || key == entity.ConfigKeyMeilisearchHost: + return "search" + case key == entity.ConfigKeyTelegramBotEnabled || key == entity.ConfigKeyTelegramBotApiKey: + return "telegram" + case key == entity.ConfigKeyMaintenanceMode || key == entity.ConfigKeyEnableRegister: + return "system" + case key == entity.ConfigKeyForbiddenWords || key == entity.ConfigKeyAdKeywords: + return "filtering" + default: + return "other" + } +} + +// isSensitiveConfig 判断是否是敏感配置 +func (cm *ConfigManager) isSensitiveConfig(key string) bool { + switch key { + case entity.ConfigKeyApiToken, + entity.ConfigKeyMeilisearchMasterKey, + entity.ConfigKeyTelegramBotApiKey, + entity.ConfigKeyTelegramProxyUsername, + entity.ConfigKeyTelegramProxyPassword: + return true + default: + return strings.Contains(strings.ToLower(key), "password") || + strings.Contains(strings.ToLower(key), "secret") || + strings.Contains(strings.ToLower(key), "key") || + strings.Contains(strings.ToLower(key), "token") + } +} + +// getDefaultConfigType 获取默认配置类型 +func (cm *ConfigManager) getDefaultConfigType(key string) string { + switch key { + case entity.ConfigKeyAutoProcessReadyResources, + entity.ConfigKeyAutoTransferEnabled, + entity.ConfigKeyAutoFetchHotDramaEnabled, + entity.ConfigKeyMaintenanceMode, + entity.ConfigKeyEnableRegister, + entity.ConfigKeyMeilisearchEnabled, + entity.ConfigKeyTelegramBotEnabled: + return entity.ConfigTypeBool + case entity.ConfigKeyAutoProcessInterval, + entity.ConfigKeyAutoTransferLimitDays, + entity.ConfigKeyAutoTransferMinSpace, + entity.ConfigKeyPageSize: + return entity.ConfigTypeInt + case entity.ConfigKeyAnnouncements: + return entity.ConfigTypeJSON + default: + return entity.ConfigTypeString + } +} + +// LoadAllConfigs 加载所有配置到缓存 +func (cm *ConfigManager) LoadAllConfigs() error { + configs, err := cm.repo.SystemConfigRepository.FindAll() + if err != nil { + return fmt.Errorf("加载所有配置失败: %v", err) + } + + cm.cacheMutex.Lock() + defer cm.cacheMutex.Unlock() + + // 清空现有缓存 + cm.cache = make(map[string]*ConfigItem) + + // 更新缓存 + for _, config := range configs { + item := &ConfigItem{ + Key: config.Key, + Value: config.Value, + Type: config.Type, + UpdatedAt: time.Now(), // 实际应该从数据库获取 + } + + if group := cm.getGroupByConfigKey(config.Key); group != "" { + item.Group = string(group) + } + + if category := cm.getCategoryByConfigKey(config.Key); category != "" { + item.Category = category + } + + item.IsSensitive = cm.isSensitiveConfig(config.Key) + + cm.cache[config.Key] = item + } + + cm.lastLoadTime = time.Now() + + utils.Info("已加载 %d 个配置项到缓存", len(configs)) + + return nil +} + +// RefreshConfigCache 刷新配置缓存 +func (cm *ConfigManager) RefreshConfigCache() error { + return cm.LoadAllConfigs() +} + +// GetCachedConfig 获取缓存的配置 +func (cm *ConfigManager) getCachedConfig(key string) (*ConfigItem, bool) { + cm.cacheMutex.RLock() + defer cm.cacheMutex.RUnlock() + + item, exists := cm.cache[key] + return item, exists +} + +// setCachedConfig 设置缓存的配置 +func (cm *ConfigManager) setCachedConfig(key string, item *ConfigItem) { + cm.cacheMutex.Lock() + defer cm.cacheMutex.Unlock() + + cm.cache[key] = item +} + +// GetConfigByGroup 按分组获取配置 +func (cm *ConfigManager) GetConfigByGroup(group ConfigGroup) (map[string]*ConfigItem, error) { + cm.cacheMutex.RLock() + defer cm.cacheMutex.RUnlock() + + result := make(map[string]*ConfigItem) + + for key, item := range cm.cache { + if ConfigGroup(item.Group) == group { + result[key] = item + } + } + + return result, nil +} + +// GetConfigByCategory 按分类获取配置 +func (cm *ConfigManager) GetConfigByCategory(category string) (map[string]*ConfigItem, error) { + cm.cacheMutex.RLock() + defer cm.cacheMutex.RUnlock() + + result := make(map[string]*ConfigItem) + + for key, item := range cm.cache { + if item.Category == category { + result[key] = item + } + } + + return result, nil +} + +// DeleteConfig 删除配置 +func (cm *ConfigManager) DeleteConfig(key string) error { + // 先查找配置获取ID + config, err := cm.repo.SystemConfigRepository.FindByKey(key) + if err != nil { + return fmt.Errorf("查找配置失败: %v", err) + } + + // 从数据库删除 + err = cm.repo.SystemConfigRepository.Delete(config.ID) + if err != nil { + return fmt.Errorf("删除配置失败: %v", err) + } + + // 从缓存中移除 + cm.cacheMutex.Lock() + delete(cm.cache, key) + cm.cacheMutex.Unlock() + + utils.Info("配置已删除: %s", key) + + return nil +} + +// GetSensitiveConfigKeys 获取所有敏感配置键 +func (cm *ConfigManager) GetSensitiveConfigKeys() []string { + cm.cacheMutex.RLock() + defer cm.cacheMutex.RUnlock() + + var sensitiveKeys []string + for key, item := range cm.cache { + if item.IsSensitive { + sensitiveKeys = append(sensitiveKeys, key) + } + } + + return sensitiveKeys +} + +// GetConfigWithMask 获取配置值(敏感配置会被遮蔽) +func (cm *ConfigManager) GetConfigWithMask(key string) (*ConfigItem, error) { + item, err := cm.GetConfig(key) + if err != nil { + return nil, err + } + + if item.IsSensitive { + // 创建副本并遮蔽敏感值 + maskedItem := *item + maskedItem.Value = cm.maskSensitiveValue(item.Value) + return &maskedItem, nil + } + + return item, nil +} + +// maskSensitiveValue 遮蔽敏感值 +func (cm *ConfigManager) maskSensitiveValue(value string) string { + if len(value) <= 4 { + return "****" + } + + // 保留前2个和后2个字符,中间用****替代 + return value[:2] + "****" + value[len(value)-2:] +} + +// GetConfigAsJSON 获取配置为JSON格式 +func (cm *ConfigManager) GetConfigAsJSON() ([]byte, error) { + cm.cacheMutex.RLock() + defer cm.cacheMutex.RUnlock() + + // 创建副本,敏感配置使用遮蔽值 + configMap := make(map[string]*ConfigItem) + for key, item := range cm.cache { + if item.IsSensitive { + maskedItem := *item + maskedItem.Value = cm.maskSensitiveValue(item.Value) + configMap[key] = &maskedItem + } else { + configMap[key] = item + } + } + + return json.MarshalIndent(configMap, "", " ") +} + +// GetConfigStatistics 获取配置统计信息 +func (cm *ConfigManager) GetConfigStatistics() map[string]interface{} { + cm.cacheMutex.RLock() + defer cm.cacheMutex.RUnlock() + + stats := map[string]interface{}{ + "total_configs": len(cm.cache), + "last_load_time": cm.lastLoadTime, + "cache_size_bytes": len(cm.cache) * 100, // 估算每个配置约100字节 + "groups": make(map[string]int), + "types": make(map[string]int), + "categories": make(map[string]int), + "sensitive_configs": 0, + "config_keys": make([]string, 0), + } + + groups := make(map[string]int) + types := make(map[string]int) + categories := make(map[string]int) + + for key, item := range cm.cache { + // 统计分组 + groups[item.Group]++ + + // 统计类型 + types[item.Type]++ + + // 统计分类 + categories[item.Category]++ + + // 统计敏感配置 + if item.IsSensitive { + stats["sensitive_configs"] = stats["sensitive_configs"].(int) + 1 + } + + // 添加配置键到列表 + keys := stats["config_keys"].([]string) + keys = append(keys, key) + stats["config_keys"] = keys + } + + stats["groups"] = groups + stats["types"] = types + stats["categories"] = categories + + return stats +} + +// GetEnvironmentConfig 从环境变量获取配置 +func (cm *ConfigManager) GetEnvironmentConfig(key string) (string, bool) { + value := os.Getenv(key) + if value != "" { + return value, true + } + + // 尝试使用大写版本的键 + value = os.Getenv(strings.ToUpper(key)) + if value != "" { + return value, true + } + + // 尝试使用大写带下划线的格式 + upperKey := strings.ToUpper(strings.ReplaceAll(key, ".", "_")) + value = os.Getenv(upperKey) + if value != "" { + return value, true + } + + return "", false +} + +// GetConfigWithEnvFallback 获取配置,环境变量优先 +func (cm *ConfigManager) GetConfigWithEnvFallback(configKey, envKey string) (string, error) { + // 优先从环境变量获取 + if envValue, exists := cm.GetEnvironmentConfig(envKey); exists { + return envValue, nil + } + + // 如果环境变量不存在,从数据库获取 + return cm.GetConfigValue(configKey) +} + +// GetConfigIntWithEnvFallback 获取整数配置,环境变量优先 +func (cm *ConfigManager) GetConfigIntWithEnvFallback(configKey, envKey string) (int, error) { + // 优先从环境变量获取 + if envValue, exists := cm.GetEnvironmentConfig(envKey); exists { + return strconv.Atoi(envValue) + } + + // 如果环境变量不存在,从数据库获取 + return cm.GetConfigInt(configKey) +} + +// GetConfigBoolWithEnvFallback 获取布尔配置,环境变量优先 +func (cm *ConfigManager) GetConfigBoolWithEnvFallback(configKey, envKey string) (bool, error) { + // 优先从环境变量获取 + if envValue, exists := cm.GetEnvironmentConfig(envKey); exists { + switch strings.ToLower(envValue) { + case "true", "1", "yes", "on": + return true, nil + case "false", "0", "no", "off", "": + return false, nil + default: + return false, fmt.Errorf("无法将环境变量值 '%s' 转换为布尔值", envValue) + } + } + + // 如果环境变量不存在,从数据库获取 + return cm.GetConfigBool(configKey) +} \ No newline at end of file diff --git a/config/global.go b/config/global.go new file mode 100644 index 0000000..5e1c5b8 --- /dev/null +++ b/config/global.go @@ -0,0 +1,124 @@ +package config + +import ( + "sync" +) + +var ( + globalConfigManager *ConfigManager + once sync.Once +) + +// SetGlobalConfigManager 设置全局配置管理器 +func SetGlobalConfigManager(cm *ConfigManager) { + globalConfigManager = cm +} + +// GetGlobalConfigManager 获取全局配置管理器 +func GetGlobalConfigManager() *ConfigManager { + return globalConfigManager +} + +// GetConfig 获取配置值(全局函数) +func GetConfig(key string) (*ConfigItem, error) { + if globalConfigManager == nil { + return nil, ErrConfigManagerNotInitialized + } + return globalConfigManager.GetConfig(key) +} + +// GetConfigValue 获取配置值(全局函数) +func GetConfigValue(key string) (string, error) { + if globalConfigManager == nil { + return "", ErrConfigManagerNotInitialized + } + return globalConfigManager.GetConfigValue(key) +} + +// GetConfigBool 获取布尔配置值(全局函数) +func GetConfigBool(key string) (bool, error) { + if globalConfigManager == nil { + return false, ErrConfigManagerNotInitialized + } + return globalConfigManager.GetConfigBool(key) +} + +// GetConfigInt 获取整数配置值(全局函数) +func GetConfigInt(key string) (int, error) { + if globalConfigManager == nil { + return 0, ErrConfigManagerNotInitialized + } + return globalConfigManager.GetConfigInt(key) +} + +// GetConfigInt64 获取64位整数配置值(全局函数) +func GetConfigInt64(key string) (int64, error) { + if globalConfigManager == nil { + return 0, ErrConfigManagerNotInitialized + } + return globalConfigManager.GetConfigInt64(key) +} + +// GetConfigFloat64 获取浮点数配置值(全局函数) +func GetConfigFloat64(key string) (float64, error) { + if globalConfigManager == nil { + return 0, ErrConfigManagerNotInitialized + } + return globalConfigManager.GetConfigFloat64(key) +} + +// SetConfig 设置配置值(全局函数) +func SetConfig(key, value string) error { + if globalConfigManager == nil { + return ErrConfigManagerNotInitialized + } + return globalConfigManager.SetConfig(key, value) +} + +// SetConfigWithType 设置配置值(指定类型,全局函数) +func SetConfigWithType(key, value, configType string) error { + if globalConfigManager == nil { + return ErrConfigManagerNotInitialized + } + return globalConfigManager.SetConfigWithType(key, value, configType) +} + +// GetConfigWithEnvFallback 获取配置值(环境变量优先,全局函数) +func GetConfigWithEnvFallback(configKey, envKey string) (string, error) { + if globalConfigManager == nil { + return "", ErrConfigManagerNotInitialized + } + return globalConfigManager.GetConfigWithEnvFallback(configKey, envKey) +} + +// GetConfigIntWithEnvFallback 获取整数配置值(环境变量优先,全局函数) +func GetConfigIntWithEnvFallback(configKey, envKey string) (int, error) { + if globalConfigManager == nil { + return 0, ErrConfigManagerNotInitialized + } + return globalConfigManager.GetConfigIntWithEnvFallback(configKey, envKey) +} + +// GetConfigBoolWithEnvFallback 获取布尔配置值(环境变量优先,全局函数) +func GetConfigBoolWithEnvFallback(configKey, envKey string) (bool, error) { + if globalConfigManager == nil { + return false, ErrConfigManagerNotInitialized + } + return globalConfigManager.GetConfigBoolWithEnvFallback(configKey, envKey) +} + +// ErrConfigManagerNotInitialized 配置管理器未初始化错误 +var ErrConfigManagerNotInitialized = &ConfigError{ + Code: "CONFIG_MANAGER_NOT_INITIALIZED", + Message: "配置管理器未初始化", +} + +// ConfigError 配置错误 +type ConfigError struct { + Code string + Message string +} + +func (e *ConfigError) Error() string { + return e.Message +} \ No newline at end of file diff --git a/config/sync.go b/config/sync.go new file mode 100644 index 0000000..0b69fb9 --- /dev/null +++ b/config/sync.go @@ -0,0 +1,31 @@ +package config + +import ( + "github.com/ctwj/urldb/db/repo" + "github.com/ctwj/urldb/utils" +) + +// SyncWithRepository 同步配置管理器与Repository的缓存 +func (cm *ConfigManager) SyncWithRepository(repoManager *repo.RepositoryManager) { + // 监听配置变更事件并同步缓存 + // 这是一个抽象概念,实际实现需要修改Repository接口 + + // 当配置更新时,通知Repository清理缓存 + go func() { + watcher := cm.AddConfigWatcher() + for { + select { + case key := <-watcher: + // 通知Repository层清理缓存(如果Repository支持) + utils.Debug("配置 %s 已更新,可能需要同步到Repository缓存", key) + } + } + }() +} + +// UpdateRepositoryCache 当配置管理器更新配置时,通知Repository层同步 +func (cm *ConfigManager) UpdateRepositoryCache(repoManager *repo.RepositoryManager) { + // 这个函数需要Repository支持特定的缓存清理方法 + // 由于现有Repository没有提供这样的接口,我们只能依赖数据库同步 + utils.Info("配置已通过配置管理器更新,Repository层将从数据库重新加载") +} \ No newline at end of file diff --git a/db/connection.go b/db/connection.go index d7af929..970cb74 100644 --- a/db/connection.go +++ b/db/connection.go @@ -2,7 +2,9 @@ package db import ( "fmt" + "log" "os" + "strconv" "time" "github.com/ctwj/urldb/db/entity" @@ -45,8 +47,22 @@ func InitDB() error { host, port, user, password, dbname) var err error + // 配置慢查询日志 + slowThreshold := getEnvInt("DB_SLOW_THRESHOLD_MS", 200) + logLevel := logger.Info + if os.Getenv("ENV") == "production" { + logLevel = logger.Warn + } + DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Info), + Logger: logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), + logger.Config{ + SlowThreshold: time.Duration(slowThreshold) * time.Millisecond, + LogLevel: logLevel, + Colorful: true, + }, + ), }) if err != nil { return err @@ -58,10 +74,17 @@ func InitDB() error { return err } - // 设置连接池参数 - sqlDB.SetMaxIdleConns(10) // 最大空闲连接数 - sqlDB.SetMaxOpenConns(100) // 最大打开连接数 - sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大生命周期 + // 优化数据库连接池参数 + maxOpenConns := getEnvInt("DB_MAX_OPEN_CONNS", 50) + maxIdleConns := getEnvInt("DB_MAX_IDLE_CONNS", 20) + connMaxLifetime := getEnvInt("DB_CONN_MAX_LIFETIME_MINUTES", 30) + + sqlDB.SetMaxOpenConns(maxOpenConns) // 最大打开连接数 + sqlDB.SetMaxIdleConns(maxIdleConns) // 最大空闲连接数 + sqlDB.SetConnMaxLifetime(time.Duration(connMaxLifetime) * time.Minute) // 连接最大生命周期 + + utils.Info("数据库连接池配置 - 最大连接: %d, 空闲连接: %d, 生命周期: %d分钟", + maxOpenConns, maxIdleConns, connMaxLifetime) // 检查是否需要迁移(只在开发环境或首次启动时) if shouldRunMigration() { @@ -300,3 +323,19 @@ func insertDefaultDataIfEmpty() error { utils.Info("默认数据插入完成") return nil } + +// getEnvInt 获取环境变量中的整数值,如果不存在则返回默认值 +func getEnvInt(key string, defaultValue int) int { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + + intValue, err := strconv.Atoi(value) + if err != nil { + utils.Warn("环境变量 %s 的值 '%s' 不是有效的整数,使用默认值 %d", key, value, defaultValue) + return defaultValue + } + + return intValue +} diff --git a/db/repo/pagination.go b/db/repo/pagination.go new file mode 100644 index 0000000..3fd9257 --- /dev/null +++ b/db/repo/pagination.go @@ -0,0 +1,114 @@ +package repo + +import ( + "gorm.io/gorm" +) + +// PaginationResult 分页查询结果 +type PaginationResult[T any] struct { + Data []T `json:"data"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int `json:"total_pages"` +} + +// PaginationOptions 分页查询选项 +type PaginationOptions struct { + Page int `json:"page"` + PageSize int `json:"page_size"` + OrderBy string `json:"order_by"` + OrderDir string `json:"order_dir"` // asc or desc + Preloads []string `json:"preloads"` // 需要预加载的关联 + Filters map[string]interface{} `json:"filters"` // 过滤条件 +} + +// DefaultPaginationOptions 默认分页选项 +func DefaultPaginationOptions() *PaginationOptions { + return &PaginationOptions{ + Page: 1, + PageSize: 20, + OrderBy: "id", + OrderDir: "desc", + Preloads: []string{}, + Filters: make(map[string]interface{}), + } +} + +// PaginatedQuery 通用分页查询函数 +func PaginatedQuery[T any](db *gorm.DB, options *PaginationOptions) (*PaginationResult[T], error) { + // 验证分页参数 + if options.Page < 1 { + options.Page = 1 + } + if options.PageSize < 1 || options.PageSize > 1000 { + options.PageSize = 20 + } + + // 应用预加载 + query := db.Model(new(T)) + for _, preload := range options.Preloads { + query = query.Preload(preload) + } + + // 应用过滤条件 + for key, value := range options.Filters { + // 处理特殊过滤条件 + switch key { + case "search": + // 搜索条件需要特殊处理 + if searchStr, ok := value.(string); ok && searchStr != "" { + query = query.Where("title ILIKE ? OR description ILIKE ?", "%"+searchStr+"%", "%"+searchStr+"%") + } + case "category_id": + if categoryID, ok := value.(uint); ok { + query = query.Where("category_id = ?", categoryID) + } + case "pan_id": + if panID, ok := value.(uint); ok { + query = query.Where("pan_id = ?", panID) + } + case "is_valid": + if isValid, ok := value.(bool); ok { + query = query.Where("is_valid = ?", isValid) + } + case "is_public": + if isPublic, ok := value.(bool); ok { + query = query.Where("is_public = ?", isPublic) + } + default: + // 通用过滤条件 + query = query.Where(key+" = ?", value) + } + } + + // 应用排序 + orderClause := options.OrderBy + " " + options.OrderDir + query = query.Order(orderClause) + + // 计算偏移量 + offset := (options.Page - 1) * options.PageSize + + // 获取总数 + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, err + } + + // 查询数据 + var data []T + if err := query.Offset(offset).Limit(options.PageSize).Find(&data).Error; err != nil { + return nil, err + } + + // 计算总页数 + totalPages := int((total + int64(options.PageSize) - 1) / int64(options.PageSize)) + + return &PaginationResult[T]{ + Data: data, + Total: total, + Page: options.Page, + PageSize: options.PageSize, + TotalPages: totalPages, + }, nil +} \ No newline at end of file diff --git a/db/repo/resource_repository.go b/db/repo/resource_repository.go index 6286875..28396b1 100644 --- a/db/repo/resource_repository.go +++ b/db/repo/resource_repository.go @@ -68,38 +68,21 @@ func (r *ResourceRepositoryImpl) FindWithRelations() ([]entity.Resource, error) // FindWithRelationsPaginated 分页查找包含关联关系的资源 func (r *ResourceRepositoryImpl) FindWithRelationsPaginated(page, limit int) ([]entity.Resource, int64, error) { - var resources []entity.Resource - var total int64 - - offset := (page - 1) * limit - - // 优化查询:只预加载必要的关联,并添加排序 - db := r.db.Model(&entity.Resource{}). - Preload("Category"). - Preload("Pan"). - Order("updated_at DESC") // 按更新时间倒序,显示最新内容 - - // 获取总数(使用缓存键) - cacheKey := fmt.Sprintf("resources_total_%d_%d", page, limit) - if cached, exists := r.cache[cacheKey]; exists { - if totalCached, ok := cached.(int64); ok { - total = totalCached - } - } else { - if err := db.Count(&total).Error; err != nil { - return nil, 0, err - } - // 缓存总数(5分钟) - r.cache[cacheKey] = total - go func() { - time.Sleep(5 * time.Minute) - delete(r.cache, cacheKey) - }() + // 使用新的分页查询功能 + options := &PaginationOptions{ + Page: page, + PageSize: limit, + OrderBy: "updated_at", + OrderDir: "desc", + Preloads: []string{"Category", "Pan"}, } - // 获取分页数据 - err := db.Offset(offset).Limit(limit).Find(&resources).Error - return resources, total, err + result, err := PaginatedQuery[entity.Resource](r.db, options) + if err != nil { + return nil, 0, err + } + + return result.Data, result.Total, nil } // FindByCategoryID 根据分类ID查找 diff --git a/go.mod b/go.mod index eb0e1fa..f107f09 100644 --- a/go.mod +++ b/go.mod @@ -13,14 +13,22 @@ require ( github.com/joho/godotenv v1.5.1 github.com/meilisearch/meilisearch-go v0.33.1 github.com/robfig/cron/v3 v3.0.1 - golang.org/x/crypto v0.40.0 + golang.org/x/crypto v0.41.0 gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.30.0 ) require ( github.com/andybalholm/brotli v1.1.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect ) require ( @@ -52,10 +60,10 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect golang.org/x/arch v0.19.0 // indirect - golang.org/x/net v0.42.0 + golang.org/x/net v0.43.0 golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.27.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 85c25dd..f0dfd3a 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,14 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= @@ -94,12 +98,22 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -126,14 +140,20 @@ github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU= golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -143,11 +163,15 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -156,6 +180,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/main.go b/main.go index 12e962f..4e1b65c 100644 --- a/main.go +++ b/main.go @@ -5,12 +5,15 @@ import ( "log" "os" "strings" + "time" + "github.com/ctwj/urldb/config" "github.com/ctwj/urldb/db" "github.com/ctwj/urldb/db/entity" "github.com/ctwj/urldb/db/repo" "github.com/ctwj/urldb/handlers" "github.com/ctwj/urldb/middleware" + "github.com/ctwj/urldb/monitor" "github.com/ctwj/urldb/scheduler" "github.com/ctwj/urldb/services" "github.com/ctwj/urldb/task" @@ -85,6 +88,17 @@ func main() { // 创建Repository管理器 repoManager := repo.NewRepositoryManager(db.DB) + // 创建配置管理器 + configManager := config.NewConfigManager(repoManager) + + // 设置全局配置管理器 + config.SetGlobalConfigManager(configManager) + + // 加载所有配置到缓存 + if err := configManager.LoadAllConfigs(); err != nil { + utils.Error("加载配置缓存失败: %v", err) + } + // 创建任务管理器 taskManager := task.NewTaskManager(repoManager) @@ -112,7 +126,22 @@ func main() { utils.Info("任务管理器初始化完成") // 创建Gin实例 - r := gin.Default() + r := gin.New() + + // 创建监控和错误处理器 + metrics := monitor.GetGlobalMetrics() + errorHandler := monitor.GetGlobalErrorHandler() + if errorHandler == nil { + errorHandler = monitor.NewErrorHandler(1000, 24*time.Hour) + monitor.SetGlobalErrorHandler(errorHandler) + } + + // 添加中间件 + r.Use(gin.Logger()) // Gin日志中间件 + r.Use(errorHandler.RecoverMiddleware()) // Panic恢复中间件 + r.Use(errorHandler.ErrorMiddleware()) // 错误处理中间件 + r.Use(metrics.MetricsMiddleware()) // 监控中间件 + r.Use(gin.Recovery()) // Gin恢复中间件 // 配置CORS config := cors.DefaultConfig() @@ -366,6 +395,27 @@ func main() { api.POST("/telegram/webhook", telegramHandler.HandleWebhook) } + // 设置监控系统 + monitor.SetupMonitoring(r) + + // 添加测试路由(仅在开发环境) + if os.Getenv("ENV") != "production" { + r.GET("/test/metrics", monitor.TestMetrics) + r.GET("/test/error", monitor.TestError) + r.GET("/test/error-stats", monitor.GetErrorStats) + r.GET("/test/metrics-stats", monitor.GetMetricsStats) + } + + // 启动监控服务器 + metricsConfig := &monitor.MetricsConfig{ + Enabled: true, + ListenAddress: ":9090", + MetricsPath: "/metrics", + Namespace: "urldb", + Subsystem: "api", + } + metrics.StartMetricsServer(metricsConfig) + // 静态文件服务 r.Static("/uploads", "./uploads") diff --git a/monitor/error_handler.go b/monitor/error_handler.go new file mode 100644 index 0000000..efa251b --- /dev/null +++ b/monitor/error_handler.go @@ -0,0 +1,327 @@ +package monitor + +import ( + "fmt" + "net/http" + "runtime" + "strings" + "sync" + "time" + + "github.com/ctwj/urldb/utils" + + "github.com/gin-gonic/gin" +) + +// ErrorInfo 错误信息结构 +type ErrorInfo struct { + ID string `json:"id"` + Timestamp time.Time `json:"timestamp"` + Message string `json:"message"` + StackTrace string `json:"stack_trace"` + RequestInfo *RequestInfo `json:"request_info,omitempty"` + Level string `json:"level"` // error, warn, info + Count int `json:"count"` +} + +// RequestInfo 请求信息结构 +type RequestInfo struct { + Method string `json:"method"` + URL string `json:"url"` + Headers map[string]string `json:"headers"` + RemoteAddr string `json:"remote_addr"` + UserAgent string `json:"user_agent"` + RequestBody string `json:"request_body"` +} + +// ErrorHandler 错误处理器 +type ErrorHandler struct { + errors map[string]*ErrorInfo + mu sync.RWMutex + maxErrors int + retention time.Duration +} + +// NewErrorHandler 创建新的错误处理器 +func NewErrorHandler(maxErrors int, retention time.Duration) *ErrorHandler { + eh := &ErrorHandler{ + errors: make(map[string]*ErrorInfo), + maxErrors: maxErrors, + retention: retention, + } + + // 启动错误清理协程 + go eh.cleanupRoutine() + + return eh +} + +// RecoverMiddleware panic恢复中间件 +func (eh *ErrorHandler) RecoverMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + // 记录错误信息 + stackTrace := getStackTrace() + + errorInfo := &ErrorInfo{ + ID: fmt.Sprintf("panic_%d", time.Now().UnixNano()), + Timestamp: time.Now(), + Message: fmt.Sprintf("%v", err), + StackTrace: stackTrace, + RequestInfo: &RequestInfo{ + Method: c.Request.Method, + URL: c.Request.URL.String(), + RemoteAddr: c.ClientIP(), + UserAgent: c.GetHeader("User-Agent"), + }, + Level: "error", + Count: 1, + } + + // 保存错误信息 + eh.saveError(errorInfo) + + utils.Error("Panic recovered: %v\nStack trace: %s", err, stackTrace) + + // 返回错误响应 + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal server error", + "code": "INTERNAL_ERROR", + }) + + // 不继续处理 + c.Abort() + } + }() + + c.Next() + } +} + +// ErrorMiddleware 通用错误处理中间件 +func (eh *ErrorHandler) ErrorMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + + // 检查是否有错误 + if len(c.Errors) > 0 { + for _, ginErr := range c.Errors { + errorInfo := &ErrorInfo{ + ID: fmt.Sprintf("error_%d_%s", time.Now().UnixNano(), ginErr.Type), + Timestamp: time.Now(), + Message: ginErr.Error(), + Level: "error", + Count: 1, + RequestInfo: &RequestInfo{ + Method: c.Request.Method, + URL: c.Request.URL.String(), + RemoteAddr: c.ClientIP(), + UserAgent: c.GetHeader("User-Agent"), + }, + } + + eh.saveError(errorInfo) + } + } + } +} + +// saveError 保存错误信息 +func (eh *ErrorHandler) saveError(errorInfo *ErrorInfo) { + eh.mu.Lock() + defer eh.mu.Unlock() + + key := errorInfo.Message + if existing, exists := eh.errors[key]; exists { + // 如果错误已存在,增加计数 + existing.Count++ + existing.Timestamp = time.Now() + } else { + // 如果是新错误,添加到映射中 + eh.errors[key] = errorInfo + } + + // 如果错误数量超过限制,清理旧错误 + if len(eh.errors) > eh.maxErrors { + eh.cleanupOldErrors() + } +} + +// GetErrors 获取错误列表 +func (eh *ErrorHandler) GetErrors() []*ErrorInfo { + eh.mu.RLock() + defer eh.mu.RUnlock() + + errors := make([]*ErrorInfo, 0, len(eh.errors)) + for _, errorInfo := range eh.errors { + errors = append(errors, errorInfo) + } + + return errors +} + +// GetErrorByID 根据ID获取错误 +func (eh *ErrorHandler) GetErrorByID(id string) (*ErrorInfo, bool) { + eh.mu.RLock() + defer eh.mu.RUnlock() + + for _, errorInfo := range eh.errors { + if errorInfo.ID == id { + return errorInfo, true + } + } + + return nil, false +} + +// ClearErrors 清空所有错误 +func (eh *ErrorHandler) ClearErrors() { + eh.mu.Lock() + defer eh.mu.Unlock() + + eh.errors = make(map[string]*ErrorInfo) +} + +// cleanupOldErrors 清理旧错误 +func (eh *ErrorHandler) cleanupOldErrors() { + // 简单策略:保留最近的错误,删除旧的 + errors := make([]*ErrorInfo, 0, len(eh.errors)) + for _, errorInfo := range eh.errors { + errors = append(errors, errorInfo) + } + + // 按时间戳排序 + for i := 0; i < len(errors)-1; i++ { + for j := i + 1; j < len(errors); j++ { + if errors[i].Timestamp.Before(errors[j].Timestamp) { + errors[i], errors[j] = errors[j], errors[i] + } + } + } + + // 保留最新的maxErrors/2个错误 + keep := eh.maxErrors / 2 + if keep < 1 { + keep = 1 + } + + if len(errors) > keep { + // 重建错误映射 + newErrors := make(map[string]*ErrorInfo) + for i := 0; i < keep; i++ { + newErrors[errors[i].Message] = errors[i] + } + eh.errors = newErrors + } +} + +// cleanupRoutine 定期清理过期错误的协程 +func (eh *ErrorHandler) cleanupRoutine() { + ticker := time.NewTicker(5 * time.Minute) // 每5分钟清理一次 + defer ticker.Stop() + + for range ticker.C { + eh.mu.Lock() + for key, errorInfo := range eh.errors { + if time.Since(errorInfo.Timestamp) > eh.retention { + delete(eh.errors, key) + } + } + eh.mu.Unlock() + } +} + +// getStackTrace 获取堆栈跟踪信息 +func getStackTrace() string { + var buf [4096]byte + n := runtime.Stack(buf[:], false) + return string(buf[:n]) +} + +// GetErrorStatistics 获取错误统计信息 +func (eh *ErrorHandler) GetErrorStatistics() map[string]interface{} { + eh.mu.RLock() + defer eh.mu.RUnlock() + + totalErrors := len(eh.errors) + totalCount := 0 + errorTypes := make(map[string]int) + + for _, errorInfo := range eh.errors { + totalCount += errorInfo.Count + // 提取错误类型(基于错误消息的前几个单词) + parts := strings.Split(errorInfo.Message, " ") + if len(parts) > 0 { + errorType := strings.Join(parts[:min(3, len(parts))], " ") + errorTypes[errorType]++ + } + } + + return map[string]interface{}{ + "total_errors": totalErrors, + "total_count": totalCount, + "error_types": errorTypes, + "max_errors": eh.maxErrors, + "retention": eh.retention, + "active_errors": len(eh.errors), + } +} + +// min 辅助函数 +func min(a, b int) int { + if a < b { + return a + } + return b +} + +// GlobalErrorHandler 全局错误处理器 +var globalErrorHandler *ErrorHandler + +// InitGlobalErrorHandler 初始化全局错误处理器 +func InitGlobalErrorHandler(maxErrors int, retention time.Duration) { + globalErrorHandler = NewErrorHandler(maxErrors, retention) +} + +// GetGlobalErrorHandler 获取全局错误处理器 +func GetGlobalErrorHandler() *ErrorHandler { + if globalErrorHandler == nil { + InitGlobalErrorHandler(100, 24*time.Hour) + } + return globalErrorHandler +} + +// Recover 全局panic恢复函数 +func Recover() gin.HandlerFunc { + if globalErrorHandler == nil { + InitGlobalErrorHandler(100, 24*time.Hour) + } + return globalErrorHandler.RecoverMiddleware() +} + +// Error 全局错误处理函数 +func Error() gin.HandlerFunc { + if globalErrorHandler == nil { + InitGlobalErrorHandler(100, 24*time.Hour) + } + return globalErrorHandler.ErrorMiddleware() +} + +// RecordError 记录错误(全局函数) +func RecordError(message string, level string) { + if globalErrorHandler == nil { + InitGlobalErrorHandler(100, 24*time.Hour) + return + } + + errorInfo := &ErrorInfo{ + ID: fmt.Sprintf("%s_%d", level, time.Now().UnixNano()), + Timestamp: time.Now(), + Message: message, + Level: level, + Count: 1, + } + + globalErrorHandler.saveError(errorInfo) +} \ No newline at end of file diff --git a/monitor/metrics.go b/monitor/metrics.go new file mode 100644 index 0000000..1729b31 --- /dev/null +++ b/monitor/metrics.go @@ -0,0 +1,458 @@ +package monitor + +import ( + "fmt" + "net/http" + "runtime" + "sync" + "time" + + "github.com/ctwj/urldb/utils" + "github.com/gin-gonic/gin" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +// Metrics 监控指标 +type Metrics struct { + // HTTP请求指标 + RequestsTotal *prometheus.CounterVec + RequestDuration *prometheus.HistogramVec + RequestSize *prometheus.SummaryVec + ResponseSize *prometheus.SummaryVec + + // 数据库指标 + DatabaseQueries *prometheus.CounterVec + DatabaseErrors *prometheus.CounterVec + DatabaseDuration *prometheus.HistogramVec + + // 系统指标 + MemoryUsage prometheus.Gauge + Goroutines prometheus.Gauge + GCStats *prometheus.CounterVec + + // 业务指标 + ResourcesCreated *prometheus.CounterVec + ResourcesViewed *prometheus.CounterVec + Searches *prometheus.CounterVec + Transfers *prometheus.CounterVec + + // 错误指标 + ErrorsTotal *prometheus.CounterVec + + // 自定义指标 + CustomCounters map[string]prometheus.Counter + CustomGauges map[string]prometheus.Gauge + mu sync.RWMutex +} + +// MetricsConfig 监控配置 +type MetricsConfig struct { + Enabled bool + ListenAddress string + MetricsPath string + Namespace string + Subsystem string +} + +// DefaultMetricsConfig 默认监控配置 +func DefaultMetricsConfig() *MetricsConfig { + return &MetricsConfig{ + Enabled: true, + ListenAddress: ":9090", + MetricsPath: "/metrics", + Namespace: "urldb", + Subsystem: "api", + } +} + +// GlobalMetrics 全局监控实例 +var ( + globalMetrics *Metrics + once sync.Once +) + +// NewMetrics 创建新的监控指标 +func NewMetrics(config *MetricsConfig) *Metrics { + if config == nil { + config = DefaultMetricsConfig() + } + + namespace := config.Namespace + subsystem := config.Subsystem + + m := &Metrics{ + // HTTP请求指标 + RequestsTotal: promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "http_requests_total", + Help: "Total number of HTTP requests", + }, + []string{"method", "endpoint", "status"}, + ), + RequestDuration: promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "http_request_duration_seconds", + Help: "HTTP request duration in seconds", + Buckets: prometheus.DefBuckets, + }, + []string{"method", "endpoint", "status"}, + ), + RequestSize: promauto.NewSummaryVec( + prometheus.SummaryOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "http_request_size_bytes", + Help: "HTTP request size in bytes", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"method", "endpoint"}, + ), + ResponseSize: promauto.NewSummaryVec( + prometheus.SummaryOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "http_response_size_bytes", + Help: "HTTP response size in bytes", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"method", "endpoint"}, + ), + + // 数据库指标 + DatabaseQueries: promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: "database", + Name: "queries_total", + Help: "Total number of database queries", + }, + []string{"table", "operation"}, + ), + DatabaseErrors: promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: "database", + Name: "errors_total", + Help: "Total number of database errors", + }, + []string{"table", "operation", "error"}, + ), + DatabaseDuration: promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: namespace, + Subsystem: "database", + Name: "query_duration_seconds", + Help: "Database query duration in seconds", + Buckets: prometheus.DefBuckets, + }, + []string{"table", "operation"}, + ), + + // 系统指标 + MemoryUsage: promauto.NewGauge( + prometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: "system", + Name: "memory_usage_bytes", + Help: "Current memory usage in bytes", + }, + ), + Goroutines: promauto.NewGauge( + prometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: "system", + Name: "goroutines", + Help: "Number of goroutines", + }, + ), + GCStats: promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: "system", + Name: "gc_stats_total", + Help: "Garbage collection statistics", + }, + []string{"type"}, + ), + + // 业务指标 + ResourcesCreated: promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: "business", + Name: "resources_created_total", + Help: "Total number of resources created", + }, + []string{"category", "platform"}, + ), + ResourcesViewed: promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: "business", + Name: "resources_viewed_total", + Help: "Total number of resources viewed", + }, + []string{"category"}, + ), + Searches: promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: "business", + Name: "searches_total", + Help: "Total number of searches", + }, + []string{"platform"}, + ), + Transfers: promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: "business", + Name: "transfers_total", + Help: "Total number of transfers", + }, + []string{"platform", "status"}, + ), + + // 错误指标 + ErrorsTotal: promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: "errors", + Name: "total", + Help: "Total number of errors", + }, + []string{"type", "endpoint"}, + ), + + // 自定义指标 + CustomCounters: make(map[string]prometheus.Counter), + CustomGauges: make(map[string]prometheus.Gauge), + } + + // 启动系统指标收集 + go m.collectSystemMetrics() + + return m +} + +// GetGlobalMetrics 获取全局监控实例 +func GetGlobalMetrics() *Metrics { + once.Do(func() { + globalMetrics = NewMetrics(DefaultMetricsConfig()) + }) + return globalMetrics +} + +// SetGlobalMetrics 设置全局监控实例 +func SetGlobalMetrics(metrics *Metrics) { + globalMetrics = metrics +} + +// collectSystemMetrics 收集系统指标 +func (m *Metrics) collectSystemMetrics() { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for range ticker.C { + // 收集内存使用情况 + var ms runtime.MemStats + runtime.ReadMemStats(&ms) + m.MemoryUsage.Set(float64(ms.Alloc)) + + // 收集goroutine数量 + m.Goroutines.Set(float64(runtime.NumGoroutine())) + + // 收集GC统计 + m.GCStats.WithLabelValues("alloc").Add(float64(ms.TotalAlloc)) + m.GCStats.WithLabelValues("sys").Add(float64(ms.Sys)) + m.GCStats.WithLabelValues("lookups").Add(float64(ms.Lookups)) + m.GCStats.WithLabelValues("mallocs").Add(float64(ms.Mallocs)) + m.GCStats.WithLabelValues("frees").Add(float64(ms.Frees)) + } +} + +// MetricsMiddleware 监控中间件 +func (m *Metrics) MetricsMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.FullPath() + + // 如果没有匹配的路由,使用请求路径 + if path == "" { + path = c.Request.URL.Path + } + + // 记录请求大小 + requestSize := float64(c.Request.ContentLength) + m.RequestSize.WithLabelValues(c.Request.Method, path).Observe(requestSize) + + c.Next() + + // 记录响应信息 + status := c.Writer.Status() + latency := time.Since(start).Seconds() + responseSize := float64(c.Writer.Size()) + + // 更新指标 + m.RequestsTotal.WithLabelValues(c.Request.Method, path, fmt.Sprintf("%d", status)).Inc() + m.RequestDuration.WithLabelValues(c.Request.Method, path, fmt.Sprintf("%d", status)).Observe(latency) + m.ResponseSize.WithLabelValues(c.Request.Method, path).Observe(responseSize) + + // 如果是错误状态码,记录错误 + if status >= 400 { + m.ErrorsTotal.WithLabelValues("http", path).Inc() + } + } +} + +// StartMetricsServer 启动监控服务器 +func (m *Metrics) StartMetricsServer(config *MetricsConfig) { + if config == nil { + config = DefaultMetricsConfig() + } + + if !config.Enabled { + utils.Info("监控服务器未启用") + return + } + + // 创建新的Gin路由器 + router := gin.New() + router.Use(gin.Recovery()) + + // 注册Prometheus指标端点 + router.GET(config.MetricsPath, gin.WrapH(promhttp.Handler())) + + // 启动HTTP服务器 + go func() { + utils.Info("监控服务器启动在 %s", config.ListenAddress) + if err := router.Run(config.ListenAddress); err != nil { + utils.Error("监控服务器启动失败: %v", err) + } + }() + + utils.Info("监控服务器已启动,指标路径: %s%s", config.ListenAddress, config.MetricsPath) +} + +// IncrementDatabaseQuery 增加数据库查询计数 +func (m *Metrics) IncrementDatabaseQuery(table, operation string) { + m.DatabaseQueries.WithLabelValues(table, operation).Inc() +} + +// IncrementDatabaseError 增加数据库错误计数 +func (m *Metrics) IncrementDatabaseError(table, operation, error string) { + m.DatabaseErrors.WithLabelValues(table, operation, error).Inc() +} + +// ObserveDatabaseDuration 记录数据库查询耗时 +func (m *Metrics) ObserveDatabaseDuration(table, operation string, duration float64) { + m.DatabaseDuration.WithLabelValues(table, operation).Observe(duration) +} + +// IncrementResourceCreated 增加资源创建计数 +func (m *Metrics) IncrementResourceCreated(category, platform string) { + m.ResourcesCreated.WithLabelValues(category, platform).Inc() +} + +// IncrementResourceViewed 增加资源查看计数 +func (m *Metrics) IncrementResourceViewed(category string) { + m.ResourcesViewed.WithLabelValues(category).Inc() +} + +// IncrementSearch 增加搜索计数 +func (m *Metrics) IncrementSearch(platform string) { + m.Searches.WithLabelValues(platform).Inc() +} + +// IncrementTransfer 增加转存计数 +func (m *Metrics) IncrementTransfer(platform, status string) { + m.Transfers.WithLabelValues(platform, status).Inc() +} + +// IncrementError 增加错误计数 +func (m *Metrics) IncrementError(errorType, endpoint string) { + m.ErrorsTotal.WithLabelValues(errorType, endpoint).Inc() +} + +// AddCustomCounter 添加自定义计数器 +func (m *Metrics) AddCustomCounter(name, help string, labels []string) prometheus.Counter { + m.mu.Lock() + defer m.mu.Unlock() + + key := fmt.Sprintf("%s_%v", name, labels) + if counter, exists := m.CustomCounters[key]; exists { + return counter + } + + counter := promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "urldb", + Name: name, + Help: help, + }, + labels, + ).WithLabelValues() // 如果没有标签,返回默认实例 + + m.CustomCounters[key] = counter + return counter +} + +// AddCustomGauge 添加自定义仪表盘 +func (m *Metrics) AddCustomGauge(name, help string, labels []string) prometheus.Gauge { + m.mu.Lock() + defer m.mu.Unlock() + + key := fmt.Sprintf("%s_%v", name, labels) + if gauge, exists := m.CustomGauges[key]; exists { + return gauge + } + + gauge := promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "urldb", + Name: name, + Help: help, + }, + labels, + ).WithLabelValues() // 如果没有标签,返回默认实例 + + m.CustomGauges[key] = gauge + return gauge +} + +// GetMetricsSummary 获取指标摘要 +func (m *Metrics) GetMetricsSummary() map[string]interface{} { + // 这里可以实现获取当前指标摘要的逻辑 + // 由于Prometheus指标不能直接读取,我们只能返回一些基本的统计信息 + return map[string]interface{}{ + "timestamp": time.Now(), + "status": "running", + "info": "使用 /metrics 端点获取详细指标", + } +} + +// HealthCheck 健康检查 +func (m *Metrics) HealthCheck(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": "healthy", + "timestamp": time.Now().Unix(), + "version": "1.0.0", + }) +} + +// SetupHealthCheck 设置健康检查端点 +func (m *Metrics) SetupHealthCheck(router *gin.Engine) { + router.GET("/health", m.HealthCheck) + router.GET("/healthz", m.HealthCheck) +} + +// MetricsHandler 指标处理器 +func (m *Metrics) MetricsHandler() gin.HandlerFunc { + return gin.WrapH(promhttp.Handler()) +} \ No newline at end of file diff --git a/monitor/setup.go b/monitor/setup.go new file mode 100644 index 0000000..33816cc --- /dev/null +++ b/monitor/setup.go @@ -0,0 +1,25 @@ +package monitor + +import ( + "github.com/ctwj/urldb/utils" + "github.com/gin-gonic/gin" +) + +// SetupMonitoring 设置完整的监控系统 +func SetupMonitoring(router *gin.Engine) { + // 获取全局监控实例 + metrics := GetGlobalMetrics() + + // 设置健康检查端点 + metrics.SetupHealthCheck(router) + + // 设置指标端点 + router.GET("/metrics", metrics.MetricsHandler()) + + utils.Info("监控系统已设置完成") +} + +// SetGlobalErrorHandler 设置全局错误处理器 +func SetGlobalErrorHandler(eh *ErrorHandler) { + globalErrorHandler = eh +} \ No newline at end of file