Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ logging-to-file: false
# files are deleted until within the limit. Set to 0 to disable.
logs-max-total-size-mb: 0

# Maximum number of error log files retained when request logging is disabled.
# When exceeded, the oldest error log files are deleted. Default is 10. Set to 0 to disable cleanup.
error-logs-max-files: 10

# When false, disable in-memory usage statistics aggregation
usage-statistics-enabled: false

Expand Down
2 changes: 1 addition & 1 deletion examples/custom-provider/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func main() {
// Optional: add a simple middleware + custom request logger
api.WithMiddleware(func(c *gin.Context) { c.Header("X-Example", "custom-provider"); c.Next() }),
api.WithRequestLoggerFactory(func(cfg *config.Config, cfgPath string) logging.RequestLogger {
return logging.NewFileRequestLogger(true, "logs", filepath.Dir(cfgPath))
return logging.NewFileRequestLoggerWithOptions(true, "logs", filepath.Dir(cfgPath), cfg.ErrorLogsMaxFiles)
}),
).
WithHooks(hooks).
Expand Down
20 changes: 20 additions & 0 deletions internal/api/handlers/management/config_basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,26 @@ func (h *Handler) PutLogsMaxTotalSizeMB(c *gin.Context) {
h.persist(c)
}

// ErrorLogsMaxFiles
func (h *Handler) GetErrorLogsMaxFiles(c *gin.Context) {
c.JSON(200, gin.H{"error-logs-max-files": h.cfg.ErrorLogsMaxFiles})
}
func (h *Handler) PutErrorLogsMaxFiles(c *gin.Context) {
var body struct {
Value *int `json:"value"`
}
if errBindJSON := c.ShouldBindJSON(&body); errBindJSON != nil || body.Value == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
return
}
value := *body.Value
if value < 0 {
value = 10
}
Comment on lines +238 to +240
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The default value 10 is hardcoded here. This value is also used as a default and for validation in internal/config/config.go (lines 509 and 559). To improve maintainability and consistency, consider defining this as a shared constant.

I recommend adding an exported constant in the internal/config package:

// internal/config/config.go
const DefaultErrorLogsMaxFiles = 10

Then, use it across the codebase, including here.

Suggested change
if value < 0 {
value = 10
}
if value < 0 {
value = config.DefaultErrorLogsMaxFiles
}

h.cfg.ErrorLogsMaxFiles = value
h.persist(c)
}

// 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) {
Expand Down
17 changes: 15 additions & 2 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ type ServerOption func(*serverOptionConfig)
func defaultRequestLoggerFactory(cfg *config.Config, configPath string) logging.RequestLogger {
configDir := filepath.Dir(configPath)
if base := util.WritablePath(); base != "" {
return logging.NewFileRequestLogger(cfg.RequestLog, filepath.Join(base, "logs"), configDir)
return logging.NewFileRequestLogger(cfg.RequestLog, filepath.Join(base, "logs"), configDir, cfg.ErrorLogsMaxFiles)
}
return logging.NewFileRequestLogger(cfg.RequestLog, "logs", configDir)
return logging.NewFileRequestLogger(cfg.RequestLog, "logs", configDir, cfg.ErrorLogsMaxFiles)
}

