diff --git a/auth-center/cmd/auth-center/init.go b/auth-center/cmd/auth-center/init.go deleted file mode 100644 index b185b338..00000000 --- a/auth-center/cmd/auth-center/init.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "os" - "strconv" - "time" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" -) - -func initLogger(level string) { - l := zerolog.New(os.Stdout). - With(). - Timestamp(). - Caller(). - Logger() - - zerolog.TimeFieldFormat = time.RFC3339Nano - zerolog.ErrorFieldName = "err" - zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { - short := file - for i := len(file) - 1; i > 0; i-- { - if file[i] == '/' { - short = file[i+1:] - break - } - } - file = short - return file + ":" + strconv.Itoa(line) - } - - log.Logger = l - - switch level { - case "DEBUG": - zerolog.SetGlobalLevel(zerolog.DebugLevel) - case "INFO": - zerolog.SetGlobalLevel(zerolog.InfoLevel) - case "WARN": - zerolog.SetGlobalLevel(zerolog.WarnLevel) - case "ERROR": - zerolog.SetGlobalLevel(zerolog.ErrorLevel) - case "FATAL": - zerolog.SetGlobalLevel(zerolog.FatalLevel) - default: - zerolog.SetGlobalLevel(zerolog.TraceLevel) - } -} diff --git a/auth-center/cmd/auth-center/main.go b/auth-center/cmd/auth-center/main.go index fc6c6c82..ab38f87a 100644 --- a/auth-center/cmd/auth-center/main.go +++ b/auth-center/cmd/auth-center/main.go @@ -21,6 +21,7 @@ import ( "github.com/runtime-radar/runtime-radar/auth-center/pkg/database" "github.com/runtime-radar/runtime-radar/auth-center/pkg/server" "github.com/runtime-radar/runtime-radar/auth-center/pkg/service" + "github.com/runtime-radar/runtime-radar/lib/logger" "github.com/runtime-radar/runtime-radar/lib/security" "github.com/runtime-radar/runtime-radar/lib/security/jwt" "github.com/runtime-radar/runtime-radar/lib/server/healthcheck" @@ -67,7 +68,7 @@ func readPasswordDictionary(path string) ([]string, error) { func main() { cfg := config.New() - initLogger(cfg.LogLevel) + logger.Init("", cfg.LogLevel) log.Info(). Str("build_release", build.Release). diff --git a/cluster-manager/cmd/cluster-manager/init.go b/auth-center/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go similarity index 85% rename from cluster-manager/cmd/cluster-manager/init.go rename to auth-center/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go index 9e1801ff..f1e606a1 100644 --- a/cluster-manager/cmd/cluster-manager/init.go +++ b/auth-center/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go @@ -1,4 +1,4 @@ -package main +package logger import ( "io" @@ -8,19 +8,18 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "github.com/runtime-radar/runtime-radar/lib/logger" ) +// Logging configuration const ( - // Logging configuration logMaxNum = 3 logMaxSize = 10 * 1024 * 1024 // 10MB ) -func initLogger(file, level string) { +func Init(file, level string) { var out io.Writer = os.Stdout if file != "" { - rl, err := logger.NewRotateFileWriter(file, logMaxNum, logMaxSize) + rl, err := NewRotateFileWriter(file, logMaxNum, logMaxSize) if err != nil { log.Fatal().Msgf("### Failed to create log file: %v", err) } diff --git a/auth-center/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go b/auth-center/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go new file mode 100644 index 00000000..50373280 --- /dev/null +++ b/auth-center/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go @@ -0,0 +1,165 @@ +package logger + +import ( + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strconv" + "sync" +) + +var _ io.Writer = (*RotateFileWriter)(nil) + +// RotateFileWriter is a writer that rotates log files based on size and number of backups. +type RotateFileWriter struct { + mu sync.Mutex + + filename string + basename string + dir string + maxSize int + maxBackups int + + file *os.File + size int + nameMatcher *regexp.Regexp +} + +// NewRotateFileWriter creates a new RotateFileWriter for the given filename and +// configuration. It ensures the log directory exists, compiles a regexp to match +// rotated log files, opens the initial log file, and returns a RotateFileWriter. +func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { + if filename == "" { + return nil, fmt.Errorf("filename cannot be empty") + } + + if maxBackups <= 0 { + return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") + } + + if maxSize <= 0 { + return nil, fmt.Errorf("maxSize cannot be less or equal to zero") + } + + filename = filepath.Clean(filename) + basename := filepath.Base(filename) + dir := filepath.Dir(filename) + + err := os.MkdirAll(dir, 0755) + if err != nil { + return nil, fmt.Errorf("can't make directories for new logfile: %w", err) + } + + r, err := regexp.Compile(basename + `\.[0-9]+$`) + if err != nil { + return nil, fmt.Errorf("can't compile regexp: %w", err) + } + + file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, fmt.Errorf("can't open log file: %w", err) + } + + info, err := file.Stat() + if err != nil { + return nil, fmt.Errorf("can't get info about file: %w", err) + } + + rfw := &RotateFileWriter{ + filename: filename, + basename: basename, + dir: dir, + maxSize: maxSize, + maxBackups: maxBackups, + nameMatcher: r, + file: file, + size: int(info.Size()), + } + + return rfw, nil +} + +// Write implements the io.Writer interface. It writes the given bytes to the +// rolling log file, handling log rotation when the size exceeds the max. +// It locks access to the file for the duration of the write. +func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { + rfw.mu.Lock() + defer rfw.mu.Unlock() + + if rfw.size+len(p) > rfw.maxSize { + if err := rfw.rotate(); err != nil { + return 0, fmt.Errorf("can't rotate log file: %w", err) + } + } + + n, err = rfw.file.Write(p) + rfw.size += n + + return +} + +// rotate sequentially increments old log suffixes, +// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc +// and then closes current file and renames it to the first backup. +func (rfw *RotateFileWriter) rotate() error { + if rfw.maxBackups == 0 { + return nil + } + + cnt := 0 + err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + if d.IsDir() && path != rfw.dir { + return filepath.SkipDir + } + + if rfw.nameMatcher.MatchString(d.Name()) { + cnt++ + } + + return nil + }) + + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + for i := cnt; i > 0; i-- { + oldName := rfw.filename + "." + strconv.Itoa(i) + newName := rfw.filename + "." + strconv.Itoa(i+1) + // just remove everything over `rfw.maxBackups` + if i >= rfw.maxBackups { + if err := os.Remove(oldName); err != nil { + return fmt.Errorf("can't remove old log file: %w", err) + } + continue + } + + if err := os.Rename(oldName, newName); err != nil { + return fmt.Errorf("can't rename old log file: %w", err) + } + } + + // Try to close current file and rename it + if err := rfw.file.Close(); err != nil { + return fmt.Errorf("can't close current log file: %w", err) + } + + if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { + return fmt.Errorf("can't rename current log file: %w", err) + } + + file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("can't open log file: %w", err) + } + rfw.file = file + rfw.size = 0 + + return nil +} diff --git a/auth-center/vendor/modules.txt b/auth-center/vendor/modules.txt index f770fe1e..a345f74a 100644 --- a/auth-center/vendor/modules.txt +++ b/auth-center/vendor/modules.txt @@ -778,6 +778,7 @@ github.com/rs/zerolog/log ## explicit; go 1.25 github.com/runtime-radar/runtime-radar/lib/config github.com/runtime-radar/runtime-radar/lib/errcommon +github.com/runtime-radar/runtime-radar/lib/logger github.com/runtime-radar/runtime-radar/lib/security github.com/runtime-radar/runtime-radar/lib/security/cipher github.com/runtime-radar/runtime-radar/lib/security/jwt diff --git a/cluster-manager/cmd/cluster-manager/main.go b/cluster-manager/cmd/cluster-manager/main.go index 54baadcb..d67511c3 100644 --- a/cluster-manager/cmd/cluster-manager/main.go +++ b/cluster-manager/cmd/cluster-manager/main.go @@ -19,6 +19,7 @@ import ( "github.com/runtime-radar/runtime-radar/cluster-manager/pkg/database" "github.com/runtime-radar/runtime-radar/cluster-manager/pkg/server" "github.com/runtime-radar/runtime-radar/cluster-manager/pkg/service" + "github.com/runtime-radar/runtime-radar/lib/logger" "github.com/runtime-radar/runtime-radar/lib/security" "github.com/runtime-radar/runtime-radar/lib/security/cipher" "github.com/runtime-radar/runtime-radar/lib/security/jwt" @@ -50,7 +51,7 @@ var ( func main() { cfg := config.New() - initLogger(cfg.LogFile, cfg.LogLevel) + logger.Init(cfg.LogFile, cfg.LogLevel) log.Info().Str("build_release", build.Release).Str("build_branch", build.Branch).Str("build_commit", build.Commit).Str("build_date", build.Date).Msgf("-> %s started", build.AppName) defer log.Info().Msgf("<- %s exited", build.AppName) diff --git a/cluster-manager/cmd/cluster-manager/main_test.go b/cluster-manager/cmd/cluster-manager/main_test.go index dfc68249..89c50762 100644 --- a/cluster-manager/cmd/cluster-manager/main_test.go +++ b/cluster-manager/cmd/cluster-manager/main_test.go @@ -16,6 +16,7 @@ import ( "github.com/runtime-radar/runtime-radar/cluster-manager/pkg/config" "github.com/runtime-radar/runtime-radar/cluster-manager/pkg/database" "github.com/runtime-radar/runtime-radar/cluster-manager/pkg/model" + "github.com/runtime-radar/runtime-radar/lib/logger" "github.com/runtime-radar/runtime-radar/lib/security" "github.com/runtime-radar/runtime-radar/lib/security/cipher" "github.com/runtime-radar/runtime-radar/lib/security/jwt" @@ -56,9 +57,9 @@ func TestMain(m *testing.M) { cfg.EncryptionKey = hex.EncodeToString(security.Rand(32)) if testing.Verbose() { - initLogger("", "DEBUG") + logger.Init("", "DEBUG") } else { - initLogger("", "INFO") + logger.Init("", "INFO") } crypter, err := cipher.NewCrypt(cfg.EncryptionKey) diff --git a/cluster-manager/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go b/cluster-manager/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go index 50373280..f1e606a1 100644 --- a/cluster-manager/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go +++ b/cluster-manager/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go @@ -1,165 +1,65 @@ package logger import ( - "fmt" "io" "os" - "path/filepath" - "regexp" "strconv" - "sync" -) - -var _ io.Writer = (*RotateFileWriter)(nil) - -// RotateFileWriter is a writer that rotates log files based on size and number of backups. -type RotateFileWriter struct { - mu sync.Mutex - - filename string - basename string - dir string - maxSize int - maxBackups int - - file *os.File - size int - nameMatcher *regexp.Regexp -} - -// NewRotateFileWriter creates a new RotateFileWriter for the given filename and -// configuration. It ensures the log directory exists, compiles a regexp to match -// rotated log files, opens the initial log file, and returns a RotateFileWriter. -func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { - if filename == "" { - return nil, fmt.Errorf("filename cannot be empty") - } - - if maxBackups <= 0 { - return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") - } - - if maxSize <= 0 { - return nil, fmt.Errorf("maxSize cannot be less or equal to zero") - } + "time" - filename = filepath.Clean(filename) - basename := filepath.Base(filename) - dir := filepath.Dir(filename) - - err := os.MkdirAll(dir, 0755) - if err != nil { - return nil, fmt.Errorf("can't make directories for new logfile: %w", err) - } - - r, err := regexp.Compile(basename + `\.[0-9]+$`) - if err != nil { - return nil, fmt.Errorf("can't compile regexp: %w", err) - } - - file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, fmt.Errorf("can't open log file: %w", err) - } - - info, err := file.Stat() - if err != nil { - return nil, fmt.Errorf("can't get info about file: %w", err) - } - - rfw := &RotateFileWriter{ - filename: filename, - basename: basename, - dir: dir, - maxSize: maxSize, - maxBackups: maxBackups, - nameMatcher: r, - file: file, - size: int(info.Size()), - } - - return rfw, nil -} - -// Write implements the io.Writer interface. It writes the given bytes to the -// rolling log file, handling log rotation when the size exceeds the max. -// It locks access to the file for the duration of the write. -func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { - rfw.mu.Lock() - defer rfw.mu.Unlock() - - if rfw.size+len(p) > rfw.maxSize { - if err := rfw.rotate(); err != nil { - return 0, fmt.Errorf("can't rotate log file: %w", err) - } - } - - n, err = rfw.file.Write(p) - rfw.size += n - - return -} + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) -// rotate sequentially increments old log suffixes, -// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc -// and then closes current file and renames it to the first backup. -func (rfw *RotateFileWriter) rotate() error { - if rfw.maxBackups == 0 { - return nil - } +// Logging configuration +const ( + logMaxNum = 3 + logMaxSize = 10 * 1024 * 1024 // 10MB +) - cnt := 0 - err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { +func Init(file, level string) { + var out io.Writer = os.Stdout + if file != "" { + rl, err := NewRotateFileWriter(file, logMaxNum, logMaxSize) if err != nil { - return fmt.Errorf("can't walk directory: %w", err) - } - - if d.IsDir() && path != rfw.dir { - return filepath.SkipDir + log.Fatal().Msgf("### Failed to create log file: %v", err) } - - if rfw.nameMatcher.MatchString(d.Name()) { - cnt++ - } - - return nil - }) - - if err != nil { - return fmt.Errorf("can't walk directory: %w", err) + out = io.MultiWriter(os.Stdout, rl) } - for i := cnt; i > 0; i-- { - oldName := rfw.filename + "." + strconv.Itoa(i) - newName := rfw.filename + "." + strconv.Itoa(i+1) - // just remove everything over `rfw.maxBackups` - if i >= rfw.maxBackups { - if err := os.Remove(oldName); err != nil { - return fmt.Errorf("can't remove old log file: %w", err) + l := zerolog.New(out). + With(). + Timestamp(). + Caller(). + Logger() + + zerolog.TimeFieldFormat = time.RFC3339Nano + zerolog.ErrorFieldName = "err" + zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { + short := file + for i := len(file) - 1; i > 0; i-- { + if file[i] == '/' { + short = file[i+1:] + break } - continue - } - - if err := os.Rename(oldName, newName); err != nil { - return fmt.Errorf("can't rename old log file: %w", err) } + file = short + return file + ":" + strconv.Itoa(line) } - // Try to close current file and rename it - if err := rfw.file.Close(); err != nil { - return fmt.Errorf("can't close current log file: %w", err) + log.Logger = l + + switch level { + case "DEBUG": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "INFO": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "WARN": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "ERROR": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + case "FATAL": + zerolog.SetGlobalLevel(zerolog.FatalLevel) + default: + zerolog.SetGlobalLevel(zerolog.TraceLevel) } - - if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { - return fmt.Errorf("can't rename current log file: %w", err) - } - - file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - return fmt.Errorf("can't open log file: %w", err) - } - rfw.file = file - rfw.size = 0 - - return nil } diff --git a/cluster-manager/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go b/cluster-manager/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go new file mode 100644 index 00000000..50373280 --- /dev/null +++ b/cluster-manager/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go @@ -0,0 +1,165 @@ +package logger + +import ( + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strconv" + "sync" +) + +var _ io.Writer = (*RotateFileWriter)(nil) + +// RotateFileWriter is a writer that rotates log files based on size and number of backups. +type RotateFileWriter struct { + mu sync.Mutex + + filename string + basename string + dir string + maxSize int + maxBackups int + + file *os.File + size int + nameMatcher *regexp.Regexp +} + +// NewRotateFileWriter creates a new RotateFileWriter for the given filename and +// configuration. It ensures the log directory exists, compiles a regexp to match +// rotated log files, opens the initial log file, and returns a RotateFileWriter. +func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { + if filename == "" { + return nil, fmt.Errorf("filename cannot be empty") + } + + if maxBackups <= 0 { + return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") + } + + if maxSize <= 0 { + return nil, fmt.Errorf("maxSize cannot be less or equal to zero") + } + + filename = filepath.Clean(filename) + basename := filepath.Base(filename) + dir := filepath.Dir(filename) + + err := os.MkdirAll(dir, 0755) + if err != nil { + return nil, fmt.Errorf("can't make directories for new logfile: %w", err) + } + + r, err := regexp.Compile(basename + `\.[0-9]+$`) + if err != nil { + return nil, fmt.Errorf("can't compile regexp: %w", err) + } + + file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, fmt.Errorf("can't open log file: %w", err) + } + + info, err := file.Stat() + if err != nil { + return nil, fmt.Errorf("can't get info about file: %w", err) + } + + rfw := &RotateFileWriter{ + filename: filename, + basename: basename, + dir: dir, + maxSize: maxSize, + maxBackups: maxBackups, + nameMatcher: r, + file: file, + size: int(info.Size()), + } + + return rfw, nil +} + +// Write implements the io.Writer interface. It writes the given bytes to the +// rolling log file, handling log rotation when the size exceeds the max. +// It locks access to the file for the duration of the write. +func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { + rfw.mu.Lock() + defer rfw.mu.Unlock() + + if rfw.size+len(p) > rfw.maxSize { + if err := rfw.rotate(); err != nil { + return 0, fmt.Errorf("can't rotate log file: %w", err) + } + } + + n, err = rfw.file.Write(p) + rfw.size += n + + return +} + +// rotate sequentially increments old log suffixes, +// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc +// and then closes current file and renames it to the first backup. +func (rfw *RotateFileWriter) rotate() error { + if rfw.maxBackups == 0 { + return nil + } + + cnt := 0 + err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + if d.IsDir() && path != rfw.dir { + return filepath.SkipDir + } + + if rfw.nameMatcher.MatchString(d.Name()) { + cnt++ + } + + return nil + }) + + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + for i := cnt; i > 0; i-- { + oldName := rfw.filename + "." + strconv.Itoa(i) + newName := rfw.filename + "." + strconv.Itoa(i+1) + // just remove everything over `rfw.maxBackups` + if i >= rfw.maxBackups { + if err := os.Remove(oldName); err != nil { + return fmt.Errorf("can't remove old log file: %w", err) + } + continue + } + + if err := os.Rename(oldName, newName); err != nil { + return fmt.Errorf("can't rename old log file: %w", err) + } + } + + // Try to close current file and rename it + if err := rfw.file.Close(); err != nil { + return fmt.Errorf("can't close current log file: %w", err) + } + + if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { + return fmt.Errorf("can't rename current log file: %w", err) + } + + file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("can't open log file: %w", err) + } + rfw.file = file + rfw.size = 0 + + return nil +} diff --git a/cs-manager/cmd/cs-manager/init.go b/cs-manager/cmd/cs-manager/init.go deleted file mode 100644 index 3c24ebcf..00000000 --- a/cs-manager/cmd/cs-manager/init.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "io" - "os" - "strconv" - "time" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - cs_logger "github.com/runtime-radar/runtime-radar/lib/logger" -) - -const ( - // Logging configuration - logMaxNum = 3 - logMaxSize = 10 * 1024 * 1024 // 10MB -) - -func initLogger(file, level string) { - var out io.Writer = os.Stdout - if file != "" { - rl, err := cs_logger.NewRotateFileWriter(file, logMaxNum, logMaxSize) - if err != nil { - log.Fatal().Msgf("### Failed to create log file: %v", err) - } - out = io.MultiWriter(os.Stdout, rl) - } - - l := zerolog.New(out). - With(). - Timestamp(). - Caller(). - Logger() - - zerolog.TimeFieldFormat = time.RFC3339Nano - zerolog.ErrorFieldName = "err" - zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { - short := file - for i := len(file) - 1; i > 0; i-- { - if file[i] == '/' { - short = file[i+1:] - break - } - } - file = short - return file + ":" + strconv.Itoa(line) - } - - log.Logger = l - - switch level { - case "DEBUG": - zerolog.SetGlobalLevel(zerolog.DebugLevel) - case "INFO": - zerolog.SetGlobalLevel(zerolog.InfoLevel) - case "WARN": - zerolog.SetGlobalLevel(zerolog.WarnLevel) - case "ERROR": - zerolog.SetGlobalLevel(zerolog.ErrorLevel) - case "FATAL": - zerolog.SetGlobalLevel(zerolog.FatalLevel) - default: - zerolog.SetGlobalLevel(zerolog.TraceLevel) - } -} diff --git a/cs-manager/cmd/cs-manager/main.go b/cs-manager/cmd/cs-manager/main.go index 48e1f1ea..dadb0ad8 100644 --- a/cs-manager/cmd/cs-manager/main.go +++ b/cs-manager/cmd/cs-manager/main.go @@ -27,6 +27,7 @@ import ( "github.com/runtime-radar/runtime-radar/cs-manager/pkg/server" "github.com/runtime-radar/runtime-radar/cs-manager/pkg/service" "github.com/runtime-radar/runtime-radar/cs-manager/pkg/state" + "github.com/runtime-radar/runtime-radar/lib/logger" "github.com/runtime-radar/runtime-radar/lib/security" "github.com/runtime-radar/runtime-radar/lib/security/jwt" "github.com/runtime-radar/runtime-radar/lib/server/healthcheck" @@ -57,7 +58,7 @@ var ( func main() { cfg := config.New() - initLogger(cfg.LogFile, cfg.LogLevel) + logger.Init(cfg.LogFile, cfg.LogLevel) log.Info().Str("build_release", build.Release).Str("build_branch", build.Branch).Str("build_commit", build.Commit).Str("build_date", build.Date).Msgf("-> %s started", build.AppName) defer log.Info().Msgf("<- %s exited", build.AppName) diff --git a/cs-manager/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go b/cs-manager/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go index 50373280..f1e606a1 100644 --- a/cs-manager/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go +++ b/cs-manager/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go @@ -1,165 +1,65 @@ package logger import ( - "fmt" "io" "os" - "path/filepath" - "regexp" "strconv" - "sync" -) - -var _ io.Writer = (*RotateFileWriter)(nil) - -// RotateFileWriter is a writer that rotates log files based on size and number of backups. -type RotateFileWriter struct { - mu sync.Mutex - - filename string - basename string - dir string - maxSize int - maxBackups int - - file *os.File - size int - nameMatcher *regexp.Regexp -} - -// NewRotateFileWriter creates a new RotateFileWriter for the given filename and -// configuration. It ensures the log directory exists, compiles a regexp to match -// rotated log files, opens the initial log file, and returns a RotateFileWriter. -func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { - if filename == "" { - return nil, fmt.Errorf("filename cannot be empty") - } - - if maxBackups <= 0 { - return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") - } - - if maxSize <= 0 { - return nil, fmt.Errorf("maxSize cannot be less or equal to zero") - } + "time" - filename = filepath.Clean(filename) - basename := filepath.Base(filename) - dir := filepath.Dir(filename) - - err := os.MkdirAll(dir, 0755) - if err != nil { - return nil, fmt.Errorf("can't make directories for new logfile: %w", err) - } - - r, err := regexp.Compile(basename + `\.[0-9]+$`) - if err != nil { - return nil, fmt.Errorf("can't compile regexp: %w", err) - } - - file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, fmt.Errorf("can't open log file: %w", err) - } - - info, err := file.Stat() - if err != nil { - return nil, fmt.Errorf("can't get info about file: %w", err) - } - - rfw := &RotateFileWriter{ - filename: filename, - basename: basename, - dir: dir, - maxSize: maxSize, - maxBackups: maxBackups, - nameMatcher: r, - file: file, - size: int(info.Size()), - } - - return rfw, nil -} - -// Write implements the io.Writer interface. It writes the given bytes to the -// rolling log file, handling log rotation when the size exceeds the max. -// It locks access to the file for the duration of the write. -func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { - rfw.mu.Lock() - defer rfw.mu.Unlock() - - if rfw.size+len(p) > rfw.maxSize { - if err := rfw.rotate(); err != nil { - return 0, fmt.Errorf("can't rotate log file: %w", err) - } - } - - n, err = rfw.file.Write(p) - rfw.size += n - - return -} + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) -// rotate sequentially increments old log suffixes, -// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc -// and then closes current file and renames it to the first backup. -func (rfw *RotateFileWriter) rotate() error { - if rfw.maxBackups == 0 { - return nil - } +// Logging configuration +const ( + logMaxNum = 3 + logMaxSize = 10 * 1024 * 1024 // 10MB +) - cnt := 0 - err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { +func Init(file, level string) { + var out io.Writer = os.Stdout + if file != "" { + rl, err := NewRotateFileWriter(file, logMaxNum, logMaxSize) if err != nil { - return fmt.Errorf("can't walk directory: %w", err) - } - - if d.IsDir() && path != rfw.dir { - return filepath.SkipDir + log.Fatal().Msgf("### Failed to create log file: %v", err) } - - if rfw.nameMatcher.MatchString(d.Name()) { - cnt++ - } - - return nil - }) - - if err != nil { - return fmt.Errorf("can't walk directory: %w", err) + out = io.MultiWriter(os.Stdout, rl) } - for i := cnt; i > 0; i-- { - oldName := rfw.filename + "." + strconv.Itoa(i) - newName := rfw.filename + "." + strconv.Itoa(i+1) - // just remove everything over `rfw.maxBackups` - if i >= rfw.maxBackups { - if err := os.Remove(oldName); err != nil { - return fmt.Errorf("can't remove old log file: %w", err) + l := zerolog.New(out). + With(). + Timestamp(). + Caller(). + Logger() + + zerolog.TimeFieldFormat = time.RFC3339Nano + zerolog.ErrorFieldName = "err" + zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { + short := file + for i := len(file) - 1; i > 0; i-- { + if file[i] == '/' { + short = file[i+1:] + break } - continue - } - - if err := os.Rename(oldName, newName); err != nil { - return fmt.Errorf("can't rename old log file: %w", err) } + file = short + return file + ":" + strconv.Itoa(line) } - // Try to close current file and rename it - if err := rfw.file.Close(); err != nil { - return fmt.Errorf("can't close current log file: %w", err) + log.Logger = l + + switch level { + case "DEBUG": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "INFO": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "WARN": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "ERROR": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + case "FATAL": + zerolog.SetGlobalLevel(zerolog.FatalLevel) + default: + zerolog.SetGlobalLevel(zerolog.TraceLevel) } - - if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { - return fmt.Errorf("can't rename current log file: %w", err) - } - - file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - return fmt.Errorf("can't open log file: %w", err) - } - rfw.file = file - rfw.size = 0 - - return nil } diff --git a/cs-manager/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go b/cs-manager/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go new file mode 100644 index 00000000..50373280 --- /dev/null +++ b/cs-manager/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go @@ -0,0 +1,165 @@ +package logger + +import ( + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strconv" + "sync" +) + +var _ io.Writer = (*RotateFileWriter)(nil) + +// RotateFileWriter is a writer that rotates log files based on size and number of backups. +type RotateFileWriter struct { + mu sync.Mutex + + filename string + basename string + dir string + maxSize int + maxBackups int + + file *os.File + size int + nameMatcher *regexp.Regexp +} + +// NewRotateFileWriter creates a new RotateFileWriter for the given filename and +// configuration. It ensures the log directory exists, compiles a regexp to match +// rotated log files, opens the initial log file, and returns a RotateFileWriter. +func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { + if filename == "" { + return nil, fmt.Errorf("filename cannot be empty") + } + + if maxBackups <= 0 { + return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") + } + + if maxSize <= 0 { + return nil, fmt.Errorf("maxSize cannot be less or equal to zero") + } + + filename = filepath.Clean(filename) + basename := filepath.Base(filename) + dir := filepath.Dir(filename) + + err := os.MkdirAll(dir, 0755) + if err != nil { + return nil, fmt.Errorf("can't make directories for new logfile: %w", err) + } + + r, err := regexp.Compile(basename + `\.[0-9]+$`) + if err != nil { + return nil, fmt.Errorf("can't compile regexp: %w", err) + } + + file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, fmt.Errorf("can't open log file: %w", err) + } + + info, err := file.Stat() + if err != nil { + return nil, fmt.Errorf("can't get info about file: %w", err) + } + + rfw := &RotateFileWriter{ + filename: filename, + basename: basename, + dir: dir, + maxSize: maxSize, + maxBackups: maxBackups, + nameMatcher: r, + file: file, + size: int(info.Size()), + } + + return rfw, nil +} + +// Write implements the io.Writer interface. It writes the given bytes to the +// rolling log file, handling log rotation when the size exceeds the max. +// It locks access to the file for the duration of the write. +func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { + rfw.mu.Lock() + defer rfw.mu.Unlock() + + if rfw.size+len(p) > rfw.maxSize { + if err := rfw.rotate(); err != nil { + return 0, fmt.Errorf("can't rotate log file: %w", err) + } + } + + n, err = rfw.file.Write(p) + rfw.size += n + + return +} + +// rotate sequentially increments old log suffixes, +// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc +// and then closes current file and renames it to the first backup. +func (rfw *RotateFileWriter) rotate() error { + if rfw.maxBackups == 0 { + return nil + } + + cnt := 0 + err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + if d.IsDir() && path != rfw.dir { + return filepath.SkipDir + } + + if rfw.nameMatcher.MatchString(d.Name()) { + cnt++ + } + + return nil + }) + + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + for i := cnt; i > 0; i-- { + oldName := rfw.filename + "." + strconv.Itoa(i) + newName := rfw.filename + "." + strconv.Itoa(i+1) + // just remove everything over `rfw.maxBackups` + if i >= rfw.maxBackups { + if err := os.Remove(oldName); err != nil { + return fmt.Errorf("can't remove old log file: %w", err) + } + continue + } + + if err := os.Rename(oldName, newName); err != nil { + return fmt.Errorf("can't rename old log file: %w", err) + } + } + + // Try to close current file and rename it + if err := rfw.file.Close(); err != nil { + return fmt.Errorf("can't close current log file: %w", err) + } + + if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { + return fmt.Errorf("can't rename current log file: %w", err) + } + + file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("can't open log file: %w", err) + } + rfw.file = file + rfw.size = 0 + + return nil +} diff --git a/event-processor/cmd/event-processor/init.go b/event-processor/cmd/event-processor/init.go deleted file mode 100644 index 877207fe..00000000 --- a/event-processor/cmd/event-processor/init.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "io" - "os" - "strconv" - "time" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - lib_logger "github.com/runtime-radar/runtime-radar/lib/logger" -) - -const ( - // Logging configuration - logMaxNum = 3 - logMaxSize = 10 * 1024 * 1024 // 10MB -) - -func initLogger(file, level string) { - var out io.Writer = os.Stdout - if file != "" { - rl, err := lib_logger.NewRotateFileWriter(file, logMaxNum, logMaxSize) - if err != nil { - log.Fatal().Msgf("### Failed to create log file: %v", err) - } - out = io.MultiWriter(os.Stdout, rl) - } - - l := zerolog.New(out). - With(). - Timestamp(). - Caller(). - Logger() - - zerolog.TimeFieldFormat = time.RFC3339Nano - zerolog.ErrorFieldName = "err" - zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { - short := file - for i := len(file) - 1; i > 0; i-- { - if file[i] == '/' { - short = file[i+1:] - break - } - } - file = short - return file + ":" + strconv.Itoa(line) - } - - log.Logger = l - - switch level { - case "DEBUG": - zerolog.SetGlobalLevel(zerolog.DebugLevel) - case "INFO": - zerolog.SetGlobalLevel(zerolog.InfoLevel) - case "WARN": - zerolog.SetGlobalLevel(zerolog.WarnLevel) - case "ERROR": - zerolog.SetGlobalLevel(zerolog.ErrorLevel) - case "FATAL": - zerolog.SetGlobalLevel(zerolog.FatalLevel) - default: - zerolog.SetGlobalLevel(zerolog.TraceLevel) - } -} diff --git a/event-processor/cmd/event-processor/main.go b/event-processor/cmd/event-processor/main.go index 0783d2af..e02e92e6 100644 --- a/event-processor/cmd/event-processor/main.go +++ b/event-processor/cmd/event-processor/main.go @@ -30,6 +30,7 @@ import ( "github.com/runtime-radar/runtime-radar/event-processor/pkg/processor/updater" "github.com/runtime-radar/runtime-radar/event-processor/pkg/server" "github.com/runtime-radar/runtime-radar/event-processor/pkg/service" + "github.com/runtime-radar/runtime-radar/lib/logger" "github.com/runtime-radar/runtime-radar/lib/rabbit" "github.com/runtime-radar/runtime-radar/lib/security" "github.com/runtime-radar/runtime-radar/lib/security/jwt" @@ -65,7 +66,7 @@ var ( func main() { cfg := config.New() - initLogger(cfg.LogFile, cfg.LogLevel) + logger.Init(cfg.LogFile, cfg.LogLevel) log.Info().Str("build_release", build.Release).Str("build_branch", build.Branch).Str("build_commit", build.Commit).Str("build_date", build.Date).Msgf("-> %s started", build.AppName) defer log.Info().Msgf("<- %s exited", build.AppName) diff --git a/event-processor/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go b/event-processor/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go index 50373280..f1e606a1 100644 --- a/event-processor/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go +++ b/event-processor/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go @@ -1,165 +1,65 @@ package logger import ( - "fmt" "io" "os" - "path/filepath" - "regexp" "strconv" - "sync" -) - -var _ io.Writer = (*RotateFileWriter)(nil) - -// RotateFileWriter is a writer that rotates log files based on size and number of backups. -type RotateFileWriter struct { - mu sync.Mutex - - filename string - basename string - dir string - maxSize int - maxBackups int - - file *os.File - size int - nameMatcher *regexp.Regexp -} - -// NewRotateFileWriter creates a new RotateFileWriter for the given filename and -// configuration. It ensures the log directory exists, compiles a regexp to match -// rotated log files, opens the initial log file, and returns a RotateFileWriter. -func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { - if filename == "" { - return nil, fmt.Errorf("filename cannot be empty") - } - - if maxBackups <= 0 { - return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") - } - - if maxSize <= 0 { - return nil, fmt.Errorf("maxSize cannot be less or equal to zero") - } + "time" - filename = filepath.Clean(filename) - basename := filepath.Base(filename) - dir := filepath.Dir(filename) - - err := os.MkdirAll(dir, 0755) - if err != nil { - return nil, fmt.Errorf("can't make directories for new logfile: %w", err) - } - - r, err := regexp.Compile(basename + `\.[0-9]+$`) - if err != nil { - return nil, fmt.Errorf("can't compile regexp: %w", err) - } - - file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, fmt.Errorf("can't open log file: %w", err) - } - - info, err := file.Stat() - if err != nil { - return nil, fmt.Errorf("can't get info about file: %w", err) - } - - rfw := &RotateFileWriter{ - filename: filename, - basename: basename, - dir: dir, - maxSize: maxSize, - maxBackups: maxBackups, - nameMatcher: r, - file: file, - size: int(info.Size()), - } - - return rfw, nil -} - -// Write implements the io.Writer interface. It writes the given bytes to the -// rolling log file, handling log rotation when the size exceeds the max. -// It locks access to the file for the duration of the write. -func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { - rfw.mu.Lock() - defer rfw.mu.Unlock() - - if rfw.size+len(p) > rfw.maxSize { - if err := rfw.rotate(); err != nil { - return 0, fmt.Errorf("can't rotate log file: %w", err) - } - } - - n, err = rfw.file.Write(p) - rfw.size += n - - return -} + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) -// rotate sequentially increments old log suffixes, -// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc -// and then closes current file and renames it to the first backup. -func (rfw *RotateFileWriter) rotate() error { - if rfw.maxBackups == 0 { - return nil - } +// Logging configuration +const ( + logMaxNum = 3 + logMaxSize = 10 * 1024 * 1024 // 10MB +) - cnt := 0 - err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { +func Init(file, level string) { + var out io.Writer = os.Stdout + if file != "" { + rl, err := NewRotateFileWriter(file, logMaxNum, logMaxSize) if err != nil { - return fmt.Errorf("can't walk directory: %w", err) - } - - if d.IsDir() && path != rfw.dir { - return filepath.SkipDir + log.Fatal().Msgf("### Failed to create log file: %v", err) } - - if rfw.nameMatcher.MatchString(d.Name()) { - cnt++ - } - - return nil - }) - - if err != nil { - return fmt.Errorf("can't walk directory: %w", err) + out = io.MultiWriter(os.Stdout, rl) } - for i := cnt; i > 0; i-- { - oldName := rfw.filename + "." + strconv.Itoa(i) - newName := rfw.filename + "." + strconv.Itoa(i+1) - // just remove everything over `rfw.maxBackups` - if i >= rfw.maxBackups { - if err := os.Remove(oldName); err != nil { - return fmt.Errorf("can't remove old log file: %w", err) + l := zerolog.New(out). + With(). + Timestamp(). + Caller(). + Logger() + + zerolog.TimeFieldFormat = time.RFC3339Nano + zerolog.ErrorFieldName = "err" + zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { + short := file + for i := len(file) - 1; i > 0; i-- { + if file[i] == '/' { + short = file[i+1:] + break } - continue - } - - if err := os.Rename(oldName, newName); err != nil { - return fmt.Errorf("can't rename old log file: %w", err) } + file = short + return file + ":" + strconv.Itoa(line) } - // Try to close current file and rename it - if err := rfw.file.Close(); err != nil { - return fmt.Errorf("can't close current log file: %w", err) + log.Logger = l + + switch level { + case "DEBUG": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "INFO": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "WARN": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "ERROR": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + case "FATAL": + zerolog.SetGlobalLevel(zerolog.FatalLevel) + default: + zerolog.SetGlobalLevel(zerolog.TraceLevel) } - - if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { - return fmt.Errorf("can't rename current log file: %w", err) - } - - file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - return fmt.Errorf("can't open log file: %w", err) - } - rfw.file = file - rfw.size = 0 - - return nil } diff --git a/event-processor/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go b/event-processor/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go new file mode 100644 index 00000000..50373280 --- /dev/null +++ b/event-processor/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go @@ -0,0 +1,165 @@ +package logger + +import ( + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strconv" + "sync" +) + +var _ io.Writer = (*RotateFileWriter)(nil) + +// RotateFileWriter is a writer that rotates log files based on size and number of backups. +type RotateFileWriter struct { + mu sync.Mutex + + filename string + basename string + dir string + maxSize int + maxBackups int + + file *os.File + size int + nameMatcher *regexp.Regexp +} + +// NewRotateFileWriter creates a new RotateFileWriter for the given filename and +// configuration. It ensures the log directory exists, compiles a regexp to match +// rotated log files, opens the initial log file, and returns a RotateFileWriter. +func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { + if filename == "" { + return nil, fmt.Errorf("filename cannot be empty") + } + + if maxBackups <= 0 { + return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") + } + + if maxSize <= 0 { + return nil, fmt.Errorf("maxSize cannot be less or equal to zero") + } + + filename = filepath.Clean(filename) + basename := filepath.Base(filename) + dir := filepath.Dir(filename) + + err := os.MkdirAll(dir, 0755) + if err != nil { + return nil, fmt.Errorf("can't make directories for new logfile: %w", err) + } + + r, err := regexp.Compile(basename + `\.[0-9]+$`) + if err != nil { + return nil, fmt.Errorf("can't compile regexp: %w", err) + } + + file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, fmt.Errorf("can't open log file: %w", err) + } + + info, err := file.Stat() + if err != nil { + return nil, fmt.Errorf("can't get info about file: %w", err) + } + + rfw := &RotateFileWriter{ + filename: filename, + basename: basename, + dir: dir, + maxSize: maxSize, + maxBackups: maxBackups, + nameMatcher: r, + file: file, + size: int(info.Size()), + } + + return rfw, nil +} + +// Write implements the io.Writer interface. It writes the given bytes to the +// rolling log file, handling log rotation when the size exceeds the max. +// It locks access to the file for the duration of the write. +func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { + rfw.mu.Lock() + defer rfw.mu.Unlock() + + if rfw.size+len(p) > rfw.maxSize { + if err := rfw.rotate(); err != nil { + return 0, fmt.Errorf("can't rotate log file: %w", err) + } + } + + n, err = rfw.file.Write(p) + rfw.size += n + + return +} + +// rotate sequentially increments old log suffixes, +// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc +// and then closes current file and renames it to the first backup. +func (rfw *RotateFileWriter) rotate() error { + if rfw.maxBackups == 0 { + return nil + } + + cnt := 0 + err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + if d.IsDir() && path != rfw.dir { + return filepath.SkipDir + } + + if rfw.nameMatcher.MatchString(d.Name()) { + cnt++ + } + + return nil + }) + + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + for i := cnt; i > 0; i-- { + oldName := rfw.filename + "." + strconv.Itoa(i) + newName := rfw.filename + "." + strconv.Itoa(i+1) + // just remove everything over `rfw.maxBackups` + if i >= rfw.maxBackups { + if err := os.Remove(oldName); err != nil { + return fmt.Errorf("can't remove old log file: %w", err) + } + continue + } + + if err := os.Rename(oldName, newName); err != nil { + return fmt.Errorf("can't rename old log file: %w", err) + } + } + + // Try to close current file and rename it + if err := rfw.file.Close(); err != nil { + return fmt.Errorf("can't close current log file: %w", err) + } + + if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { + return fmt.Errorf("can't rename current log file: %w", err) + } + + file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("can't open log file: %w", err) + } + rfw.file = file + rfw.size = 0 + + return nil +} diff --git a/history-api/cmd/history-api/init.go b/history-api/cmd/history-api/init.go deleted file mode 100644 index 877207fe..00000000 --- a/history-api/cmd/history-api/init.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "io" - "os" - "strconv" - "time" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - lib_logger "github.com/runtime-radar/runtime-radar/lib/logger" -) - -const ( - // Logging configuration - logMaxNum = 3 - logMaxSize = 10 * 1024 * 1024 // 10MB -) - -func initLogger(file, level string) { - var out io.Writer = os.Stdout - if file != "" { - rl, err := lib_logger.NewRotateFileWriter(file, logMaxNum, logMaxSize) - if err != nil { - log.Fatal().Msgf("### Failed to create log file: %v", err) - } - out = io.MultiWriter(os.Stdout, rl) - } - - l := zerolog.New(out). - With(). - Timestamp(). - Caller(). - Logger() - - zerolog.TimeFieldFormat = time.RFC3339Nano - zerolog.ErrorFieldName = "err" - zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { - short := file - for i := len(file) - 1; i > 0; i-- { - if file[i] == '/' { - short = file[i+1:] - break - } - } - file = short - return file + ":" + strconv.Itoa(line) - } - - log.Logger = l - - switch level { - case "DEBUG": - zerolog.SetGlobalLevel(zerolog.DebugLevel) - case "INFO": - zerolog.SetGlobalLevel(zerolog.InfoLevel) - case "WARN": - zerolog.SetGlobalLevel(zerolog.WarnLevel) - case "ERROR": - zerolog.SetGlobalLevel(zerolog.ErrorLevel) - case "FATAL": - zerolog.SetGlobalLevel(zerolog.FatalLevel) - default: - zerolog.SetGlobalLevel(zerolog.TraceLevel) - } -} diff --git a/history-api/cmd/history-api/main.go b/history-api/cmd/history-api/main.go index bdf03f1f..0d8df5fe 100644 --- a/history-api/cmd/history-api/main.go +++ b/history-api/cmd/history-api/main.go @@ -20,6 +20,7 @@ import ( "github.com/runtime-radar/runtime-radar/history-api/pkg/database/clickhouse" "github.com/runtime-radar/runtime-radar/history-api/pkg/server" "github.com/runtime-radar/runtime-radar/history-api/pkg/service" + "github.com/runtime-radar/runtime-radar/lib/logger" "github.com/runtime-radar/runtime-radar/lib/rabbit" "github.com/runtime-radar/runtime-radar/lib/security" "github.com/runtime-radar/runtime-radar/lib/security/jwt" @@ -51,7 +52,7 @@ var ( func main() { cfg := config.New() - initLogger(cfg.LogFile, cfg.LogLevel) + logger.Init(cfg.LogFile, cfg.LogLevel) log.Info().Str("build_release", build.Release).Str("build_branch", build.Branch).Str("build_commit", build.Commit).Str("build_date", build.Date).Msgf("-> %s started", build.AppName) defer log.Info().Msgf("<- %s exited", build.AppName) diff --git a/history-api/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go b/history-api/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go index 50373280..f1e606a1 100644 --- a/history-api/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go +++ b/history-api/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go @@ -1,165 +1,65 @@ package logger import ( - "fmt" "io" "os" - "path/filepath" - "regexp" "strconv" - "sync" -) - -var _ io.Writer = (*RotateFileWriter)(nil) - -// RotateFileWriter is a writer that rotates log files based on size and number of backups. -type RotateFileWriter struct { - mu sync.Mutex - - filename string - basename string - dir string - maxSize int - maxBackups int - - file *os.File - size int - nameMatcher *regexp.Regexp -} - -// NewRotateFileWriter creates a new RotateFileWriter for the given filename and -// configuration. It ensures the log directory exists, compiles a regexp to match -// rotated log files, opens the initial log file, and returns a RotateFileWriter. -func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { - if filename == "" { - return nil, fmt.Errorf("filename cannot be empty") - } - - if maxBackups <= 0 { - return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") - } - - if maxSize <= 0 { - return nil, fmt.Errorf("maxSize cannot be less or equal to zero") - } + "time" - filename = filepath.Clean(filename) - basename := filepath.Base(filename) - dir := filepath.Dir(filename) - - err := os.MkdirAll(dir, 0755) - if err != nil { - return nil, fmt.Errorf("can't make directories for new logfile: %w", err) - } - - r, err := regexp.Compile(basename + `\.[0-9]+$`) - if err != nil { - return nil, fmt.Errorf("can't compile regexp: %w", err) - } - - file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, fmt.Errorf("can't open log file: %w", err) - } - - info, err := file.Stat() - if err != nil { - return nil, fmt.Errorf("can't get info about file: %w", err) - } - - rfw := &RotateFileWriter{ - filename: filename, - basename: basename, - dir: dir, - maxSize: maxSize, - maxBackups: maxBackups, - nameMatcher: r, - file: file, - size: int(info.Size()), - } - - return rfw, nil -} - -// Write implements the io.Writer interface. It writes the given bytes to the -// rolling log file, handling log rotation when the size exceeds the max. -// It locks access to the file for the duration of the write. -func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { - rfw.mu.Lock() - defer rfw.mu.Unlock() - - if rfw.size+len(p) > rfw.maxSize { - if err := rfw.rotate(); err != nil { - return 0, fmt.Errorf("can't rotate log file: %w", err) - } - } - - n, err = rfw.file.Write(p) - rfw.size += n - - return -} + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) -// rotate sequentially increments old log suffixes, -// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc -// and then closes current file and renames it to the first backup. -func (rfw *RotateFileWriter) rotate() error { - if rfw.maxBackups == 0 { - return nil - } +// Logging configuration +const ( + logMaxNum = 3 + logMaxSize = 10 * 1024 * 1024 // 10MB +) - cnt := 0 - err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { +func Init(file, level string) { + var out io.Writer = os.Stdout + if file != "" { + rl, err := NewRotateFileWriter(file, logMaxNum, logMaxSize) if err != nil { - return fmt.Errorf("can't walk directory: %w", err) - } - - if d.IsDir() && path != rfw.dir { - return filepath.SkipDir + log.Fatal().Msgf("### Failed to create log file: %v", err) } - - if rfw.nameMatcher.MatchString(d.Name()) { - cnt++ - } - - return nil - }) - - if err != nil { - return fmt.Errorf("can't walk directory: %w", err) + out = io.MultiWriter(os.Stdout, rl) } - for i := cnt; i > 0; i-- { - oldName := rfw.filename + "." + strconv.Itoa(i) - newName := rfw.filename + "." + strconv.Itoa(i+1) - // just remove everything over `rfw.maxBackups` - if i >= rfw.maxBackups { - if err := os.Remove(oldName); err != nil { - return fmt.Errorf("can't remove old log file: %w", err) + l := zerolog.New(out). + With(). + Timestamp(). + Caller(). + Logger() + + zerolog.TimeFieldFormat = time.RFC3339Nano + zerolog.ErrorFieldName = "err" + zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { + short := file + for i := len(file) - 1; i > 0; i-- { + if file[i] == '/' { + short = file[i+1:] + break } - continue - } - - if err := os.Rename(oldName, newName); err != nil { - return fmt.Errorf("can't rename old log file: %w", err) } + file = short + return file + ":" + strconv.Itoa(line) } - // Try to close current file and rename it - if err := rfw.file.Close(); err != nil { - return fmt.Errorf("can't close current log file: %w", err) + log.Logger = l + + switch level { + case "DEBUG": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "INFO": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "WARN": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "ERROR": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + case "FATAL": + zerolog.SetGlobalLevel(zerolog.FatalLevel) + default: + zerolog.SetGlobalLevel(zerolog.TraceLevel) } - - if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { - return fmt.Errorf("can't rename current log file: %w", err) - } - - file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - return fmt.Errorf("can't open log file: %w", err) - } - rfw.file = file - rfw.size = 0 - - return nil } diff --git a/history-api/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go b/history-api/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go new file mode 100644 index 00000000..50373280 --- /dev/null +++ b/history-api/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go @@ -0,0 +1,165 @@ +package logger + +import ( + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strconv" + "sync" +) + +var _ io.Writer = (*RotateFileWriter)(nil) + +// RotateFileWriter is a writer that rotates log files based on size and number of backups. +type RotateFileWriter struct { + mu sync.Mutex + + filename string + basename string + dir string + maxSize int + maxBackups int + + file *os.File + size int + nameMatcher *regexp.Regexp +} + +// NewRotateFileWriter creates a new RotateFileWriter for the given filename and +// configuration. It ensures the log directory exists, compiles a regexp to match +// rotated log files, opens the initial log file, and returns a RotateFileWriter. +func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { + if filename == "" { + return nil, fmt.Errorf("filename cannot be empty") + } + + if maxBackups <= 0 { + return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") + } + + if maxSize <= 0 { + return nil, fmt.Errorf("maxSize cannot be less or equal to zero") + } + + filename = filepath.Clean(filename) + basename := filepath.Base(filename) + dir := filepath.Dir(filename) + + err := os.MkdirAll(dir, 0755) + if err != nil { + return nil, fmt.Errorf("can't make directories for new logfile: %w", err) + } + + r, err := regexp.Compile(basename + `\.[0-9]+$`) + if err != nil { + return nil, fmt.Errorf("can't compile regexp: %w", err) + } + + file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, fmt.Errorf("can't open log file: %w", err) + } + + info, err := file.Stat() + if err != nil { + return nil, fmt.Errorf("can't get info about file: %w", err) + } + + rfw := &RotateFileWriter{ + filename: filename, + basename: basename, + dir: dir, + maxSize: maxSize, + maxBackups: maxBackups, + nameMatcher: r, + file: file, + size: int(info.Size()), + } + + return rfw, nil +} + +// Write implements the io.Writer interface. It writes the given bytes to the +// rolling log file, handling log rotation when the size exceeds the max. +// It locks access to the file for the duration of the write. +func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { + rfw.mu.Lock() + defer rfw.mu.Unlock() + + if rfw.size+len(p) > rfw.maxSize { + if err := rfw.rotate(); err != nil { + return 0, fmt.Errorf("can't rotate log file: %w", err) + } + } + + n, err = rfw.file.Write(p) + rfw.size += n + + return +} + +// rotate sequentially increments old log suffixes, +// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc +// and then closes current file and renames it to the first backup. +func (rfw *RotateFileWriter) rotate() error { + if rfw.maxBackups == 0 { + return nil + } + + cnt := 0 + err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + if d.IsDir() && path != rfw.dir { + return filepath.SkipDir + } + + if rfw.nameMatcher.MatchString(d.Name()) { + cnt++ + } + + return nil + }) + + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + for i := cnt; i > 0; i-- { + oldName := rfw.filename + "." + strconv.Itoa(i) + newName := rfw.filename + "." + strconv.Itoa(i+1) + // just remove everything over `rfw.maxBackups` + if i >= rfw.maxBackups { + if err := os.Remove(oldName); err != nil { + return fmt.Errorf("can't remove old log file: %w", err) + } + continue + } + + if err := os.Rename(oldName, newName); err != nil { + return fmt.Errorf("can't rename old log file: %w", err) + } + } + + // Try to close current file and rename it + if err := rfw.file.Close(); err != nil { + return fmt.Errorf("can't close current log file: %w", err) + } + + if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { + return fmt.Errorf("can't rename current log file: %w", err) + } + + file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("can't open log file: %w", err) + } + rfw.file = file + rfw.size = 0 + + return nil +} diff --git a/lib/logger/logger.go b/lib/logger/logger.go index 50373280..f1e606a1 100644 --- a/lib/logger/logger.go +++ b/lib/logger/logger.go @@ -1,165 +1,65 @@ package logger import ( - "fmt" "io" "os" - "path/filepath" - "regexp" "strconv" - "sync" -) - -var _ io.Writer = (*RotateFileWriter)(nil) - -// RotateFileWriter is a writer that rotates log files based on size and number of backups. -type RotateFileWriter struct { - mu sync.Mutex - - filename string - basename string - dir string - maxSize int - maxBackups int - - file *os.File - size int - nameMatcher *regexp.Regexp -} - -// NewRotateFileWriter creates a new RotateFileWriter for the given filename and -// configuration. It ensures the log directory exists, compiles a regexp to match -// rotated log files, opens the initial log file, and returns a RotateFileWriter. -func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { - if filename == "" { - return nil, fmt.Errorf("filename cannot be empty") - } - - if maxBackups <= 0 { - return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") - } - - if maxSize <= 0 { - return nil, fmt.Errorf("maxSize cannot be less or equal to zero") - } + "time" - filename = filepath.Clean(filename) - basename := filepath.Base(filename) - dir := filepath.Dir(filename) - - err := os.MkdirAll(dir, 0755) - if err != nil { - return nil, fmt.Errorf("can't make directories for new logfile: %w", err) - } - - r, err := regexp.Compile(basename + `\.[0-9]+$`) - if err != nil { - return nil, fmt.Errorf("can't compile regexp: %w", err) - } - - file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, fmt.Errorf("can't open log file: %w", err) - } - - info, err := file.Stat() - if err != nil { - return nil, fmt.Errorf("can't get info about file: %w", err) - } - - rfw := &RotateFileWriter{ - filename: filename, - basename: basename, - dir: dir, - maxSize: maxSize, - maxBackups: maxBackups, - nameMatcher: r, - file: file, - size: int(info.Size()), - } - - return rfw, nil -} - -// Write implements the io.Writer interface. It writes the given bytes to the -// rolling log file, handling log rotation when the size exceeds the max. -// It locks access to the file for the duration of the write. -func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { - rfw.mu.Lock() - defer rfw.mu.Unlock() - - if rfw.size+len(p) > rfw.maxSize { - if err := rfw.rotate(); err != nil { - return 0, fmt.Errorf("can't rotate log file: %w", err) - } - } - - n, err = rfw.file.Write(p) - rfw.size += n - - return -} + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) -// rotate sequentially increments old log suffixes, -// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc -// and then closes current file and renames it to the first backup. -func (rfw *RotateFileWriter) rotate() error { - if rfw.maxBackups == 0 { - return nil - } +// Logging configuration +const ( + logMaxNum = 3 + logMaxSize = 10 * 1024 * 1024 // 10MB +) - cnt := 0 - err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { +func Init(file, level string) { + var out io.Writer = os.Stdout + if file != "" { + rl, err := NewRotateFileWriter(file, logMaxNum, logMaxSize) if err != nil { - return fmt.Errorf("can't walk directory: %w", err) - } - - if d.IsDir() && path != rfw.dir { - return filepath.SkipDir + log.Fatal().Msgf("### Failed to create log file: %v", err) } - - if rfw.nameMatcher.MatchString(d.Name()) { - cnt++ - } - - return nil - }) - - if err != nil { - return fmt.Errorf("can't walk directory: %w", err) + out = io.MultiWriter(os.Stdout, rl) } - for i := cnt; i > 0; i-- { - oldName := rfw.filename + "." + strconv.Itoa(i) - newName := rfw.filename + "." + strconv.Itoa(i+1) - // just remove everything over `rfw.maxBackups` - if i >= rfw.maxBackups { - if err := os.Remove(oldName); err != nil { - return fmt.Errorf("can't remove old log file: %w", err) + l := zerolog.New(out). + With(). + Timestamp(). + Caller(). + Logger() + + zerolog.TimeFieldFormat = time.RFC3339Nano + zerolog.ErrorFieldName = "err" + zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { + short := file + for i := len(file) - 1; i > 0; i-- { + if file[i] == '/' { + short = file[i+1:] + break } - continue - } - - if err := os.Rename(oldName, newName); err != nil { - return fmt.Errorf("can't rename old log file: %w", err) } + file = short + return file + ":" + strconv.Itoa(line) } - // Try to close current file and rename it - if err := rfw.file.Close(); err != nil { - return fmt.Errorf("can't close current log file: %w", err) + log.Logger = l + + switch level { + case "DEBUG": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "INFO": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "WARN": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "ERROR": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + case "FATAL": + zerolog.SetGlobalLevel(zerolog.FatalLevel) + default: + zerolog.SetGlobalLevel(zerolog.TraceLevel) } - - if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { - return fmt.Errorf("can't rename current log file: %w", err) - } - - file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - return fmt.Errorf("can't open log file: %w", err) - } - rfw.file = file - rfw.size = 0 - - return nil } diff --git a/lib/logger/rotate.go b/lib/logger/rotate.go new file mode 100644 index 00000000..50373280 --- /dev/null +++ b/lib/logger/rotate.go @@ -0,0 +1,165 @@ +package logger + +import ( + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strconv" + "sync" +) + +var _ io.Writer = (*RotateFileWriter)(nil) + +// RotateFileWriter is a writer that rotates log files based on size and number of backups. +type RotateFileWriter struct { + mu sync.Mutex + + filename string + basename string + dir string + maxSize int + maxBackups int + + file *os.File + size int + nameMatcher *regexp.Regexp +} + +// NewRotateFileWriter creates a new RotateFileWriter for the given filename and +// configuration. It ensures the log directory exists, compiles a regexp to match +// rotated log files, opens the initial log file, and returns a RotateFileWriter. +func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { + if filename == "" { + return nil, fmt.Errorf("filename cannot be empty") + } + + if maxBackups <= 0 { + return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") + } + + if maxSize <= 0 { + return nil, fmt.Errorf("maxSize cannot be less or equal to zero") + } + + filename = filepath.Clean(filename) + basename := filepath.Base(filename) + dir := filepath.Dir(filename) + + err := os.MkdirAll(dir, 0755) + if err != nil { + return nil, fmt.Errorf("can't make directories for new logfile: %w", err) + } + + r, err := regexp.Compile(basename + `\.[0-9]+$`) + if err != nil { + return nil, fmt.Errorf("can't compile regexp: %w", err) + } + + file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, fmt.Errorf("can't open log file: %w", err) + } + + info, err := file.Stat() + if err != nil { + return nil, fmt.Errorf("can't get info about file: %w", err) + } + + rfw := &RotateFileWriter{ + filename: filename, + basename: basename, + dir: dir, + maxSize: maxSize, + maxBackups: maxBackups, + nameMatcher: r, + file: file, + size: int(info.Size()), + } + + return rfw, nil +} + +// Write implements the io.Writer interface. It writes the given bytes to the +// rolling log file, handling log rotation when the size exceeds the max. +// It locks access to the file for the duration of the write. +func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { + rfw.mu.Lock() + defer rfw.mu.Unlock() + + if rfw.size+len(p) > rfw.maxSize { + if err := rfw.rotate(); err != nil { + return 0, fmt.Errorf("can't rotate log file: %w", err) + } + } + + n, err = rfw.file.Write(p) + rfw.size += n + + return +} + +// rotate sequentially increments old log suffixes, +// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc +// and then closes current file and renames it to the first backup. +func (rfw *RotateFileWriter) rotate() error { + if rfw.maxBackups == 0 { + return nil + } + + cnt := 0 + err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + if d.IsDir() && path != rfw.dir { + return filepath.SkipDir + } + + if rfw.nameMatcher.MatchString(d.Name()) { + cnt++ + } + + return nil + }) + + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + for i := cnt; i > 0; i-- { + oldName := rfw.filename + "." + strconv.Itoa(i) + newName := rfw.filename + "." + strconv.Itoa(i+1) + // just remove everything over `rfw.maxBackups` + if i >= rfw.maxBackups { + if err := os.Remove(oldName); err != nil { + return fmt.Errorf("can't remove old log file: %w", err) + } + continue + } + + if err := os.Rename(oldName, newName); err != nil { + return fmt.Errorf("can't rename old log file: %w", err) + } + } + + // Try to close current file and rename it + if err := rfw.file.Close(); err != nil { + return fmt.Errorf("can't close current log file: %w", err) + } + + if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { + return fmt.Errorf("can't rename current log file: %w", err) + } + + file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("can't open log file: %w", err) + } + rfw.file = file + rfw.size = 0 + + return nil +} diff --git a/notifier/cmd/notifier/init.go b/notifier/cmd/notifier/init.go deleted file mode 100644 index 877207fe..00000000 --- a/notifier/cmd/notifier/init.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "io" - "os" - "strconv" - "time" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - lib_logger "github.com/runtime-radar/runtime-radar/lib/logger" -) - -const ( - // Logging configuration - logMaxNum = 3 - logMaxSize = 10 * 1024 * 1024 // 10MB -) - -func initLogger(file, level string) { - var out io.Writer = os.Stdout - if file != "" { - rl, err := lib_logger.NewRotateFileWriter(file, logMaxNum, logMaxSize) - if err != nil { - log.Fatal().Msgf("### Failed to create log file: %v", err) - } - out = io.MultiWriter(os.Stdout, rl) - } - - l := zerolog.New(out). - With(). - Timestamp(). - Caller(). - Logger() - - zerolog.TimeFieldFormat = time.RFC3339Nano - zerolog.ErrorFieldName = "err" - zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { - short := file - for i := len(file) - 1; i > 0; i-- { - if file[i] == '/' { - short = file[i+1:] - break - } - } - file = short - return file + ":" + strconv.Itoa(line) - } - - log.Logger = l - - switch level { - case "DEBUG": - zerolog.SetGlobalLevel(zerolog.DebugLevel) - case "INFO": - zerolog.SetGlobalLevel(zerolog.InfoLevel) - case "WARN": - zerolog.SetGlobalLevel(zerolog.WarnLevel) - case "ERROR": - zerolog.SetGlobalLevel(zerolog.ErrorLevel) - case "FATAL": - zerolog.SetGlobalLevel(zerolog.FatalLevel) - default: - zerolog.SetGlobalLevel(zerolog.TraceLevel) - } -} diff --git a/notifier/cmd/notifier/main.go b/notifier/cmd/notifier/main.go index 019ebdfc..dbb90800 100644 --- a/notifier/cmd/notifier/main.go +++ b/notifier/cmd/notifier/main.go @@ -13,6 +13,7 @@ import ( "github.com/google/gops/agent" "github.com/rs/zerolog/log" + "github.com/runtime-radar/runtime-radar/lib/logger" "github.com/runtime-radar/runtime-radar/lib/security" "github.com/runtime-radar/runtime-radar/lib/security/cipher" "github.com/runtime-radar/runtime-radar/lib/security/jwt" @@ -53,7 +54,7 @@ var ( func main() { cfg := config.New() - initLogger(cfg.LogFile, cfg.LogLevel) + logger.Init(cfg.LogFile, cfg.LogLevel) log.Info().Str("build_release", build.Release).Str("build_branch", build.Branch).Str("build_commit", build.Commit).Str("build_date", build.Date).Msgf("-> %s started", build.AppName) defer log.Info().Msgf("<- %s exited", build.AppName) diff --git a/notifier/cmd/notifier/main_test.go b/notifier/cmd/notifier/main_test.go index 450130ab..17354ddd 100644 --- a/notifier/cmd/notifier/main_test.go +++ b/notifier/cmd/notifier/main_test.go @@ -16,6 +16,7 @@ import ( "github.com/rs/zerolog/log" history "github.com/runtime-radar/runtime-radar/history-api/pkg/model" "github.com/runtime-radar/runtime-radar/lib/errcommon" + "github.com/runtime-radar/runtime-radar/lib/logger" "github.com/runtime-radar/runtime-radar/lib/security" "github.com/runtime-radar/runtime-radar/lib/security/cipher" "github.com/runtime-radar/runtime-radar/lib/security/jwt" @@ -71,9 +72,9 @@ func TestMain(m *testing.M) { template.Init(cfg.TemplatesHTMLFolder, cfg.TemplatesTextFolder) if testing.Verbose() { - initLogger("", "DEBUG") + logger.Init("", "DEBUG") } else { - initLogger("", "INFO") + logger.Init("", "INFO") } mailpitClient = mailpit.NewClient(cfg.TestMailpitHTTPAddr) diff --git a/notifier/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go b/notifier/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go index 50373280..f1e606a1 100644 --- a/notifier/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go +++ b/notifier/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go @@ -1,165 +1,65 @@ package logger import ( - "fmt" "io" "os" - "path/filepath" - "regexp" "strconv" - "sync" -) - -var _ io.Writer = (*RotateFileWriter)(nil) - -// RotateFileWriter is a writer that rotates log files based on size and number of backups. -type RotateFileWriter struct { - mu sync.Mutex - - filename string - basename string - dir string - maxSize int - maxBackups int - - file *os.File - size int - nameMatcher *regexp.Regexp -} - -// NewRotateFileWriter creates a new RotateFileWriter for the given filename and -// configuration. It ensures the log directory exists, compiles a regexp to match -// rotated log files, opens the initial log file, and returns a RotateFileWriter. -func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { - if filename == "" { - return nil, fmt.Errorf("filename cannot be empty") - } - - if maxBackups <= 0 { - return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") - } - - if maxSize <= 0 { - return nil, fmt.Errorf("maxSize cannot be less or equal to zero") - } + "time" - filename = filepath.Clean(filename) - basename := filepath.Base(filename) - dir := filepath.Dir(filename) - - err := os.MkdirAll(dir, 0755) - if err != nil { - return nil, fmt.Errorf("can't make directories for new logfile: %w", err) - } - - r, err := regexp.Compile(basename + `\.[0-9]+$`) - if err != nil { - return nil, fmt.Errorf("can't compile regexp: %w", err) - } - - file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, fmt.Errorf("can't open log file: %w", err) - } - - info, err := file.Stat() - if err != nil { - return nil, fmt.Errorf("can't get info about file: %w", err) - } - - rfw := &RotateFileWriter{ - filename: filename, - basename: basename, - dir: dir, - maxSize: maxSize, - maxBackups: maxBackups, - nameMatcher: r, - file: file, - size: int(info.Size()), - } - - return rfw, nil -} - -// Write implements the io.Writer interface. It writes the given bytes to the -// rolling log file, handling log rotation when the size exceeds the max. -// It locks access to the file for the duration of the write. -func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { - rfw.mu.Lock() - defer rfw.mu.Unlock() - - if rfw.size+len(p) > rfw.maxSize { - if err := rfw.rotate(); err != nil { - return 0, fmt.Errorf("can't rotate log file: %w", err) - } - } - - n, err = rfw.file.Write(p) - rfw.size += n - - return -} + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) -// rotate sequentially increments old log suffixes, -// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc -// and then closes current file and renames it to the first backup. -func (rfw *RotateFileWriter) rotate() error { - if rfw.maxBackups == 0 { - return nil - } +// Logging configuration +const ( + logMaxNum = 3 + logMaxSize = 10 * 1024 * 1024 // 10MB +) - cnt := 0 - err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { +func Init(file, level string) { + var out io.Writer = os.Stdout + if file != "" { + rl, err := NewRotateFileWriter(file, logMaxNum, logMaxSize) if err != nil { - return fmt.Errorf("can't walk directory: %w", err) - } - - if d.IsDir() && path != rfw.dir { - return filepath.SkipDir + log.Fatal().Msgf("### Failed to create log file: %v", err) } - - if rfw.nameMatcher.MatchString(d.Name()) { - cnt++ - } - - return nil - }) - - if err != nil { - return fmt.Errorf("can't walk directory: %w", err) + out = io.MultiWriter(os.Stdout, rl) } - for i := cnt; i > 0; i-- { - oldName := rfw.filename + "." + strconv.Itoa(i) - newName := rfw.filename + "." + strconv.Itoa(i+1) - // just remove everything over `rfw.maxBackups` - if i >= rfw.maxBackups { - if err := os.Remove(oldName); err != nil { - return fmt.Errorf("can't remove old log file: %w", err) + l := zerolog.New(out). + With(). + Timestamp(). + Caller(). + Logger() + + zerolog.TimeFieldFormat = time.RFC3339Nano + zerolog.ErrorFieldName = "err" + zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { + short := file + for i := len(file) - 1; i > 0; i-- { + if file[i] == '/' { + short = file[i+1:] + break } - continue - } - - if err := os.Rename(oldName, newName); err != nil { - return fmt.Errorf("can't rename old log file: %w", err) } + file = short + return file + ":" + strconv.Itoa(line) } - // Try to close current file and rename it - if err := rfw.file.Close(); err != nil { - return fmt.Errorf("can't close current log file: %w", err) + log.Logger = l + + switch level { + case "DEBUG": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "INFO": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "WARN": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "ERROR": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + case "FATAL": + zerolog.SetGlobalLevel(zerolog.FatalLevel) + default: + zerolog.SetGlobalLevel(zerolog.TraceLevel) } - - if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { - return fmt.Errorf("can't rename current log file: %w", err) - } - - file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - return fmt.Errorf("can't open log file: %w", err) - } - rfw.file = file - rfw.size = 0 - - return nil } diff --git a/notifier/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go b/notifier/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go new file mode 100644 index 00000000..50373280 --- /dev/null +++ b/notifier/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go @@ -0,0 +1,165 @@ +package logger + +import ( + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strconv" + "sync" +) + +var _ io.Writer = (*RotateFileWriter)(nil) + +// RotateFileWriter is a writer that rotates log files based on size and number of backups. +type RotateFileWriter struct { + mu sync.Mutex + + filename string + basename string + dir string + maxSize int + maxBackups int + + file *os.File + size int + nameMatcher *regexp.Regexp +} + +// NewRotateFileWriter creates a new RotateFileWriter for the given filename and +// configuration. It ensures the log directory exists, compiles a regexp to match +// rotated log files, opens the initial log file, and returns a RotateFileWriter. +func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { + if filename == "" { + return nil, fmt.Errorf("filename cannot be empty") + } + + if maxBackups <= 0 { + return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") + } + + if maxSize <= 0 { + return nil, fmt.Errorf("maxSize cannot be less or equal to zero") + } + + filename = filepath.Clean(filename) + basename := filepath.Base(filename) + dir := filepath.Dir(filename) + + err := os.MkdirAll(dir, 0755) + if err != nil { + return nil, fmt.Errorf("can't make directories for new logfile: %w", err) + } + + r, err := regexp.Compile(basename + `\.[0-9]+$`) + if err != nil { + return nil, fmt.Errorf("can't compile regexp: %w", err) + } + + file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, fmt.Errorf("can't open log file: %w", err) + } + + info, err := file.Stat() + if err != nil { + return nil, fmt.Errorf("can't get info about file: %w", err) + } + + rfw := &RotateFileWriter{ + filename: filename, + basename: basename, + dir: dir, + maxSize: maxSize, + maxBackups: maxBackups, + nameMatcher: r, + file: file, + size: int(info.Size()), + } + + return rfw, nil +} + +// Write implements the io.Writer interface. It writes the given bytes to the +// rolling log file, handling log rotation when the size exceeds the max. +// It locks access to the file for the duration of the write. +func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { + rfw.mu.Lock() + defer rfw.mu.Unlock() + + if rfw.size+len(p) > rfw.maxSize { + if err := rfw.rotate(); err != nil { + return 0, fmt.Errorf("can't rotate log file: %w", err) + } + } + + n, err = rfw.file.Write(p) + rfw.size += n + + return +} + +// rotate sequentially increments old log suffixes, +// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc +// and then closes current file and renames it to the first backup. +func (rfw *RotateFileWriter) rotate() error { + if rfw.maxBackups == 0 { + return nil + } + + cnt := 0 + err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + if d.IsDir() && path != rfw.dir { + return filepath.SkipDir + } + + if rfw.nameMatcher.MatchString(d.Name()) { + cnt++ + } + + return nil + }) + + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + for i := cnt; i > 0; i-- { + oldName := rfw.filename + "." + strconv.Itoa(i) + newName := rfw.filename + "." + strconv.Itoa(i+1) + // just remove everything over `rfw.maxBackups` + if i >= rfw.maxBackups { + if err := os.Remove(oldName); err != nil { + return fmt.Errorf("can't remove old log file: %w", err) + } + continue + } + + if err := os.Rename(oldName, newName); err != nil { + return fmt.Errorf("can't rename old log file: %w", err) + } + } + + // Try to close current file and rename it + if err := rfw.file.Close(); err != nil { + return fmt.Errorf("can't close current log file: %w", err) + } + + if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { + return fmt.Errorf("can't rename current log file: %w", err) + } + + file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("can't open log file: %w", err) + } + rfw.file = file + rfw.size = 0 + + return nil +} diff --git a/policy-enforcer/cmd/policy-enforcer/main.go b/policy-enforcer/cmd/policy-enforcer/main.go index 72e81633..b043bba3 100644 --- a/policy-enforcer/cmd/policy-enforcer/main.go +++ b/policy-enforcer/cmd/policy-enforcer/main.go @@ -13,6 +13,7 @@ import ( "github.com/google/gops/agent" "github.com/rs/zerolog/log" + "github.com/runtime-radar/runtime-radar/lib/logger" "github.com/runtime-radar/runtime-radar/lib/security" "github.com/runtime-radar/runtime-radar/lib/security/jwt" "github.com/runtime-radar/runtime-radar/lib/server/healthcheck" @@ -50,7 +51,7 @@ var ( func main() { cfg := config.New() - initLogger(cfg.LogFile, cfg.LogLevel) + logger.Init(cfg.LogFile, cfg.LogLevel) log.Info().Str("build_release", build.Release).Str("build_branch", build.Branch).Str("build_commit", build.Commit).Str("build_date", build.Date).Msgf("-> %s started", build.AppName) defer log.Info().Msgf("<- %s exited", build.AppName) diff --git a/policy-enforcer/cmd/policy-enforcer/main_test.go b/policy-enforcer/cmd/policy-enforcer/main_test.go index 88000a44..3b291d9b 100644 --- a/policy-enforcer/cmd/policy-enforcer/main_test.go +++ b/policy-enforcer/cmd/policy-enforcer/main_test.go @@ -16,6 +16,7 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" "github.com/rs/zerolog/log" + "github.com/runtime-radar/runtime-radar/lib/logger" "github.com/runtime-radar/runtime-radar/lib/security" "github.com/runtime-radar/runtime-radar/lib/security/jwt" "github.com/runtime-radar/runtime-radar/lib/server/interceptor" @@ -58,9 +59,9 @@ func TestMain(m *testing.M) { cfg = config.New() if testing.Verbose() { - initLogger("", "DEBUG") + logger.Init("", "DEBUG") } else { - initLogger("", "INFO") + logger.Init("", "INFO") } lis, err := net.Listen("tcp", listenGRPCAddr) diff --git a/policy-enforcer/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go b/policy-enforcer/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go index 50373280..f1e606a1 100644 --- a/policy-enforcer/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go +++ b/policy-enforcer/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go @@ -1,165 +1,65 @@ package logger import ( - "fmt" "io" "os" - "path/filepath" - "regexp" "strconv" - "sync" -) - -var _ io.Writer = (*RotateFileWriter)(nil) - -// RotateFileWriter is a writer that rotates log files based on size and number of backups. -type RotateFileWriter struct { - mu sync.Mutex - - filename string - basename string - dir string - maxSize int - maxBackups int - - file *os.File - size int - nameMatcher *regexp.Regexp -} - -// NewRotateFileWriter creates a new RotateFileWriter for the given filename and -// configuration. It ensures the log directory exists, compiles a regexp to match -// rotated log files, opens the initial log file, and returns a RotateFileWriter. -func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { - if filename == "" { - return nil, fmt.Errorf("filename cannot be empty") - } - - if maxBackups <= 0 { - return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") - } - - if maxSize <= 0 { - return nil, fmt.Errorf("maxSize cannot be less or equal to zero") - } + "time" - filename = filepath.Clean(filename) - basename := filepath.Base(filename) - dir := filepath.Dir(filename) - - err := os.MkdirAll(dir, 0755) - if err != nil { - return nil, fmt.Errorf("can't make directories for new logfile: %w", err) - } - - r, err := regexp.Compile(basename + `\.[0-9]+$`) - if err != nil { - return nil, fmt.Errorf("can't compile regexp: %w", err) - } - - file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, fmt.Errorf("can't open log file: %w", err) - } - - info, err := file.Stat() - if err != nil { - return nil, fmt.Errorf("can't get info about file: %w", err) - } - - rfw := &RotateFileWriter{ - filename: filename, - basename: basename, - dir: dir, - maxSize: maxSize, - maxBackups: maxBackups, - nameMatcher: r, - file: file, - size: int(info.Size()), - } - - return rfw, nil -} - -// Write implements the io.Writer interface. It writes the given bytes to the -// rolling log file, handling log rotation when the size exceeds the max. -// It locks access to the file for the duration of the write. -func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { - rfw.mu.Lock() - defer rfw.mu.Unlock() - - if rfw.size+len(p) > rfw.maxSize { - if err := rfw.rotate(); err != nil { - return 0, fmt.Errorf("can't rotate log file: %w", err) - } - } - - n, err = rfw.file.Write(p) - rfw.size += n - - return -} + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) -// rotate sequentially increments old log suffixes, -// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc -// and then closes current file and renames it to the first backup. -func (rfw *RotateFileWriter) rotate() error { - if rfw.maxBackups == 0 { - return nil - } +// Logging configuration +const ( + logMaxNum = 3 + logMaxSize = 10 * 1024 * 1024 // 10MB +) - cnt := 0 - err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { +func Init(file, level string) { + var out io.Writer = os.Stdout + if file != "" { + rl, err := NewRotateFileWriter(file, logMaxNum, logMaxSize) if err != nil { - return fmt.Errorf("can't walk directory: %w", err) - } - - if d.IsDir() && path != rfw.dir { - return filepath.SkipDir + log.Fatal().Msgf("### Failed to create log file: %v", err) } - - if rfw.nameMatcher.MatchString(d.Name()) { - cnt++ - } - - return nil - }) - - if err != nil { - return fmt.Errorf("can't walk directory: %w", err) + out = io.MultiWriter(os.Stdout, rl) } - for i := cnt; i > 0; i-- { - oldName := rfw.filename + "." + strconv.Itoa(i) - newName := rfw.filename + "." + strconv.Itoa(i+1) - // just remove everything over `rfw.maxBackups` - if i >= rfw.maxBackups { - if err := os.Remove(oldName); err != nil { - return fmt.Errorf("can't remove old log file: %w", err) + l := zerolog.New(out). + With(). + Timestamp(). + Caller(). + Logger() + + zerolog.TimeFieldFormat = time.RFC3339Nano + zerolog.ErrorFieldName = "err" + zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { + short := file + for i := len(file) - 1; i > 0; i-- { + if file[i] == '/' { + short = file[i+1:] + break } - continue - } - - if err := os.Rename(oldName, newName); err != nil { - return fmt.Errorf("can't rename old log file: %w", err) } + file = short + return file + ":" + strconv.Itoa(line) } - // Try to close current file and rename it - if err := rfw.file.Close(); err != nil { - return fmt.Errorf("can't close current log file: %w", err) + log.Logger = l + + switch level { + case "DEBUG": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "INFO": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "WARN": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "ERROR": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + case "FATAL": + zerolog.SetGlobalLevel(zerolog.FatalLevel) + default: + zerolog.SetGlobalLevel(zerolog.TraceLevel) } - - if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { - return fmt.Errorf("can't rename current log file: %w", err) - } - - file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - return fmt.Errorf("can't open log file: %w", err) - } - rfw.file = file - rfw.size = 0 - - return nil } diff --git a/policy-enforcer/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go b/policy-enforcer/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go new file mode 100644 index 00000000..50373280 --- /dev/null +++ b/policy-enforcer/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go @@ -0,0 +1,165 @@ +package logger + +import ( + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strconv" + "sync" +) + +var _ io.Writer = (*RotateFileWriter)(nil) + +// RotateFileWriter is a writer that rotates log files based on size and number of backups. +type RotateFileWriter struct { + mu sync.Mutex + + filename string + basename string + dir string + maxSize int + maxBackups int + + file *os.File + size int + nameMatcher *regexp.Regexp +} + +// NewRotateFileWriter creates a new RotateFileWriter for the given filename and +// configuration. It ensures the log directory exists, compiles a regexp to match +// rotated log files, opens the initial log file, and returns a RotateFileWriter. +func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { + if filename == "" { + return nil, fmt.Errorf("filename cannot be empty") + } + + if maxBackups <= 0 { + return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") + } + + if maxSize <= 0 { + return nil, fmt.Errorf("maxSize cannot be less or equal to zero") + } + + filename = filepath.Clean(filename) + basename := filepath.Base(filename) + dir := filepath.Dir(filename) + + err := os.MkdirAll(dir, 0755) + if err != nil { + return nil, fmt.Errorf("can't make directories for new logfile: %w", err) + } + + r, err := regexp.Compile(basename + `\.[0-9]+$`) + if err != nil { + return nil, fmt.Errorf("can't compile regexp: %w", err) + } + + file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, fmt.Errorf("can't open log file: %w", err) + } + + info, err := file.Stat() + if err != nil { + return nil, fmt.Errorf("can't get info about file: %w", err) + } + + rfw := &RotateFileWriter{ + filename: filename, + basename: basename, + dir: dir, + maxSize: maxSize, + maxBackups: maxBackups, + nameMatcher: r, + file: file, + size: int(info.Size()), + } + + return rfw, nil +} + +// Write implements the io.Writer interface. It writes the given bytes to the +// rolling log file, handling log rotation when the size exceeds the max. +// It locks access to the file for the duration of the write. +func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { + rfw.mu.Lock() + defer rfw.mu.Unlock() + + if rfw.size+len(p) > rfw.maxSize { + if err := rfw.rotate(); err != nil { + return 0, fmt.Errorf("can't rotate log file: %w", err) + } + } + + n, err = rfw.file.Write(p) + rfw.size += n + + return +} + +// rotate sequentially increments old log suffixes, +// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc +// and then closes current file and renames it to the first backup. +func (rfw *RotateFileWriter) rotate() error { + if rfw.maxBackups == 0 { + return nil + } + + cnt := 0 + err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + if d.IsDir() && path != rfw.dir { + return filepath.SkipDir + } + + if rfw.nameMatcher.MatchString(d.Name()) { + cnt++ + } + + return nil + }) + + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + for i := cnt; i > 0; i-- { + oldName := rfw.filename + "." + strconv.Itoa(i) + newName := rfw.filename + "." + strconv.Itoa(i+1) + // just remove everything over `rfw.maxBackups` + if i >= rfw.maxBackups { + if err := os.Remove(oldName); err != nil { + return fmt.Errorf("can't remove old log file: %w", err) + } + continue + } + + if err := os.Rename(oldName, newName); err != nil { + return fmt.Errorf("can't rename old log file: %w", err) + } + } + + // Try to close current file and rename it + if err := rfw.file.Close(); err != nil { + return fmt.Errorf("can't close current log file: %w", err) + } + + if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { + return fmt.Errorf("can't rename current log file: %w", err) + } + + file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("can't open log file: %w", err) + } + rfw.file = file + rfw.size = 0 + + return nil +} diff --git a/public-api/cmd/public-api/init.go b/public-api/cmd/public-api/init.go deleted file mode 100644 index b185b338..00000000 --- a/public-api/cmd/public-api/init.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "os" - "strconv" - "time" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" -) - -func initLogger(level string) { - l := zerolog.New(os.Stdout). - With(). - Timestamp(). - Caller(). - Logger() - - zerolog.TimeFieldFormat = time.RFC3339Nano - zerolog.ErrorFieldName = "err" - zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { - short := file - for i := len(file) - 1; i > 0; i-- { - if file[i] == '/' { - short = file[i+1:] - break - } - } - file = short - return file + ":" + strconv.Itoa(line) - } - - log.Logger = l - - switch level { - case "DEBUG": - zerolog.SetGlobalLevel(zerolog.DebugLevel) - case "INFO": - zerolog.SetGlobalLevel(zerolog.InfoLevel) - case "WARN": - zerolog.SetGlobalLevel(zerolog.WarnLevel) - case "ERROR": - zerolog.SetGlobalLevel(zerolog.ErrorLevel) - case "FATAL": - zerolog.SetGlobalLevel(zerolog.FatalLevel) - default: - zerolog.SetGlobalLevel(zerolog.TraceLevel) - } -} diff --git a/public-api/cmd/public-api/main.go b/public-api/cmd/public-api/main.go index 52e9b325..599b6a85 100755 --- a/public-api/cmd/public-api/main.go +++ b/public-api/cmd/public-api/main.go @@ -14,6 +14,7 @@ import ( "github.com/google/gops/agent" "github.com/rs/zerolog/log" history_api "github.com/runtime-radar/runtime-radar/history-api/api" + "github.com/runtime-radar/runtime-radar/lib/logger" "github.com/runtime-radar/runtime-radar/lib/security" "github.com/runtime-radar/runtime-radar/lib/security/jwt" "github.com/runtime-radar/runtime-radar/lib/server/healthcheck" @@ -48,7 +49,7 @@ var ( func main() { cfg := config.New() - initLogger(cfg.LogLevel) + logger.Init("", cfg.LogLevel) log.Info().Str("build_release", build.Release).Str("build_branch", build.Branch).Str("build_commit", build.Commit).Str("build_date", build.Date).Msgf("-> %s started", build.AppName) defer log.Info().Msgf("<- %s exited", build.AppName) diff --git a/policy-enforcer/cmd/policy-enforcer/init.go b/public-api/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go similarity index 84% rename from policy-enforcer/cmd/policy-enforcer/init.go rename to public-api/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go index 3c24ebcf..f1e606a1 100644 --- a/policy-enforcer/cmd/policy-enforcer/init.go +++ b/public-api/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go @@ -1,4 +1,4 @@ -package main +package logger import ( "io" @@ -8,19 +8,18 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" - cs_logger "github.com/runtime-radar/runtime-radar/lib/logger" ) +// Logging configuration const ( - // Logging configuration logMaxNum = 3 logMaxSize = 10 * 1024 * 1024 // 10MB ) -func initLogger(file, level string) { +func Init(file, level string) { var out io.Writer = os.Stdout if file != "" { - rl, err := cs_logger.NewRotateFileWriter(file, logMaxNum, logMaxSize) + rl, err := NewRotateFileWriter(file, logMaxNum, logMaxSize) if err != nil { log.Fatal().Msgf("### Failed to create log file: %v", err) } diff --git a/public-api/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go b/public-api/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go new file mode 100644 index 00000000..50373280 --- /dev/null +++ b/public-api/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go @@ -0,0 +1,165 @@ +package logger + +import ( + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strconv" + "sync" +) + +var _ io.Writer = (*RotateFileWriter)(nil) + +// RotateFileWriter is a writer that rotates log files based on size and number of backups. +type RotateFileWriter struct { + mu sync.Mutex + + filename string + basename string + dir string + maxSize int + maxBackups int + + file *os.File + size int + nameMatcher *regexp.Regexp +} + +// NewRotateFileWriter creates a new RotateFileWriter for the given filename and +// configuration. It ensures the log directory exists, compiles a regexp to match +// rotated log files, opens the initial log file, and returns a RotateFileWriter. +func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { + if filename == "" { + return nil, fmt.Errorf("filename cannot be empty") + } + + if maxBackups <= 0 { + return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") + } + + if maxSize <= 0 { + return nil, fmt.Errorf("maxSize cannot be less or equal to zero") + } + + filename = filepath.Clean(filename) + basename := filepath.Base(filename) + dir := filepath.Dir(filename) + + err := os.MkdirAll(dir, 0755) + if err != nil { + return nil, fmt.Errorf("can't make directories for new logfile: %w", err) + } + + r, err := regexp.Compile(basename + `\.[0-9]+$`) + if err != nil { + return nil, fmt.Errorf("can't compile regexp: %w", err) + } + + file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, fmt.Errorf("can't open log file: %w", err) + } + + info, err := file.Stat() + if err != nil { + return nil, fmt.Errorf("can't get info about file: %w", err) + } + + rfw := &RotateFileWriter{ + filename: filename, + basename: basename, + dir: dir, + maxSize: maxSize, + maxBackups: maxBackups, + nameMatcher: r, + file: file, + size: int(info.Size()), + } + + return rfw, nil +} + +// Write implements the io.Writer interface. It writes the given bytes to the +// rolling log file, handling log rotation when the size exceeds the max. +// It locks access to the file for the duration of the write. +func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { + rfw.mu.Lock() + defer rfw.mu.Unlock() + + if rfw.size+len(p) > rfw.maxSize { + if err := rfw.rotate(); err != nil { + return 0, fmt.Errorf("can't rotate log file: %w", err) + } + } + + n, err = rfw.file.Write(p) + rfw.size += n + + return +} + +// rotate sequentially increments old log suffixes, +// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc +// and then closes current file and renames it to the first backup. +func (rfw *RotateFileWriter) rotate() error { + if rfw.maxBackups == 0 { + return nil + } + + cnt := 0 + err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + if d.IsDir() && path != rfw.dir { + return filepath.SkipDir + } + + if rfw.nameMatcher.MatchString(d.Name()) { + cnt++ + } + + return nil + }) + + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + for i := cnt; i > 0; i-- { + oldName := rfw.filename + "." + strconv.Itoa(i) + newName := rfw.filename + "." + strconv.Itoa(i+1) + // just remove everything over `rfw.maxBackups` + if i >= rfw.maxBackups { + if err := os.Remove(oldName); err != nil { + return fmt.Errorf("can't remove old log file: %w", err) + } + continue + } + + if err := os.Rename(oldName, newName); err != nil { + return fmt.Errorf("can't rename old log file: %w", err) + } + } + + // Try to close current file and rename it + if err := rfw.file.Close(); err != nil { + return fmt.Errorf("can't close current log file: %w", err) + } + + if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { + return fmt.Errorf("can't rename current log file: %w", err) + } + + file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("can't open log file: %w", err) + } + rfw.file = file + rfw.size = 0 + + return nil +} diff --git a/public-api/vendor/modules.txt b/public-api/vendor/modules.txt index 28e86eb3..884eda3a 100644 --- a/public-api/vendor/modules.txt +++ b/public-api/vendor/modules.txt @@ -727,6 +727,7 @@ github.com/runtime-radar/runtime-radar/history-api/api ## explicit; go 1.25 github.com/runtime-radar/runtime-radar/lib/config github.com/runtime-radar/runtime-radar/lib/errcommon +github.com/runtime-radar/runtime-radar/lib/logger github.com/runtime-radar/runtime-radar/lib/security github.com/runtime-radar/runtime-radar/lib/security/cipher github.com/runtime-radar/runtime-radar/lib/security/jwt diff --git a/runtime-monitor/cmd/runtime-monitor/init.go b/runtime-monitor/cmd/runtime-monitor/init.go deleted file mode 100644 index 877207fe..00000000 --- a/runtime-monitor/cmd/runtime-monitor/init.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "io" - "os" - "strconv" - "time" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - lib_logger "github.com/runtime-radar/runtime-radar/lib/logger" -) - -const ( - // Logging configuration - logMaxNum = 3 - logMaxSize = 10 * 1024 * 1024 // 10MB -) - -func initLogger(file, level string) { - var out io.Writer = os.Stdout - if file != "" { - rl, err := lib_logger.NewRotateFileWriter(file, logMaxNum, logMaxSize) - if err != nil { - log.Fatal().Msgf("### Failed to create log file: %v", err) - } - out = io.MultiWriter(os.Stdout, rl) - } - - l := zerolog.New(out). - With(). - Timestamp(). - Caller(). - Logger() - - zerolog.TimeFieldFormat = time.RFC3339Nano - zerolog.ErrorFieldName = "err" - zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { - short := file - for i := len(file) - 1; i > 0; i-- { - if file[i] == '/' { - short = file[i+1:] - break - } - } - file = short - return file + ":" + strconv.Itoa(line) - } - - log.Logger = l - - switch level { - case "DEBUG": - zerolog.SetGlobalLevel(zerolog.DebugLevel) - case "INFO": - zerolog.SetGlobalLevel(zerolog.InfoLevel) - case "WARN": - zerolog.SetGlobalLevel(zerolog.WarnLevel) - case "ERROR": - zerolog.SetGlobalLevel(zerolog.ErrorLevel) - case "FATAL": - zerolog.SetGlobalLevel(zerolog.FatalLevel) - default: - zerolog.SetGlobalLevel(zerolog.TraceLevel) - } -} diff --git a/runtime-monitor/cmd/runtime-monitor/main.go b/runtime-monitor/cmd/runtime-monitor/main.go index 654e9cf1..8be9ba89 100644 --- a/runtime-monitor/cmd/runtime-monitor/main.go +++ b/runtime-monitor/cmd/runtime-monitor/main.go @@ -13,6 +13,7 @@ import ( "github.com/google/gops/agent" "github.com/rs/zerolog/log" + "github.com/runtime-radar/runtime-radar/lib/logger" "github.com/runtime-radar/runtime-radar/lib/rabbit" "github.com/runtime-radar/runtime-radar/lib/security" "github.com/runtime-radar/runtime-radar/lib/security/jwt" @@ -53,7 +54,7 @@ var ( func main() { cfg := config.New() - initLogger(cfg.LogFile, cfg.LogLevel) + logger.Init(cfg.LogFile, cfg.LogLevel) log.Info().Str("build_release", build.Release).Str("build_branch", build.Branch).Str("build_commit", build.Commit).Str("build_date", build.Date).Msgf("-> %s started", build.AppName) defer log.Info().Msgf("<- %s exited", build.AppName) diff --git a/runtime-monitor/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go b/runtime-monitor/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go index 50373280..f1e606a1 100644 --- a/runtime-monitor/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go +++ b/runtime-monitor/vendor/github.com/runtime-radar/runtime-radar/lib/logger/logger.go @@ -1,165 +1,65 @@ package logger import ( - "fmt" "io" "os" - "path/filepath" - "regexp" "strconv" - "sync" -) - -var _ io.Writer = (*RotateFileWriter)(nil) - -// RotateFileWriter is a writer that rotates log files based on size and number of backups. -type RotateFileWriter struct { - mu sync.Mutex - - filename string - basename string - dir string - maxSize int - maxBackups int - - file *os.File - size int - nameMatcher *regexp.Regexp -} - -// NewRotateFileWriter creates a new RotateFileWriter for the given filename and -// configuration. It ensures the log directory exists, compiles a regexp to match -// rotated log files, opens the initial log file, and returns a RotateFileWriter. -func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { - if filename == "" { - return nil, fmt.Errorf("filename cannot be empty") - } - - if maxBackups <= 0 { - return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") - } - - if maxSize <= 0 { - return nil, fmt.Errorf("maxSize cannot be less or equal to zero") - } + "time" - filename = filepath.Clean(filename) - basename := filepath.Base(filename) - dir := filepath.Dir(filename) - - err := os.MkdirAll(dir, 0755) - if err != nil { - return nil, fmt.Errorf("can't make directories for new logfile: %w", err) - } - - r, err := regexp.Compile(basename + `\.[0-9]+$`) - if err != nil { - return nil, fmt.Errorf("can't compile regexp: %w", err) - } - - file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, fmt.Errorf("can't open log file: %w", err) - } - - info, err := file.Stat() - if err != nil { - return nil, fmt.Errorf("can't get info about file: %w", err) - } - - rfw := &RotateFileWriter{ - filename: filename, - basename: basename, - dir: dir, - maxSize: maxSize, - maxBackups: maxBackups, - nameMatcher: r, - file: file, - size: int(info.Size()), - } - - return rfw, nil -} - -// Write implements the io.Writer interface. It writes the given bytes to the -// rolling log file, handling log rotation when the size exceeds the max. -// It locks access to the file for the duration of the write. -func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { - rfw.mu.Lock() - defer rfw.mu.Unlock() - - if rfw.size+len(p) > rfw.maxSize { - if err := rfw.rotate(); err != nil { - return 0, fmt.Errorf("can't rotate log file: %w", err) - } - } - - n, err = rfw.file.Write(p) - rfw.size += n - - return -} + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) -// rotate sequentially increments old log suffixes, -// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc -// and then closes current file and renames it to the first backup. -func (rfw *RotateFileWriter) rotate() error { - if rfw.maxBackups == 0 { - return nil - } +// Logging configuration +const ( + logMaxNum = 3 + logMaxSize = 10 * 1024 * 1024 // 10MB +) - cnt := 0 - err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { +func Init(file, level string) { + var out io.Writer = os.Stdout + if file != "" { + rl, err := NewRotateFileWriter(file, logMaxNum, logMaxSize) if err != nil { - return fmt.Errorf("can't walk directory: %w", err) - } - - if d.IsDir() && path != rfw.dir { - return filepath.SkipDir + log.Fatal().Msgf("### Failed to create log file: %v", err) } - - if rfw.nameMatcher.MatchString(d.Name()) { - cnt++ - } - - return nil - }) - - if err != nil { - return fmt.Errorf("can't walk directory: %w", err) + out = io.MultiWriter(os.Stdout, rl) } - for i := cnt; i > 0; i-- { - oldName := rfw.filename + "." + strconv.Itoa(i) - newName := rfw.filename + "." + strconv.Itoa(i+1) - // just remove everything over `rfw.maxBackups` - if i >= rfw.maxBackups { - if err := os.Remove(oldName); err != nil { - return fmt.Errorf("can't remove old log file: %w", err) + l := zerolog.New(out). + With(). + Timestamp(). + Caller(). + Logger() + + zerolog.TimeFieldFormat = time.RFC3339Nano + zerolog.ErrorFieldName = "err" + zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string { + short := file + for i := len(file) - 1; i > 0; i-- { + if file[i] == '/' { + short = file[i+1:] + break } - continue - } - - if err := os.Rename(oldName, newName); err != nil { - return fmt.Errorf("can't rename old log file: %w", err) } + file = short + return file + ":" + strconv.Itoa(line) } - // Try to close current file and rename it - if err := rfw.file.Close(); err != nil { - return fmt.Errorf("can't close current log file: %w", err) + log.Logger = l + + switch level { + case "DEBUG": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "INFO": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "WARN": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "ERROR": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + case "FATAL": + zerolog.SetGlobalLevel(zerolog.FatalLevel) + default: + zerolog.SetGlobalLevel(zerolog.TraceLevel) } - - if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { - return fmt.Errorf("can't rename current log file: %w", err) - } - - file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - return fmt.Errorf("can't open log file: %w", err) - } - rfw.file = file - rfw.size = 0 - - return nil } diff --git a/runtime-monitor/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go b/runtime-monitor/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go new file mode 100644 index 00000000..50373280 --- /dev/null +++ b/runtime-monitor/vendor/github.com/runtime-radar/runtime-radar/lib/logger/rotate.go @@ -0,0 +1,165 @@ +package logger + +import ( + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strconv" + "sync" +) + +var _ io.Writer = (*RotateFileWriter)(nil) + +// RotateFileWriter is a writer that rotates log files based on size and number of backups. +type RotateFileWriter struct { + mu sync.Mutex + + filename string + basename string + dir string + maxSize int + maxBackups int + + file *os.File + size int + nameMatcher *regexp.Regexp +} + +// NewRotateFileWriter creates a new RotateFileWriter for the given filename and +// configuration. It ensures the log directory exists, compiles a regexp to match +// rotated log files, opens the initial log file, and returns a RotateFileWriter. +func NewRotateFileWriter(filename string, maxBackups int, maxSize int) (*RotateFileWriter, error) { + if filename == "" { + return nil, fmt.Errorf("filename cannot be empty") + } + + if maxBackups <= 0 { + return nil, fmt.Errorf("maxBackups cannot be less or equal to zero") + } + + if maxSize <= 0 { + return nil, fmt.Errorf("maxSize cannot be less or equal to zero") + } + + filename = filepath.Clean(filename) + basename := filepath.Base(filename) + dir := filepath.Dir(filename) + + err := os.MkdirAll(dir, 0755) + if err != nil { + return nil, fmt.Errorf("can't make directories for new logfile: %w", err) + } + + r, err := regexp.Compile(basename + `\.[0-9]+$`) + if err != nil { + return nil, fmt.Errorf("can't compile regexp: %w", err) + } + + file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, fmt.Errorf("can't open log file: %w", err) + } + + info, err := file.Stat() + if err != nil { + return nil, fmt.Errorf("can't get info about file: %w", err) + } + + rfw := &RotateFileWriter{ + filename: filename, + basename: basename, + dir: dir, + maxSize: maxSize, + maxBackups: maxBackups, + nameMatcher: r, + file: file, + size: int(info.Size()), + } + + return rfw, nil +} + +// Write implements the io.Writer interface. It writes the given bytes to the +// rolling log file, handling log rotation when the size exceeds the max. +// It locks access to the file for the duration of the write. +func (rfw *RotateFileWriter) Write(p []byte) (n int, err error) { + rfw.mu.Lock() + defer rfw.mu.Unlock() + + if rfw.size+len(p) > rfw.maxSize { + if err := rfw.rotate(); err != nil { + return 0, fmt.Errorf("can't rotate log file: %w", err) + } + } + + n, err = rfw.file.Write(p) + rfw.size += n + + return +} + +// rotate sequentially increments old log suffixes, +// e.g. `app.log.3` -> `app.log.4`, `app.log.2` -> `app.log.3` etc +// and then closes current file and renames it to the first backup. +func (rfw *RotateFileWriter) rotate() error { + if rfw.maxBackups == 0 { + return nil + } + + cnt := 0 + err := filepath.WalkDir(rfw.dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + if d.IsDir() && path != rfw.dir { + return filepath.SkipDir + } + + if rfw.nameMatcher.MatchString(d.Name()) { + cnt++ + } + + return nil + }) + + if err != nil { + return fmt.Errorf("can't walk directory: %w", err) + } + + for i := cnt; i > 0; i-- { + oldName := rfw.filename + "." + strconv.Itoa(i) + newName := rfw.filename + "." + strconv.Itoa(i+1) + // just remove everything over `rfw.maxBackups` + if i >= rfw.maxBackups { + if err := os.Remove(oldName); err != nil { + return fmt.Errorf("can't remove old log file: %w", err) + } + continue + } + + if err := os.Rename(oldName, newName); err != nil { + return fmt.Errorf("can't rename old log file: %w", err) + } + } + + // Try to close current file and rename it + if err := rfw.file.Close(); err != nil { + return fmt.Errorf("can't close current log file: %w", err) + } + + if err := os.Rename(rfw.filename, rfw.filename+".1"); err != nil { + return fmt.Errorf("can't rename current log file: %w", err) + } + + file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("can't open log file: %w", err) + } + rfw.file = file + rfw.size = 0 + + return nil +}