From 25ba042493d012b414ecb9f284776e60c29f5247 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Fri, 26 Sep 2025 03:19:44 +0800 Subject: [PATCH] feat(config, usage): add `usage-statistics-enabled` option and dynamic toggling - Introduced `usage-statistics-enabled` configuration to control in-memory usage aggregation. - Updated API to include handlers for managing `usage-statistics-enabled` and `logging-to-file` options. - Enhanced `watcher` to log changes to both configurations dynamically. - Updated documentation and examples to reflect new configuration options. --- README.md | 8 ++++++++ README_CN.md | 8 ++++++++ cmd/server/main.go | 2 ++ config.example.yaml | 3 +++ internal/api/handlers/management/config_basic.go | 16 ++++++++++++++++ internal/api/server.go | 16 ++++++++++++++++ internal/config/config.go | 4 ++++ internal/usage/logger_plugin.go | 16 ++++++++++++++++ internal/watcher/watcher.go | 6 ++++++ 9 files changed, 79 insertions(+) diff --git a/README.md b/README.md index c6adff45..2905dff1 100644 --- a/README.md +++ b/README.md @@ -280,6 +280,8 @@ The server uses a YAML configuration file (`config.yaml`) located in the project | `quota-exceeded.switch-project` | boolean | true | Whether to automatically switch to another project when a quota is exceeded. | | `quota-exceeded.switch-preview-model` | boolean | true | Whether to automatically switch to a preview model when a quota is exceeded. | | `debug` | boolean | false | Enable debug mode for verbose logging. | +| `logging-to-file` | boolean | true | Write application logs to rotating files instead of stdout. Set to `false` to log to stdout/stderr. | +| `usage-statistics-enabled` | boolean | true | Enable in-memory usage aggregation for management APIs. Disable to drop all collected usage metrics. | | `auth` | object | {} | Request authentication configuration. | | `auth.providers` | object[] | [] | Authentication providers. Includes built-in `config-api-key` for inline keys. | | `auth.providers.*.name` | string | "" | Provider instance name. | @@ -329,6 +331,12 @@ auth-dir: "~/.cli-proxy-api" # Enable debug logging debug: false +# When true, write application logs to rotating files instead of stdout +logging-to-file: true + +# When false, disable in-memory usage statistics aggregation +usage-statistics-enabled: true + # Proxy URL. Supports socks5/http/https protocols. Example: socks5://user:pass@192.168.1.1:1080/ proxy-url: "" diff --git a/README_CN.md b/README_CN.md index cf206659..8fa82fc6 100644 --- a/README_CN.md +++ b/README_CN.md @@ -292,6 +292,8 @@ console.log(await claudeResponse.json()); | `quota-exceeded.switch-project` | boolean | true | 当配额超限时,是否自动切换到另一个项目。 | | `quota-exceeded.switch-preview-model` | boolean | true | 当配额超限时,是否自动切换到预览模型。 | | `debug` | boolean | false | 启用调试模式以获取详细日志。 | +| `logging-to-file` | boolean | true | 是否将应用日志写入滚动文件;设为 false 时输出到 stdout/stderr。 | +| `usage-statistics-enabled` | boolean | true | 是否启用内存中的使用统计;设为 false 时直接丢弃所有统计数据。 | | `auth` | object | {} | 请求鉴权配置。 | | `auth.providers` | object[] | [] | 鉴权提供方列表,内置 `config-api-key` 支持内联密钥。 | | `auth.providers.*.name` | string | "" | 提供方实例名称。 | @@ -340,6 +342,12 @@ auth-dir: "~/.cli-proxy-api" # 启用调试日志 debug: false +# 为 true 时将应用日志写入滚动文件而不是 stdout +logging-to-file: true + +# 为 false 时禁用内存中的使用统计并直接丢弃所有数据 +usage-statistics-enabled: true + # 代理URL。支持socks5/http/https协议。例如:socks5://user:pass@192.168.1.1:1080/ proxy-url: "" diff --git a/cmd/server/main.go b/cmd/server/main.go index 06c96999..7c5d35ac 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -14,6 +14,7 @@ import ( "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator" + "github.com/router-for-me/CLIProxyAPI/v6/internal/usage" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" log "github.com/sirupsen/logrus" @@ -111,6 +112,7 @@ func main() { if err != nil { log.Fatalf("failed to load config: %v", err) } + usage.SetStatisticsEnabled(cfg.UsageStatisticsEnabled) if err = logging.ConfigureLogOutput(cfg.LoggingToFile); err != nil { log.Fatalf("failed to configure log output: %v", err) diff --git a/config.example.yaml b/config.example.yaml index 5ac25bc6..9dde0b7b 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -21,6 +21,9 @@ debug: false # When true, write application logs to rotating files instead of stdout logging-to-file: true +# When false, disable in-memory usage statistics aggregation +usage-statistics-enabled: true + # Proxy URL. Supports socks5/http/https protocols. Example: socks5://user:pass@192.168.1.1:1080/ proxy-url: "" diff --git a/internal/api/handlers/management/config_basic.go b/internal/api/handlers/management/config_basic.go index a89996c9..6e3c9f67 100644 --- a/internal/api/handlers/management/config_basic.go +++ b/internal/api/handlers/management/config_basic.go @@ -12,6 +12,22 @@ func (h *Handler) GetConfig(c *gin.Context) { func (h *Handler) GetDebug(c *gin.Context) { c.JSON(200, gin.H{"debug": h.cfg.Debug}) } func (h *Handler) PutDebug(c *gin.Context) { h.updateBoolField(c, func(v bool) { h.cfg.Debug = v }) } +// UsageStatisticsEnabled +func (h *Handler) GetUsageStatisticsEnabled(c *gin.Context) { + c.JSON(200, gin.H{"usage-statistics-enabled": h.cfg.UsageStatisticsEnabled}) +} +func (h *Handler) PutUsageStatisticsEnabled(c *gin.Context) { + h.updateBoolField(c, func(v bool) { h.cfg.UsageStatisticsEnabled = v }) +} + +// UsageStatisticsEnabled +func (h *Handler) GetLoggingToFile(c *gin.Context) { + c.JSON(200, gin.H{"logging-to-file": h.cfg.LoggingToFile}) +} +func (h *Handler) PutLoggingToFile(c *gin.Context) { + h.updateBoolField(c, func(v bool) { h.cfg.LoggingToFile = v }) +} + // Request log func (h *Handler) GetRequestLog(c *gin.Context) { c.JSON(200, gin.H{"request-log": h.cfg.RequestLog}) } func (h *Handler) PutRequestLog(c *gin.Context) { diff --git a/internal/api/server.go b/internal/api/server.go index 5efc9175..70bd487e 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -24,6 +24,7 @@ import ( "github.com/router-for-me/CLIProxyAPI/v6/internal/api/middleware" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v6/internal/usage" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" @@ -317,6 +318,14 @@ func (s *Server) setupRoutes() { mgmt.PUT("/debug", s.mgmt.PutDebug) mgmt.PATCH("/debug", s.mgmt.PutDebug) + mgmt.GET("/logging-to-file", s.mgmt.GetLoggingToFile) + mgmt.PUT("/logging-to-file", s.mgmt.PutLoggingToFile) + mgmt.PATCH("/logging-to-file", s.mgmt.PutLoggingToFile) + + mgmt.GET("/usage-statistics-enabled", s.mgmt.GetUsageStatisticsEnabled) + mgmt.PUT("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled) + mgmt.PATCH("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled) + mgmt.GET("/proxy-url", s.mgmt.GetProxyURL) mgmt.PUT("/proxy-url", s.mgmt.PutProxyURL) mgmt.PATCH("/proxy-url", s.mgmt.PutProxyURL) @@ -575,6 +584,13 @@ func (s *Server) UpdateClients(cfg *config.Config) { } } + if s.cfg == nil || s.cfg.UsageStatisticsEnabled != cfg.UsageStatisticsEnabled { + usage.SetStatisticsEnabled(cfg.UsageStatisticsEnabled) + if s.cfg != nil { + log.Debugf("usage_statistics_enabled updated from %t to %t", s.cfg.UsageStatisticsEnabled, cfg.UsageStatisticsEnabled) + } + } + // Update log level dynamically when debug flag changes if s.cfg.Debug != cfg.Debug { util.SetLogLevel(cfg) diff --git a/internal/config/config.go b/internal/config/config.go index 5dcb7e08..f063c7a6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,6 +26,9 @@ type Config struct { // LoggingToFile controls whether application logs are written to rotating files or stdout. LoggingToFile bool `yaml:"logging-to-file" json:"logging-to-file"` + // UsageStatisticsEnabled toggles in-memory usage aggregation; when false, usage data is discarded. + UsageStatisticsEnabled bool `yaml:"usage-statistics-enabled" json:"usage-statistics-enabled"` + // ProxyURL is the URL of an optional proxy server to use for outbound requests. ProxyURL string `yaml:"proxy-url" json:"proxy-url"` @@ -206,6 +209,7 @@ func LoadConfig(configFile string) (*Config, error) { var config Config // Set defaults before unmarshal so that absent keys keep defaults. config.LoggingToFile = true + config.UsageStatisticsEnabled = true config.GeminiWeb.Context = true if err = yaml.Unmarshal(data, &config); err != nil { return nil, fmt.Errorf("failed to parse config file: %w", err) diff --git a/internal/usage/logger_plugin.go b/internal/usage/logger_plugin.go index 2ed49575..a96dbc7c 100644 --- a/internal/usage/logger_plugin.go +++ b/internal/usage/logger_plugin.go @@ -7,13 +7,17 @@ import ( "context" "fmt" "sync" + "sync/atomic" "time" "github.com/gin-gonic/gin" coreusage "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage" ) +var statisticsEnabled atomic.Bool + func init() { + statisticsEnabled.Store(true) coreusage.RegisterPlugin(NewLoggerPlugin()) } @@ -36,12 +40,21 @@ func NewLoggerPlugin() *LoggerPlugin { return &LoggerPlugin{stats: defaultReques // - ctx: The context for the usage record // - record: The usage record to aggregate func (p *LoggerPlugin) HandleUsage(ctx context.Context, record coreusage.Record) { + if !statisticsEnabled.Load() { + return + } if p == nil || p.stats == nil { return } p.stats.Record(ctx, record) } +// SetStatisticsEnabled toggles whether in-memory statistics are recorded. +func SetStatisticsEnabled(enabled bool) { statisticsEnabled.Store(enabled) } + +// StatisticsEnabled reports the current recording state. +func StatisticsEnabled() bool { return statisticsEnabled.Load() } + // RequestStatistics maintains aggregated request metrics in memory. type RequestStatistics struct { mu sync.RWMutex @@ -138,6 +151,9 @@ func (s *RequestStatistics) Record(ctx context.Context, record coreusage.Record) if s == nil { return } + if !statisticsEnabled.Load() { + return + } timestamp := record.RequestedAt if timestamp.IsZero() { timestamp = time.Now() diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index 7aa9dc5c..5fca2010 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -477,6 +477,12 @@ func (w *Watcher) reloadConfig() bool { if oldConfig.RemoteManagement.AllowRemote != newConfig.RemoteManagement.AllowRemote { log.Debugf(" remote-management.allow-remote: %t -> %t", oldConfig.RemoteManagement.AllowRemote, newConfig.RemoteManagement.AllowRemote) } + if oldConfig.LoggingToFile != newConfig.LoggingToFile { + log.Debugf(" logging-to-file: %t -> %t", oldConfig.LoggingToFile, newConfig.LoggingToFile) + } + if oldConfig.UsageStatisticsEnabled != newConfig.UsageStatisticsEnabled { + log.Debugf(" usage-statistics-enabled: %t -> %t", oldConfig.UsageStatisticsEnabled, newConfig.UsageStatisticsEnabled) + } } log.Infof("config successfully reloaded, triggering client reload")