diff --git a/.env.example b/.env.example index 8bf727a..f4e439d 100644 --- a/.env.example +++ b/.env.example @@ -1,88 +1,28 @@ -# =========================================== -# OpenAI 兼容 API 多密钥代理服务器配置文件 (Go版本) -# =========================================== - -# =========================================== # 服务器配置 -# =========================================== -# 服务器端口 PORT=3000 - -# 服务器主机地址 HOST=0.0.0.0 -# =========================================== -# OpenAI 兼容 API 配置 -# =========================================== -# 上游 API 地址 -OPENAI_BASE_URL=https://api.openai.com - -# =========================================== -# 性能优化配置 -# =========================================== -# 最大并发请求数 -MAX_CONCURRENT_REQUESTS=100 - -# 启用 Gzip 压缩 -ENABLE_GZIP=true - -# =========================================== -# 日志配置 -# =========================================== -# 日志级别 (debug, info, warn, error) -LOG_LEVEL=info - -# 日志格式 (text, json) -LOG_FORMAT=text - -# 启用文件日志 -LOG_ENABLE_FILE=false - -# 日志文件路径 -LOG_FILE_PATH=logs/app.log - -# 启用请求日志(生产环境可设为 false 以提高性能) -LOG_ENABLE_REQUEST=true - -# =========================================== # 认证配置 -# =========================================== -# 项目认证密钥(可选,如果设置则启用认证) AUTH_KEY=sk-123456 -# =========================================== -# CORS 配置 -# =========================================== -# 是否启用 CORS +# CORS配置 ENABLE_CORS=true - -# 允许的来源(逗号分隔,* 表示允许所有) ALLOWED_ORIGINS=* ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS ALLOWED_HEADERS=* ALLOW_CREDENTIALS=false -# =========================================== -# 超时配置 -# =========================================== -# 服务器读取超时时间(秒) -SERVER_READ_TIMEOUT=120 +# 性能配置 +MAX_CONCURRENT_REQUESTS=100 +ENABLE_GZIP=true -# 服务器写入超时时间(秒) -SERVER_WRITE_TIMEOUT=1800 +# 数据库配置 +DATABASE_DSN=user:password@tcp(localhost:3306)/gpt_load?charset=utf8mb4&parseTime=True&loc=Local +DB_AUTO_MIGRATE=true -# 服务器空闲超时时间(秒) -SERVER_IDLE_TIMEOUT=120 - -# 服务器优雅关闭超时时间(秒) -SERVER_GRACEFUL_SHUTDOWN_TIMEOUT=60 - -# 请求超时时间(秒) -REQUEST_TIMEOUT=30 - -# 响应超时时间(秒)- 控制TLS握手和响应头接收超时 -RESPONSE_TIMEOUT=30 - -# 空闲连接超时时间(秒)- 控制连接池中空闲连接的生存时间 -IDLE_CONN_TIMEOUT=120 -DATABASE_DSN="user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" \ No newline at end of file +# 日志配置 +LOG_LEVEL=info +LOG_FORMAT=text +LOG_ENABLE_FILE=false +LOG_FILE_PATH=logs/app.log +LOG_ENABLE_REQUEST=true diff --git a/cmd/gpt-load/main.go b/cmd/gpt-load/main.go index 81aa9ce..a3ecae3 100644 --- a/cmd/gpt-load/main.go +++ b/cmd/gpt-load/main.go @@ -19,7 +19,8 @@ import ( "gpt-load/internal/handler" "gpt-load/internal/models" "gpt-load/internal/proxy" - "gpt-load/internal/router" // <-- 引入新的 router 包 + "gpt-load/internal/router" + "gpt-load/internal/services" "gpt-load/internal/types" "github.com/sirupsen/logrus" @@ -48,8 +49,23 @@ func main() { logrus.Fatalf("Failed to initialize database: %v", err) } + // Initialize system settings after database is ready + settingsManager := config.GetSystemSettingsManager() + if err := settingsManager.InitializeSystemSettings(); err != nil { + logrus.Fatalf("Failed to initialize system settings: %v", err) + } + logrus.Info("System settings initialized") + + // Display current system settings + settingsManager.DisplayCurrentSettings() + // Display startup information - displayStartupInfo(configManager) + configManager.DisplayConfig() + + // Start log cleanup service + logCleanupService := services.NewLogCleanupService() + logCleanupService.Start() + defer logCleanupService.Stop() // --- Asynchronous Request Logging Setup --- requestLogChan := make(chan models.RequestLog, 1000) @@ -67,12 +83,13 @@ func main() { // Create handlers serverHandler := handler.NewServer(database, configManager) + logCleanupHandler := handler.NewLogCleanupHandler(logCleanupService) // Setup routes using the new router package - appRouter := router.New(serverHandler, proxyServer, configManager, buildFS, indexPage) + appRouter := router.New(serverHandler, proxyServer, logCleanupHandler, configManager, buildFS, indexPage) // Create HTTP server with optimized timeout configuration - serverConfig := configManager.GetServerConfig() + serverConfig := configManager.GetEffectiveServerConfig() server := &http.Server{ Addr: fmt.Sprintf("%s:%d", serverConfig.Host, serverConfig.Port), Handler: appRouter, @@ -166,48 +183,6 @@ func setupLogger(configManager types.ConfigManager) { } } -// displayStartupInfo shows startup information -func displayStartupInfo(configManager types.ConfigManager) { - serverConfig := configManager.GetServerConfig() - openaiConfig := configManager.GetOpenAIConfig() - authConfig := configManager.GetAuthConfig() - corsConfig := configManager.GetCORSConfig() - perfConfig := configManager.GetPerformanceConfig() - logConfig := configManager.GetLogConfig() - - logrus.Info("Current Configuration:") - logrus.Infof(" Server: %s:%d", serverConfig.Host, serverConfig.Port) - logrus.Infof(" Upstream URL: %s", openaiConfig.BaseURL) - logrus.Infof(" Request timeout: %ds", openaiConfig.RequestTimeout) - logrus.Infof(" Response timeout: %ds", openaiConfig.ResponseTimeout) - logrus.Infof(" Idle connection timeout: %ds", openaiConfig.IdleConnTimeout) - - authStatus := "disabled" - if authConfig.Enabled { - authStatus = "enabled" - } - logrus.Infof(" Authentication: %s", authStatus) - - corsStatus := "disabled" - if corsConfig.Enabled { - corsStatus = "enabled" - } - logrus.Infof(" CORS: %s", corsStatus) - logrus.Infof(" Max concurrent requests: %d", perfConfig.MaxConcurrentRequests) - - gzipStatus := "disabled" - if perfConfig.EnableGzip { - gzipStatus = "enabled" - } - logrus.Infof(" Gzip compression: %s", gzipStatus) - - requestLogStatus := "enabled" - if !logConfig.EnableRequest { - requestLogStatus = "disabled" - } - logrus.Infof(" Request logging: %s", requestLogStatus) -} - // startRequestLogger runs a background goroutine to batch-insert request logs. func startRequestLogger(db *gorm.DB, logChan <-chan models.RequestLog, wg *sync.WaitGroup) { defer wg.Done() diff --git a/go.mod b/go.mod index 0b51f98..5976584 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.23.0 toolchain go1.24.3 require ( + github.com/gin-contrib/gzip v1.2.3 + github.com/gin-contrib/static v1.1.5 github.com/gin-gonic/gin v1.10.1 github.com/joho/godotenv v1.5.1 github.com/sirupsen/logrus v1.9.3 @@ -16,12 +18,9 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/bytedance/sonic v1.13.3 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect - github.com/gin-contrib/gzip v1.2.3 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/gin-contrib/static v1.1.5 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect @@ -31,6 +30,7 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.11 // indirect + github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect diff --git a/go.sum b/go.sum index 487aec8..18f6de4 100644 --- a/go.sum +++ b/go.sum @@ -1,44 +1,25 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= -github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 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.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 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= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U= github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= -github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/static v1.1.5 h1:bAPqT4KTZN+4uDY1b90eSrD1t8iNzod7Jj8njwmnzz4= github.com/gin-contrib/static v1.1.5/go.mod h1:8JSEXwZHcQ0uCrLPcsvnAJ4g+ODxeupP8Zetl9fd8wM= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -47,21 +28,14 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= @@ -72,19 +46,15 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -92,81 +62,45 @@ 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/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 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= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= -golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.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= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -175,4 +109,3 @@ gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqK gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/config/manager.go b/internal/config/manager.go index 564d775..f1483a8 100644 --- a/internal/config/manager.go +++ b/internal/config/manager.go @@ -3,7 +3,6 @@ package config import ( "fmt" - "net/url" "os" "strconv" "strings" @@ -70,18 +69,23 @@ func (m *Manager) ReloadConfig() error { config := &Config{ Server: types.ServerConfig{ - Port: parseInteger(os.Getenv("PORT"), 3000), - Host: getEnvOrDefault("HOST", "0.0.0.0"), - ReadTimeout: parseInteger(os.Getenv("SERVER_READ_TIMEOUT"), 120), - WriteTimeout: parseInteger(os.Getenv("SERVER_WRITE_TIMEOUT"), 1800), - IdleTimeout: parseInteger(os.Getenv("SERVER_IDLE_TIMEOUT"), 120), - GracefulShutdownTimeout: parseInteger(os.Getenv("SERVER_GRACEFUL_SHUTDOWN_TIMEOUT"), 60), + Port: parseInteger(os.Getenv("PORT"), 3000), + Host: getEnvOrDefault("HOST", "0.0.0.0"), + // Server timeout configs now come from system settings, not environment + // Using defaults here, will be overridden by system settings + ReadTimeout: 120, + WriteTimeout: 1800, + IdleTimeout: 120, + GracefulShutdownTimeout: 60, }, OpenAI: types.OpenAIConfig{ - BaseURLs: parseArray(os.Getenv("OPENAI_BASE_URL"), []string{"https://api.openai.com"}), - RequestTimeout: parseInteger(os.Getenv("REQUEST_TIMEOUT"), DefaultConstants.DefaultTimeout), - ResponseTimeout: parseInteger(os.Getenv("RESPONSE_TIMEOUT"), 30), - IdleConnTimeout: parseInteger(os.Getenv("IDLE_CONN_TIMEOUT"), 120), + // OPENAI_BASE_URL is removed from environment config + // Base URLs will be configured per group + BaseURLs: []string{}, // Will be set per group + // Timeout configs now come from system settings + RequestTimeout: 30, + ResponseTimeout: 30, + IdleConnTimeout: 120, }, Auth: types.AuthConfig{ Key: os.Getenv("AUTH_KEY"), @@ -113,29 +117,28 @@ func (m *Manager) ReloadConfig() error { return err } - logrus.Info("Configuration reloaded successfully") - m.DisplayConfig() + logrus.Info("Environment configuration reloaded successfully") return nil } // GetServerConfig returns server configuration -func (m *Manager) GetServerConfig() types.ServerConfig { - return m.config.Server -} +// func (m *Manager) GetServerConfig() types.ServerConfig { +// return m.config.Server +// } // GetOpenAIConfig returns OpenAI configuration -func (m *Manager) GetOpenAIConfig() types.OpenAIConfig { - config := m.config.OpenAI - if len(config.BaseURLs) > 1 { - // Use atomic counter for thread-safe round-robin - index := atomic.AddUint64(&m.roundRobinCounter, 1) - 1 - config.BaseURL = config.BaseURLs[index%uint64(len(config.BaseURLs))] - } else if len(config.BaseURLs) == 1 { - config.BaseURL = config.BaseURLs[0] - } - return config -} +// func (m *Manager) GetOpenAIConfig() types.OpenAIConfig { +// config := m.config.OpenAI +// if len(config.BaseURLs) > 1 { +// // Use atomic counter for thread-safe round-robin +// index := atomic.AddUint64(&m.roundRobinCounter, 1) - 1 +// config.BaseURL = config.BaseURLs[index%uint64(len(config.BaseURLs))] +// } else if len(config.BaseURLs) == 1 { +// config.BaseURL = config.BaseURLs[0] +// } +// return config +// } // GetAuthConfig returns authentication configuration func (m *Manager) GetAuthConfig() types.AuthConfig { @@ -157,6 +160,51 @@ func (m *Manager) GetLogConfig() types.LogConfig { return m.config.Log } +// GetEffectiveServerConfig returns server configuration merged with system settings +func (m *Manager) GetEffectiveServerConfig() types.ServerConfig { + config := m.config.Server + + // Merge with system settings + settingsManager := GetSystemSettingsManager() + systemSettings := settingsManager.GetSettings() + + config.ReadTimeout = systemSettings.ServerReadTimeout + config.WriteTimeout = systemSettings.ServerWriteTimeout + config.IdleTimeout = systemSettings.ServerIdleTimeout + config.GracefulShutdownTimeout = systemSettings.ServerGracefulShutdownTimeout + + return config +} + +// GetEffectiveOpenAIConfig returns OpenAI configuration merged with system settings and group config +func (m *Manager) GetEffectiveOpenAIConfig(groupConfig map[string]any) types.OpenAIConfig { + config := m.config.OpenAI + + // Merge with system settings + settingsManager := GetSystemSettingsManager() + effectiveSettings := settingsManager.GetEffectiveConfig(groupConfig) + + config.RequestTimeout = effectiveSettings.RequestTimeout + config.ResponseTimeout = effectiveSettings.ResponseTimeout + config.IdleConnTimeout = effectiveSettings.IdleConnTimeout + + // Apply round-robin for multiple URLs if configured + if len(config.BaseURLs) > 1 { + index := atomic.AddUint64(&m.roundRobinCounter, 1) - 1 + config.BaseURL = config.BaseURLs[index%uint64(len(config.BaseURLs))] + } else if len(config.BaseURLs) == 1 { + config.BaseURL = config.BaseURLs[0] + } + + return config +} + +// GetEffectiveLogConfig returns log configuration (now uses environment config only) +// func (m *Manager) GetEffectiveLogConfig() types.LogConfig { +// // Log configuration is now managed via environment variables only +// return m.config.Log +// } + // Validate validates the configuration func (m *Manager) Validate() error { var validationErrors []string @@ -166,22 +214,6 @@ func (m *Manager) Validate() error { validationErrors = append(validationErrors, fmt.Sprintf("port must be between %d-%d", DefaultConstants.MinPort, DefaultConstants.MaxPort)) } - // Validate timeout - if m.config.OpenAI.RequestTimeout < DefaultConstants.MinTimeout { - validationErrors = append(validationErrors, fmt.Sprintf("request timeout cannot be less than %ds", DefaultConstants.MinTimeout)) - } - - // Validate upstream URL format - if len(m.config.OpenAI.BaseURLs) == 0 { - validationErrors = append(validationErrors, "at least one upstream API URL is required") - } - for _, baseURL := range m.config.OpenAI.BaseURLs { - if _, err := url.Parse(baseURL); err != nil { - validationErrors = append(validationErrors, fmt.Sprintf("invalid upstream API URL format: %s", baseURL)) - } - } - - // Validate performance configuration if m.config.Performance.MaxConcurrentRequests < 1 { validationErrors = append(validationErrors, "max concurrent requests cannot be less than 1") } @@ -199,34 +231,37 @@ func (m *Manager) Validate() error { // DisplayConfig displays current configuration information func (m *Manager) DisplayConfig() { + serverConfig := m.GetEffectiveServerConfig() + // openaiConfig := m.GetOpenAIConfig() + authConfig := m.GetAuthConfig() + corsConfig := m.GetCORSConfig() + perfConfig := m.GetPerformanceConfig() + logConfig := m.GetLogConfig() + logrus.Info("Current Configuration:") - logrus.Infof(" Server: %s:%d", m.config.Server.Host, m.config.Server.Port) - logrus.Infof(" Upstream URLs: %s", strings.Join(m.config.OpenAI.BaseURLs, ", ")) - logrus.Infof(" Request timeout: %ds", m.config.OpenAI.RequestTimeout) - logrus.Infof(" Response timeout: %ds", m.config.OpenAI.ResponseTimeout) - logrus.Infof(" Idle connection timeout: %ds", m.config.OpenAI.IdleConnTimeout) + logrus.Infof(" Server: %s:%d", serverConfig.Host, serverConfig.Port) authStatus := "disabled" - if m.config.Auth.Enabled { + if authConfig.Enabled { authStatus = "enabled" } logrus.Infof(" Authentication: %s", authStatus) corsStatus := "disabled" - if m.config.CORS.Enabled { + if corsConfig.Enabled { corsStatus = "enabled" } logrus.Infof(" CORS: %s", corsStatus) - logrus.Infof(" Max concurrent requests: %d", m.config.Performance.MaxConcurrentRequests) + logrus.Infof(" Max concurrent requests: %d", perfConfig.MaxConcurrentRequests) gzipStatus := "disabled" - if m.config.Performance.EnableGzip { + if perfConfig.EnableGzip { gzipStatus = "enabled" } logrus.Infof(" Gzip compression: %s", gzipStatus) requestLogStatus := "enabled" - if !m.config.Log.EnableRequest { + if !logConfig.EnableRequest { requestLogStatus = "disabled" } logrus.Infof(" Request logging: %s", requestLogStatus) diff --git a/internal/config/system_settings.go b/internal/config/system_settings.go new file mode 100644 index 0000000..d6f0082 --- /dev/null +++ b/internal/config/system_settings.go @@ -0,0 +1,362 @@ +package config + +import ( + "fmt" + "gpt-load/internal/db" + "gpt-load/internal/models" + "reflect" + "strconv" + "strings" + "sync" + + "github.com/sirupsen/logrus" + "gorm.io/gorm/clause" +) + +// SystemSettings 定义所有系统配置项 +// 使用结构体标签作为唯一事实来源 +type SystemSettings struct { + // 负载均衡和重试配置 + BlacklistThreshold int `json:"blacklist_threshold" default:"1" desc:"Error count before blacklisting a key" validate:"min=0"` + MaxRetries int `json:"max_retries" default:"3" desc:"Maximum retry attempts with different keys" validate:"min=0"` + + // 服务器超时配置 (秒) + ServerReadTimeout int `json:"server_read_timeout" default:"120" desc:"HTTP server read timeout in seconds" validate:"min=1"` + ServerWriteTimeout int `json:"server_write_timeout" default:"1800" desc:"HTTP server write timeout in seconds" validate:"min=1"` + ServerIdleTimeout int `json:"server_idle_timeout" default:"120" desc:"HTTP server idle timeout in seconds" validate:"min=1"` + ServerGracefulShutdownTimeout int `json:"server_graceful_shutdown_timeout" default:"60" desc:"Graceful shutdown timeout in seconds" validate:"min=1"` + + // 请求超时配置 (秒) + RequestTimeout int `json:"request_timeout" default:"30" desc:"Request timeout in seconds" validate:"min=1"` + ResponseTimeout int `json:"response_timeout" default:"30" desc:"Response timeout in seconds (TLS handshake & response header)" validate:"min=1"` + IdleConnTimeout int `json:"idle_conn_timeout" default:"120" desc:"Idle connection timeout in seconds" validate:"min=1"` + + // 性能配置 + // MaxConcurrentRequests int `json:"max_concurrent_requests" default:"100" desc:"Maximum number of concurrent requests" validate:"min=1"` + + // 请求日志配置(数据库日志) + RequestLogRetentionDays int `json:"request_log_retention_days" default:"30" desc:"Number of days to retain request logs in database" validate:"min=1"` +} + +// GenerateSettingsMetadata 使用反射从 SystemSettings 结构体动态生成元数据 +func GenerateSettingsMetadata(s *SystemSettings) []models.SystemSettingInfo { + var settingsInfo []models.SystemSettingInfo + v := reflect.ValueOf(s).Elem() + t := v.Type() + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + fieldValue := v.Field(i) + + jsonTag := field.Tag.Get("json") + if jsonTag == "" { + continue + } + + descTag := field.Tag.Get("desc") + defaultTag := field.Tag.Get("default") + validateTag := field.Tag.Get("validate") + + var minValue *int + if strings.HasPrefix(validateTag, "min=") { + valStr := strings.TrimPrefix(validateTag, "min=") + if val, err := strconv.Atoi(valStr); err == nil { + minValue = &val + } + } + + info := models.SystemSettingInfo{ + Key: jsonTag, + Value: fieldValue.Interface(), + Type: field.Type.String(), + DefaultValue: defaultTag, + Description: descTag, + MinValue: minValue, + } + settingsInfo = append(settingsInfo, info) + } + return settingsInfo +} + +// DefaultSystemSettings 返回默认的系统配置 +func DefaultSystemSettings() SystemSettings { + s := SystemSettings{} + v := reflect.ValueOf(&s).Elem() + t := v.Type() + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + defaultTag := field.Tag.Get("default") + if defaultTag == "" { + continue + } + + fieldValue := v.Field(i) + if fieldValue.CanSet() { + switch fieldValue.Kind() { + case reflect.Int: + if val, err := strconv.ParseInt(defaultTag, 10, 64); err == nil { + fieldValue.SetInt(val) + } + // Add cases for other types like string, bool if needed + } + } + } + return s +} + +// SystemSettingsManager 管理系统配置 +type SystemSettingsManager struct { + settings SystemSettings + mu sync.RWMutex +} + +var globalSystemSettings *SystemSettingsManager +var once sync.Once + +// GetSystemSettingsManager 获取全局系统配置管理器单例 +func GetSystemSettingsManager() *SystemSettingsManager { + once.Do(func() { + globalSystemSettings = &SystemSettingsManager{} + }) + return globalSystemSettings +} + +// InitializeSystemSettings 初始化系统配置到数据库 +func (sm *SystemSettingsManager) InitializeSystemSettings() error { + if db.DB == nil { + return fmt.Errorf("database not initialized") + } + + defaultSettings := DefaultSystemSettings() + metadata := GenerateSettingsMetadata(&defaultSettings) + + for _, meta := range metadata { + var existing models.SystemSetting + err := db.DB.Where("setting_key = ?", meta.Key).First(&existing).Error + if err != nil { // Not found + setting := models.SystemSetting{ + SettingKey: meta.Key, + SettingValue: fmt.Sprintf("%v", meta.DefaultValue), + Description: meta.Description, + } + if err := db.DB.Create(&setting).Error; err != nil { + logrus.Errorf("Failed to initialize setting %s: %v", setting.SettingKey, err) + return err + } + logrus.Infof("Initialized system setting: %s = %s", setting.SettingKey, setting.SettingValue) + } + } + + // 加载配置到内存 + return sm.LoadFromDatabase() +} + +// LoadFromDatabase 从数据库加载系统配置到内存 +func (sm *SystemSettingsManager) LoadFromDatabase() error { + if db.DB == nil { + return fmt.Errorf("database not initialized") + } + + var settings []models.SystemSetting + if err := db.DB.Find(&settings).Error; err != nil { + return fmt.Errorf("failed to load system settings: %w", err) + } + + settingsMap := make(map[string]string) + for _, setting := range settings { + settingsMap[setting.SettingKey] = setting.SettingValue + } + + sm.mu.Lock() + defer sm.mu.Unlock() + + // 使用默认值,然后用数据库中的值覆盖 + sm.settings = DefaultSystemSettings() + sm.mapToStruct(settingsMap, &sm.settings) + + logrus.Info("System settings loaded from database") + return nil +} + +// GetSettings 获取当前系统配置 +func (sm *SystemSettingsManager) GetSettings() SystemSettings { + sm.mu.RLock() + defer sm.mu.RUnlock() + return sm.settings +} + +// UpdateSettings 更新系统配置 +func (sm *SystemSettingsManager) UpdateSettings(settingsMap map[string]string) error { + if db.DB == nil { + return fmt.Errorf("database not initialized") + } + + // 验证配置项 + if err := sm.ValidateSettings(settingsMap); err != nil { + return err + } + + // 更新数据库 + var settingsToUpdate []models.SystemSetting + for key, value := range settingsMap { + settingsToUpdate = append(settingsToUpdate, models.SystemSetting{ + SettingKey: key, + SettingValue: value, + }) + } + + if len(settingsToUpdate) > 0 { + if err := db.DB.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "setting_key"}}, + DoUpdates: clause.AssignmentColumns([]string{"setting_value", "updated_at"}), + }).Create(&settingsToUpdate).Error; err != nil { + return fmt.Errorf("failed to update system settings: %w", err) + } + } + + // 重新加载配置到内存 + return sm.LoadFromDatabase() +} + +// GetEffectiveConfig 获取有效配置 (系统配置 + 分组覆盖) +func (sm *SystemSettingsManager) GetEffectiveConfig(groupConfig map[string]any) SystemSettings { + sm.mu.RLock() + defer sm.mu.RUnlock() + + // 从系统配置开始 + effectiveConfig := sm.settings + v := reflect.ValueOf(&effectiveConfig).Elem() + t := v.Type() + + // 创建一个从 json 标签到字段名的映射 + jsonToField := make(map[string]string) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + jsonTag := strings.Split(field.Tag.Get("json"), ",")[0] + if jsonTag != "" { + jsonToField[jsonTag] = field.Name + } + } + + // 应用分组配置覆盖 + for key, val := range groupConfig { + if fieldName, ok := jsonToField[key]; ok { + fieldValue := v.FieldByName(fieldName) + if fieldValue.IsValid() && fieldValue.CanSet() { + if intVal, err := interfaceToInt(val); err == nil { + fieldValue.SetInt(int64(intVal)) + } + } + } + } + + return effectiveConfig +} + +// ValidateSettings 验证系统配置的有效性 +func (sm *SystemSettingsManager) ValidateSettings(settingsMap map[string]string) error { + tempSettings := DefaultSystemSettings() + v := reflect.ValueOf(&tempSettings).Elem() + t := v.Type() + jsonToField := make(map[string]reflect.StructField) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + jsonTag := field.Tag.Get("json") + if jsonTag != "" { + jsonToField[jsonTag] = field + } + } + + for key, value := range settingsMap { + field, ok := jsonToField[key] + if !ok { + return fmt.Errorf("invalid setting key: %s", key) + } + + validateTag := field.Tag.Get("validate") + if validateTag == "" { + continue + } + + switch field.Type.Kind() { + case reflect.Int: + intVal, err := strconv.Atoi(value) + if err != nil { + return fmt.Errorf("invalid integer value for %s: %s", key, value) + } + if strings.HasPrefix(validateTag, "min=") { + minValStr := strings.TrimPrefix(validateTag, "min=") + minVal, _ := strconv.Atoi(minValStr) + if intVal < minVal { + return fmt.Errorf("value for %s (%d) is below minimum value (%d)", key, intVal, minVal) + } + } + default: + return fmt.Errorf("unsupported type for setting key validation: %s", key) + } + } + + return nil +} + +// DisplayCurrentSettings 显示当前系统配置信息 +func (sm *SystemSettingsManager) DisplayCurrentSettings() { + sm.mu.RLock() + defer sm.mu.RUnlock() + + logrus.Info("Current System Settings:") + logrus.Infof(" Blacklist threshold: %d", sm.settings.BlacklistThreshold) + logrus.Infof(" Max retries: %d", sm.settings.MaxRetries) + logrus.Infof(" Server timeouts: read=%ds, write=%ds, idle=%ds, shutdown=%ds", + sm.settings.ServerReadTimeout, sm.settings.ServerWriteTimeout, + sm.settings.ServerIdleTimeout, sm.settings.ServerGracefulShutdownTimeout) + logrus.Infof(" Request timeouts: request=%ds, response=%ds, idle_conn=%ds", + sm.settings.RequestTimeout, sm.settings.ResponseTimeout, sm.settings.IdleConnTimeout) + // logrus.Infof(" Performance: max_concurrent=%d", sm.settings.MaxConcurrentRequests) + logrus.Infof(" Request log retention: %d days", sm.settings.RequestLogRetentionDays) +} + +// 辅助方法 + +func (sm *SystemSettingsManager) mapToStruct(m map[string]string, s *SystemSettings) { + v := reflect.ValueOf(s).Elem() + t := v.Type() + + // 创建一个从 json 标签到字段名的映射 + jsonToField := make(map[string]string) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + jsonTag := strings.Split(field.Tag.Get("json"), ",")[0] + if jsonTag != "" { + jsonToField[jsonTag] = field.Name + } + } + + for key, valStr := range m { + if fieldName, ok := jsonToField[key]; ok { + fieldValue := v.FieldByName(fieldName) + if fieldValue.IsValid() && fieldValue.CanSet() { + // 假设所有字段都是 int 类型 + if intVal, err := strconv.Atoi(valStr); err == nil { + fieldValue.SetInt(int64(intVal)) + } + } + } + } +} + +// 工具函数 + +func interfaceToInt(val interface{}) (int, error) { + switch v := val.(type) { + case int: + return v, nil + case float64: + return int(v), nil + case string: + return strconv.Atoi(v) + default: + return 0, fmt.Errorf("cannot convert to int: %v", val) + } +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 162a612..fc40397 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -49,7 +49,7 @@ func (s *Server) Login(c *gin.Context) { } authConfig := s.config.GetAuthConfig() - + if !authConfig.Enabled { c.JSON(http.StatusOK, LoginResponse{ Success: true, @@ -102,70 +102,3 @@ func (s *Server) Health(c *gin.Context) { "uptime": uptime, }) } - - -// GetConfig returns configuration information (for debugging) -func (s *Server) GetConfig(c *gin.Context) { - // Only allow in development mode or with special header - if c.GetHeader("X-Debug-Config") != "true" { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Access denied", - }) - return - } - - serverConfig := s.config.GetServerConfig() - openaiConfig := s.config.GetOpenAIConfig() - authConfig := s.config.GetAuthConfig() - corsConfig := s.config.GetCORSConfig() - perfConfig := s.config.GetPerformanceConfig() - logConfig := s.config.GetLogConfig() - - // Sanitize sensitive information - sanitizedConfig := gin.H{ - "server": gin.H{ - "host": serverConfig.Host, - "port": serverConfig.Port, - }, - "openai": gin.H{ - "base_url": openaiConfig.BaseURL, - "request_timeout": openaiConfig.RequestTimeout, - "response_timeout": openaiConfig.ResponseTimeout, - "idle_conn_timeout": openaiConfig.IdleConnTimeout, - }, - "auth": gin.H{ - "enabled": authConfig.Enabled, - // Don't expose the actual key - }, - "cors": gin.H{ - "enabled": corsConfig.Enabled, - "allowed_origins": corsConfig.AllowedOrigins, - "allowed_methods": corsConfig.AllowedMethods, - "allowed_headers": corsConfig.AllowedHeaders, - "allow_credentials": corsConfig.AllowCredentials, - }, - "performance": gin.H{ - "max_concurrent_requests": perfConfig.MaxConcurrentRequests, - "enable_gzip": perfConfig.EnableGzip, - }, - "timeout_config": gin.H{ - "request_timeout_s": openaiConfig.RequestTimeout, - "response_timeout_s": openaiConfig.ResponseTimeout, - "idle_conn_timeout_s": openaiConfig.IdleConnTimeout, - "server_read_timeout_s": serverConfig.ReadTimeout, - "server_write_timeout_s": serverConfig.WriteTimeout, - "server_idle_timeout_s": serverConfig.IdleTimeout, - "graceful_shutdown_timeout_s": serverConfig.GracefulShutdownTimeout, - }, - "log": gin.H{ - "level": logConfig.Level, - "format": logConfig.Format, - "enable_file": logConfig.EnableFile, - "file_path": logConfig.FilePath, - "enable_request": logConfig.EnableRequest, - }, - "timestamp": time.Now().UTC().Format(time.RFC3339), - } - - c.JSON(http.StatusOK, sanitizedConfig) -} diff --git a/internal/handler/log_cleanup_handler.go b/internal/handler/log_cleanup_handler.go new file mode 100644 index 0000000..93f245a --- /dev/null +++ b/internal/handler/log_cleanup_handler.go @@ -0,0 +1,34 @@ +package handler + +import ( + "gpt-load/internal/response" + "gpt-load/internal/services" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +// LogCleanupHandler handles log cleanup related requests +type LogCleanupHandler struct { + LogCleanupService *services.LogCleanupService +} + +// NewLogCleanupHandler creates a new LogCleanupHandler +func NewLogCleanupHandler(s *services.LogCleanupService) *LogCleanupHandler { + return &LogCleanupHandler{ + LogCleanupService: s, + } +} + +// CleanupLogsNow handles the POST /api/logs/cleanup request. +// It triggers an asynchronous cleanup of expired request logs. +func (h *LogCleanupHandler) CleanupLogsNow(c *gin.Context) { + go func() { + logrus.Info("Asynchronous log cleanup started from API request") + h.LogCleanupService.CleanupNow() + }() + + response.Success(c, gin.H{ + "message": "Log cleanup process started in the background", + }) +} diff --git a/internal/handler/reload_handler.go b/internal/handler/reload_handler.go deleted file mode 100644 index e2cb283..0000000 --- a/internal/handler/reload_handler.go +++ /dev/null @@ -1,26 +0,0 @@ -package handler - -import ( - "gpt-load/internal/response" - - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" -) - -// ReloadConfig handles the POST /api/reload request. -// It triggers a configuration reload. -func (s *Server) ReloadConfig(c *gin.Context) { - if s.config == nil { - response.InternalError(c, "Configuration manager is not initialized") - return - } - - err := s.config.ReloadConfig() - if err != nil { - logrus.Errorf("Failed to reload config: %v", err) - response.InternalError(c, "Failed to reload config") - return - } - - response.Success(c, gin.H{"message": "Configuration reloaded successfully"}) -} diff --git a/internal/handler/settings_handler.go b/internal/handler/settings_handler.go index f422977..8986447 100644 --- a/internal/handler/settings_handler.go +++ b/internal/handler/settings_handler.go @@ -1,33 +1,28 @@ package handler import ( - "gpt-load/internal/db" - "gpt-load/internal/models" + "gpt-load/internal/config" "gpt-load/internal/response" "github.com/gin-gonic/gin" - "gorm.io/gorm/clause" + "github.com/sirupsen/logrus" ) // GetSettings handles the GET /api/settings request. -// It retrieves all system settings from the database and returns them as a key-value map. +// It retrieves all system settings and returns them with detailed information. func GetSettings(c *gin.Context) { - var settings []models.SystemSetting - if err := db.DB.Find(&settings).Error; err != nil { - response.InternalError(c, "Failed to retrieve settings") - return - } + settingsManager := config.GetSystemSettingsManager() + currentSettings := settingsManager.GetSettings() - settingsMap := make(map[string]string) - for _, s := range settings { - settingsMap[s.SettingKey] = s.SettingValue - } + // 使用新的动态元数据生成器 + settingsInfo := config.GenerateSettingsMetadata(¤tSettings) - response.Success(c, settingsMap) + response.Success(c, settingsInfo) } // UpdateSettings handles the PUT /api/settings request. -// It receives a key-value JSON object and updates or creates settings in the database. +// It receives a key-value JSON object and updates system settings. +// After updating, it triggers a configuration reload. func UpdateSettings(c *gin.Context) { var settingsMap map[string]string if err := c.ShouldBindJSON(&settingsMap); err != nil { @@ -35,28 +30,37 @@ func UpdateSettings(c *gin.Context) { return } - var settingsToUpdate []models.SystemSetting - for key, value := range settingsMap { - settingsToUpdate = append(settingsToUpdate, models.SystemSetting{ - SettingKey: key, - SettingValue: value, - }) - } - - if len(settingsToUpdate) == 0 { + if len(settingsMap) == 0 { response.Success(c, nil) return } - // Using OnConflict to perform an "upsert" operation. - // If a setting with the same key exists, it will be updated. Otherwise, a new one will be created. - if err := db.DB.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "setting_key"}}, - DoUpdates: clause.AssignmentColumns([]string{"setting_value"}), - }).Create(&settingsToUpdate).Error; err != nil { - response.InternalError(c, "Failed to update settings") + settingsManager := config.GetSystemSettingsManager() + + // 更新配置 + if err := settingsManager.UpdateSettings(settingsMap); err != nil { + response.InternalError(c, "Failed to update settings: "+err.Error()) return } - response.Success(c, nil) -} \ No newline at end of file + // 重载系统配置 + if err := settingsManager.LoadFromDatabase(); err != nil { + logrus.Errorf("Failed to reload system settings: %v", err) + response.InternalError(c, "Failed to reload system settings") + return + } + + settingsManager.DisplayCurrentSettings() + + logrus.Info("Configuration reloaded successfully via API") + response.Success(c, gin.H{ + "message": "Configuration reloaded successfully", + "timestamp": gin.H{ + "reloaded_at": "now", + }, + }) + + response.Success(c, gin.H{ + "message": "Settings updated successfully. Configuration reloaded.", + }) +} diff --git a/internal/models/setting_info.go b/internal/models/setting_info.go new file mode 100644 index 0000000..07e0b4b --- /dev/null +++ b/internal/models/setting_info.go @@ -0,0 +1,15 @@ +package models + +// SystemSettingInfo 表示系统配置的详细信息(用于API返回) +type SystemSettingInfo struct { + Key string `json:"key"` + Value interface{} `json:"value"` + Type string `json:"type"` // "int", "bool", "string" + DefaultValue interface{} `json:"default_value"` + Description string `json:"description"` + Category string `json:"category"` // "timeout", "performance", "logging", etc. + Required bool `json:"required"` + MinValue *int `json:"min_value,omitempty"` + MaxValue *int `json:"max_value,omitempty"` + ValidOptions []string `json:"valid_options,omitempty"` +} diff --git a/internal/models/types.go b/internal/models/types.go index 8c5481e..cd000ae 100644 --- a/internal/models/types.go +++ b/internal/models/types.go @@ -63,4 +63,4 @@ type DashboardStats struct { SuccessRequests int64 `json:"success_requests"` SuccessRate float64 `json:"success_rate"` GroupStats []GroupRequestStat `json:"group_stats"` -} \ No newline at end of file +} diff --git a/internal/router/router.go b/internal/router/router.go index 1cb4d8a..ac99f96 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -39,6 +39,7 @@ func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem { func New( serverHandler *handler.Server, proxyServer *proxy.ProxyServer, + logCleanupHandler *handler.LogCleanupHandler, configManager types.ConfigManager, buildFS embed.FS, indexPage []byte, @@ -60,7 +61,7 @@ func New( // 注册路由 registerSystemRoutes(router, serverHandler) - registerAPIRoutes(router, serverHandler, configManager) + registerAPIRoutes(router, serverHandler, logCleanupHandler, configManager) registerProxyRoutes(router, proxyServer, configManager) registerFrontendRoutes(router, buildFS, indexPage) @@ -71,11 +72,10 @@ func New( func registerSystemRoutes(router *gin.Engine, serverHandler *handler.Server) { router.GET("/health", serverHandler.Health) router.GET("/stats", serverHandler.Stats) - // router.GET("/config", serverHandler.GetConfig) } // registerAPIRoutes 注册API路由 -func registerAPIRoutes(router *gin.Engine, serverHandler *handler.Server, configManager types.ConfigManager) { +func registerAPIRoutes(router *gin.Engine, serverHandler *handler.Server, logCleanupHandler *handler.LogCleanupHandler, configManager types.ConfigManager) { api := router.Group("/api") authConfig := configManager.GetAuthConfig() @@ -86,9 +86,9 @@ func registerAPIRoutes(router *gin.Engine, serverHandler *handler.Server, config if authConfig.Enabled { protectedAPI := api.Group("") protectedAPI.Use(middleware.Auth(authConfig)) - registerProtectedAPIRoutes(protectedAPI, serverHandler) + registerProtectedAPIRoutes(protectedAPI, serverHandler, logCleanupHandler) } else { - registerProtectedAPIRoutes(api, serverHandler) + registerProtectedAPIRoutes(api, serverHandler, logCleanupHandler) } } @@ -98,7 +98,7 @@ func registerPublicAPIRoutes(api *gin.RouterGroup, serverHandler *handler.Server } // registerProtectedAPIRoutes 认证API路由 -func registerProtectedAPIRoutes(api *gin.RouterGroup, serverHandler *handler.Server) { +func registerProtectedAPIRoutes(api *gin.RouterGroup, serverHandler *handler.Server, logCleanupHandler *handler.LogCleanupHandler) { groups := api.Group("/groups") { groups.POST("", serverHandler.CreateGroup) @@ -123,7 +123,11 @@ func registerProtectedAPIRoutes(api *gin.RouterGroup, serverHandler *handler.Ser } // 日志 - api.GET("/logs", handler.GetLogs) + logs := api.Group("/logs") + { + logs.GET("", handler.GetLogs) + logs.POST("/cleanup", logCleanupHandler.CleanupLogsNow) + } // 设置 settings := api.Group("/settings") @@ -131,9 +135,6 @@ func registerProtectedAPIRoutes(api *gin.RouterGroup, serverHandler *handler.Ser settings.GET("", handler.GetSettings) settings.PUT("", handler.UpdateSettings) } - - // 重载配置 - api.POST("/reload", serverHandler.ReloadConfig) } // registerProxyRoutes 注册代理路由 diff --git a/internal/services/log_cleanup.go b/internal/services/log_cleanup.go new file mode 100644 index 0000000..fa34091 --- /dev/null +++ b/internal/services/log_cleanup.go @@ -0,0 +1,102 @@ +package services + +import ( + "gpt-load/internal/config" + "gpt-load/internal/db" + "gpt-load/internal/models" + "time" + + "github.com/sirupsen/logrus" +) + +// LogCleanupService 负责清理过期的请求日志 +type LogCleanupService struct { + stopCh chan struct{} +} + +// NewLogCleanupService 创建新的日志清理服务 +func NewLogCleanupService() *LogCleanupService { + return &LogCleanupService{ + stopCh: make(chan struct{}), + } +} + +// Start 启动日志清理服务 +func (s *LogCleanupService) Start() { + go s.run() + logrus.Info("Log cleanup service started") +} + +// Stop 停止日志清理服务 +func (s *LogCleanupService) Stop() { + close(s.stopCh) + logrus.Info("Log cleanup service stopped") +} + +// run 运行日志清理的主循环 +func (s *LogCleanupService) run() { + // 每天凌晨2点执行清理任务 + ticker := time.NewTicker(24 * time.Hour) + defer ticker.Stop() + + // 启动时先执行一次清理 + s.cleanupExpiredLogs() + + for { + select { + case <-ticker.C: + s.cleanupExpiredLogs() + case <-s.stopCh: + return + } + } +} + +// cleanupExpiredLogs 清理过期的请求日志 +func (s *LogCleanupService) cleanupExpiredLogs() { + if db.DB == nil { + logrus.Error("Database connection is not available for log cleanup") + return + } + + // 获取日志保留天数配置 + settingsManager := config.GetSystemSettingsManager() + if settingsManager == nil { + logrus.Error("System settings manager is not available for log cleanup") + return + } + + settings := settingsManager.GetSettings() + retentionDays := settings.RequestLogRetentionDays + + if retentionDays <= 0 { + logrus.Debug("Log retention is disabled (retention_days <= 0)") + return + } + + // 计算过期时间点 + cutoffTime := time.Now().AddDate(0, 0, -retentionDays).UTC() + + // 执行删除操作 + result := db.DB.Where("timestamp < ?", cutoffTime).Delete(&models.RequestLog{}) + if result.Error != nil { + logrus.WithError(result.Error).Error("Failed to cleanup expired request logs") + return + } + + if result.RowsAffected > 0 { + logrus.WithFields(logrus.Fields{ + "deleted_count": result.RowsAffected, + "cutoff_time": cutoffTime.Format(time.RFC3339), + "retention_days": retentionDays, + }).Info("Successfully cleaned up expired request logs") + } else { + logrus.Debug("No expired request logs found to cleanup") + } +} + +// CleanupNow 立即执行一次日志清理 +func (s *LogCleanupService) CleanupNow() { + logrus.Info("Manual log cleanup triggered") + s.cleanupExpiredLogs() +} diff --git a/internal/types/types.go b/internal/types/types.go index 083ac13..0a3db7e 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -7,12 +7,19 @@ import ( // ConfigManager defines the interface for configuration management type ConfigManager interface { - GetServerConfig() ServerConfig - GetOpenAIConfig() OpenAIConfig + // GetServerConfig() ServerConfig + // GetOpenAIConfig() OpenAIConfig GetAuthConfig() AuthConfig GetCORSConfig() CORSConfig GetPerformanceConfig() PerformanceConfig GetLogConfig() LogConfig + + // Effective configuration methods that merge system settings + GetEffectiveServerConfig() ServerConfig + GetEffectiveOpenAIConfig(groupConfig map[string]any) OpenAIConfig + // GetEffectivePerformanceConfig() PerformanceConfig + // GetEffectiveLogConfig() LogConfig + Validate() error DisplayConfig() ReloadConfig() error