From f173e8ad0556cd56cf285d5d4d1c7b67104f24a7 Mon Sep 17 00:00:00 2001 From: irium Date: Mon, 11 May 2026 22:52:12 +0200 Subject: [PATCH] feat(logger): add size/age/backup rotation for file output (#874) --- apps/backend/cmd/kandev/main.go | 4 ++++ apps/backend/go.mod | 1 + apps/backend/go.sum | 2 ++ apps/backend/internal/common/config/config.go | 11 ++++++++++ apps/backend/internal/common/logger/logger.go | 20 ++++++++++++++----- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/apps/backend/cmd/kandev/main.go b/apps/backend/cmd/kandev/main.go index f29b9cccab..2df995eed3 100644 --- a/apps/backend/cmd/kandev/main.go +++ b/apps/backend/cmd/kandev/main.go @@ -136,6 +136,10 @@ func realMain() int { Level: cfg.Logging.Level, Format: cfg.Logging.Format, OutputPath: cfg.Logging.OutputPath, + MaxSizeMB: cfg.Logging.MaxSizeMB, + MaxBackups: cfg.Logging.MaxBackups, + MaxAgeDays: cfg.Logging.MaxAgeDays, + Compress: cfg.Logging.Compress, }) if err != nil { fmt.Fprintf(os.Stderr, "Failed to initialize logger: %v\n", err) diff --git a/apps/backend/go.mod b/apps/backend/go.mod index 19118df39c..2db8acdccc 100644 --- a/apps/backend/go.mod +++ b/apps/backend/go.mod @@ -109,6 +109,7 @@ require ( google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gotest.tools/v3 v3.5.2 // indirect ) diff --git a/apps/backend/go.sum b/apps/backend/go.sum index 682bc62122..c8fac48f30 100644 --- a/apps/backend/go.sum +++ b/apps/backend/go.sum @@ -683,6 +683,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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= diff --git a/apps/backend/internal/common/config/config.go b/apps/backend/internal/common/config/config.go index e53f964be4..c0873b8d2c 100644 --- a/apps/backend/internal/common/config/config.go +++ b/apps/backend/internal/common/config/config.go @@ -140,6 +140,13 @@ type LoggingConfig struct { Level string `mapstructure:"level"` Format string `mapstructure:"format"` OutputPath string `mapstructure:"outputPath"` + + // Rotation options — apply only when OutputPath is a file path + // (ignored for stdout/stderr). Backed by lumberjack. + MaxSizeMB int `mapstructure:"maxSizeMb"` + MaxBackups int `mapstructure:"maxBackups"` + MaxAgeDays int `mapstructure:"maxAgeDays"` + Compress bool `mapstructure:"compress"` } // RepositoryDiscoveryConfig holds configuration for local repository scanning. @@ -271,6 +278,10 @@ func setDefaults(v *viper.Viper) { v.SetDefault("logging.level", "info") v.SetDefault("logging.format", detectDefaultLogFormat()) v.SetDefault("logging.outputPath", "stdout") + v.SetDefault("logging.maxSizeMb", 100) + v.SetDefault("logging.maxBackups", 5) + v.SetDefault("logging.maxAgeDays", 30) + v.SetDefault("logging.compress", true) // Repository discovery defaults v.SetDefault("repositoryDiscovery.roots", []string{}) diff --git a/apps/backend/internal/common/logger/logger.go b/apps/backend/internal/common/logger/logger.go index 803461e9fd..204f69b4df 100644 --- a/apps/backend/internal/common/logger/logger.go +++ b/apps/backend/internal/common/logger/logger.go @@ -8,6 +8,7 @@ import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" "github.com/kandev/kandev/internal/common/logger/buffer" ) @@ -25,6 +26,13 @@ type LoggingConfig struct { Level string `mapstructure:"level"` // debug, info, warn, error Format string `mapstructure:"format"` // json, console OutputPath string `mapstructure:"output_path"` // stdout, stderr, or file path + + // Rotation options — apply only when OutputPath is a file path + // (ignored for stdout/stderr). Backed by lumberjack. + MaxSizeMB int `mapstructure:"max_size_mb"` // rotate when file exceeds this size; 0 = lumberjack default (100MB) + MaxBackups int `mapstructure:"max_backups"` // max number of rotated files to retain; 0 = unlimited + MaxAgeDays int `mapstructure:"max_age_days"` // max age of rotated files; 0 = unlimited + Compress bool `mapstructure:"compress"` // gzip rotated files } // Logger wraps zap.Logger to provide structured logging with helper methods. @@ -94,11 +102,13 @@ func NewLogger(cfg LoggingConfig) (*Logger, error) { case "stderr": writeSyncer = zapcore.AddSync(os.Stderr) default: - file, err := os.OpenFile(cfg.OutputPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, err - } - writeSyncer = zapcore.AddSync(file) + writeSyncer = zapcore.AddSync(&lumberjack.Logger{ + Filename: cfg.OutputPath, + MaxSize: cfg.MaxSizeMB, + MaxBackups: cfg.MaxBackups, + MaxAge: cfg.MaxAgeDays, + Compress: cfg.Compress, + }) } outputCore := zapcore.NewCore(encoder, writeSyncer, level)