feat(logging): make error-logs-max-files configurable

- Add ErrorLogsMaxFiles config field with default value 10
- Support hot-reload via config file changes
- Add Management API: GET/PUT/PATCH /v0/management/error-logs-max-files
- Maintain SDK backward compatibility with NewFileRequestLogger (3 params)
- Add NewFileRequestLoggerWithOptions for custom error log retention

When request logging is disabled, forced error logs are retained up to
the configured limit. Set to 0 to disable cleanup.
This commit is contained in:
sususu98
2026-01-31 17:48:40 +08:00
parent f99cddf97f
commit 6db8d2a28e
7 changed files with 78 additions and 11 deletions

View File

@@ -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

View File

@@ -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).

View File

@@ -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
}
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) {

View File

@@ -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.
@@ -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)
@@ -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 {

View File

@@ -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"`
@@ -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
@@ -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)

View File

@@ -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.
@@ -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.
@@ -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,
}
}
@@ -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:
@@ -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
@@ -462,7 +476,7 @@ 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
}
@@ -470,7 +484,7 @@ func (l *FileRequestLogger) cleanupOldErrorLogs() error {
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)
}

View File

@@ -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
@@ -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)
}