// WithMiddleware appends additional Gin middleware during server construction.
Expand Down Expand Up @@ -497,6 +497,10 @@ func (s *Server) registerManagementRoutes() {
mgmt.PUT("/logs-max-total-size-mb", s.mgmt.PutLogsMaxTotalSizeMB)
mgmt.PATCH("/logs-max-total-size-mb", s.mgmt.PutLogsMaxTotalSizeMB)

mgmt.GET("/error-logs-max-files", s.mgmt.GetErrorLogsMaxFiles)
mgmt.PUT("/error-logs-max-files", s.mgmt.PutErrorLogsMaxFiles)
mgmt.PATCH("/error-logs-max-files", s.mgmt.PutErrorLogsMaxFiles)

mgmt.GET("/usage-statistics-enabled", s.mgmt.GetUsageStatisticsEnabled)
mgmt.PUT("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled)
mgmt.PATCH("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled)
Expand Down Expand Up @@ -907,6 +911,15 @@ func (s *Server) UpdateClients(cfg *config.Config) {
}
}

if s.requestLogger != nil && (oldCfg == nil || oldCfg.ErrorLogsMaxFiles != cfg.ErrorLogsMaxFiles) {
if setter, ok := s.requestLogger.(interface{ SetErrorLogsMaxFiles(int) }); ok {
setter.SetErrorLogsMaxFiles(cfg.ErrorLogsMaxFiles)
}
if oldCfg != nil {
log.Debugf("error_logs_max_files updated from %d to %d", oldCfg.ErrorLogsMaxFiles, cfg.ErrorLogsMaxFiles)
}
}

if oldCfg == nil || oldCfg.DisableCooling != cfg.DisableCooling {
auth.SetQuotaCooldownDisabled(cfg.DisableCooling)
if oldCfg != nil {
Expand Down
9 changes: 9 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ type Config struct {
// When exceeded, the oldest log files are deleted until within the limit. Set to 0 to disable.
LogsMaxTotalSizeMB int `yaml:"logs-max-total-size-mb" json:"logs-max-total-size-mb"`

// ErrorLogsMaxFiles limits the number of error log files retained when request logging is disabled.
// When exceeded, the oldest error log files are deleted. Default is 10. Set to 0 to disable cleanup.
ErrorLogsMaxFiles int `yaml:"error-logs-max-files" json:"error-logs-max-files"`

// UsageStatisticsEnabled toggles in-memory usage aggregation; when false, usage data is discarded.
UsageStatisticsEnabled bool `yaml:"usage-statistics-enabled" json:"usage-statistics-enabled"`

Expand Down Expand Up @@ -502,6 +506,7 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
cfg.Host = "" // Default empty: binds to all interfaces (IPv4 + IPv6)
cfg.LoggingToFile = false
cfg.LogsMaxTotalSizeMB = 0
cfg.ErrorLogsMaxFiles = 10
cfg.UsageStatisticsEnabled = false
cfg.DisableCooling = false
cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient
Expand Down Expand Up @@ -550,6 +555,10 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
cfg.LogsMaxTotalSizeMB = 0
}

if cfg.ErrorLogsMaxFiles < 0 {
cfg.ErrorLogsMaxFiles = 10
}

// Sync request authentication providers with inline API keys for backwards compatibility.
syncInlineAccessProvider(&cfg)

Expand Down
26 changes: 20 additions & 6 deletions internal/logging/request_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ type FileRequestLogger struct {

// logsDir is the directory where log files are stored.
logsDir string

// errorLogsMaxFiles limits the number of error log files retained.
errorLogsMaxFiles int
}

// NewFileRequestLogger creates a new file-based request logger.
Expand All @@ -141,10 +144,11 @@ type FileRequestLogger struct {
// - logsDir: The directory where log files should be stored (can be relative)
// - configDir: The directory of the configuration file; when logsDir is
// relative, it will be resolved relative to this directory
// - errorLogsMaxFiles: Maximum number of error log files to retain (0 = no cleanup)
//
// Returns:
// - *FileRequestLogger: A new file-based request logger instance
func NewFileRequestLogger(enabled bool, logsDir string, configDir string) *FileRequestLogger {
func NewFileRequestLogger(enabled bool, logsDir string, configDir string, errorLogsMaxFiles int) *FileRequestLogger {
// Resolve logsDir relative to the configuration file directory when it's not absolute.
if !filepath.IsAbs(logsDir) {
// If configDir is provided, resolve logsDir relative to it.
Expand All @@ -153,8 +157,9 @@ func NewFileRequestLogger(enabled bool, logsDir string, configDir string) *FileR
}
}
return &FileRequestLogger{
enabled: enabled,
logsDir: logsDir,
enabled: enabled,
logsDir: logsDir,
errorLogsMaxFiles: errorLogsMaxFiles,
}
}

Expand All @@ -175,6 +180,11 @@ func (l *FileRequestLogger) SetEnabled(enabled bool) {
l.enabled = enabled
}

// SetErrorLogsMaxFiles updates the maximum number of error log files to retain.
func (l *FileRequestLogger) SetErrorLogsMaxFiles(maxFiles int) {
l.errorLogsMaxFiles = maxFiles
}

// LogRequest logs a complete non-streaming request/response cycle to a file.
//
// Parameters:
Expand Down Expand Up @@ -433,8 +443,12 @@ func (l *FileRequestLogger) sanitizeForFilename(path string) string {
return sanitized
}

// cleanupOldErrorLogs keeps only the newest 10 forced error log files.
// cleanupOldErrorLogs keeps only the newest errorLogsMaxFiles forced error log files.
func (l *FileRequestLogger) cleanupOldErrorLogs() error {
if l.errorLogsMaxFiles <= 0 {
return nil
}

entries, errRead := os.ReadDir(l.logsDir)
if errRead != nil {
return errRead
Expand Down Expand Up @@ -462,15 +476,15 @@ func (l *FileRequestLogger) cleanupOldErrorLogs() error {
files = append(files, logFile{name: name, modTime: info.ModTime()})
}

if len(files) <= 10 {
if len(files) <= l.errorLogsMaxFiles {
return nil
}

sort.Slice(files, func(i, j int) bool {
return files[i].modTime.After(files[j].modTime)
})

for _, file := range files[10:] {
for _, file := range files[l.errorLogsMaxFiles:] {
if errRemove := os.Remove(filepath.Join(l.logsDir, file.name)); errRemove != nil {
log.WithError(errRemove).Warnf("failed to remove old error log: %s", file.name)
}
Expand Down
11 changes: 9 additions & 2 deletions sdk/logging/request_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package logging

import internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging"

const defaultErrorLogsMaxFiles = 10

// RequestLogger defines the interface for logging HTTP requests and responses.
type RequestLogger = internallogging.RequestLogger

Expand All @@ -12,7 +14,12 @@ type StreamingLogWriter = internallogging.StreamingLogWriter
// FileRequestLogger implements RequestLogger using file-based storage.
type FileRequestLogger = internallogging.FileRequestLogger

// NewFileRequestLogger creates a new file-based request logger.
// NewFileRequestLogger creates a new file-based request logger with default error log retention (10 files).
func NewFileRequestLogger(enabled bool, logsDir string, configDir string) *FileRequestLogger {
return internallogging.NewFileRequestLogger(enabled, logsDir, configDir)
return internallogging.NewFileRequestLogger(enabled, logsDir, configDir, defaultErrorLogsMaxFiles)
}

// NewFileRequestLoggerWithOptions creates a new file-based request logger with configurable error log retention.
func NewFileRequestLoggerWithOptions(enabled bool, logsDir string, configDir string, errorLogsMaxFiles int) *FileRequestLogger {
return internallogging.NewFileRequestLogger(enabled, logsDir, configDir, errorLogsMaxFiles)
}