package admin import ( "fmt" "io" "log" "os" "path/filepath" "sync" "time" ) type rotatingLogWriter struct { path string maxBytes int64 maxBackups int file *os.File size int64 mu sync.Mutex } func ConfigureLogging(cfg Config) (io.Closer, error) { if cfg.LogFile == "" { return nil, nil } writer, err := newRotatingLogWriter(cfg.LogFile, cfg.LogMaxBytes, cfg.LogMaxBackups) if err != nil { return nil, err } multi := io.MultiWriter(os.Stdout, writer) log.SetOutput(multi) log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.LUTC) return writer, nil } func newRotatingLogWriter(path string, maxBytes int64, maxBackups int) (*rotatingLogWriter, error) { if maxBytes <= 0 { maxBytes = 10 * 1024 * 1024 } if maxBackups <= 0 { maxBackups = 5 } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return nil, fmt.Errorf("create log dir: %w", err) } file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) if err != nil { return nil, fmt.Errorf("open log file: %w", err) } info, err := file.Stat() if err != nil { file.Close() return nil, fmt.Errorf("stat log file: %w", err) } return &rotatingLogWriter{ path: path, maxBytes: maxBytes, maxBackups: maxBackups, file: file, size: info.Size(), }, nil } func (w *rotatingLogWriter) Write(p []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() if w.size+int64(len(p)) > w.maxBytes { if err := w.rotateLocked(); err != nil { return 0, err } } n, err := w.file.Write(p) w.size += int64(n) return n, err } func (w *rotatingLogWriter) Close() error { w.mu.Lock() defer w.mu.Unlock() if w.file == nil { return nil } err := w.file.Close() w.file = nil return err } func (w *rotatingLogWriter) rotateLocked() error { if w.file != nil { if err := w.file.Close(); err != nil { return err } } stamp := time.Now().UTC().Format("20060102T150405") rotated := fmt.Sprintf("%s.%s", w.path, stamp) if err := os.Rename(w.path, rotated); err != nil && !os.IsNotExist(err) { return fmt.Errorf("rotate log: %w", err) } if err := w.pruneLocked(); err != nil { return err } file, err := os.OpenFile(w.path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) if err != nil { return fmt.Errorf("open rotated log file: %w", err) } w.file = file w.size = 0 return nil } func (w *rotatingLogWriter) pruneLocked() error { pattern := w.path + ".*" matches, err := filepath.Glob(pattern) if err != nil { return fmt.Errorf("list rotated logs: %w", err) } if len(matches) <= w.maxBackups { return nil } sortStrings(matches) for _, path := range matches[:len(matches)-w.maxBackups] { if err := os.Remove(path); err != nil && !os.IsNotExist(err) { return fmt.Errorf("remove old log %s: %w", path, err) } } return nil } func sortStrings(values []string) { for i := 1; i < len(values); i++ { value := values[i] j := i - 1 for ; j >= 0 && values[j] > value; j-- { values[j+1] = values[j] } values[j+1] = value } }