package daemon import ( "log/slog" "os" "path/filepath" "sync" ) // RotatingWriter is a thread-safe io.Writer that appends to a log file // and rotates it when the file exceeds maxSize. One backup (.1) is kept, // so the maximum disk usage is ≈ 2 × maxSize. type RotatingWriter struct { mu sync.Mutex file *os.File path string maxSize int64 curSize int64 } func NewRotatingWriter(path string, maxSize int64) (*RotatingWriter, error) { if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return nil, err } f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { return nil, err } info, err := f.Stat() if err != nil { f.Close() return nil, err } return &RotatingWriter{ file: f, path: path, maxSize: maxSize, curSize: info.Size(), }, nil } func (w *RotatingWriter) Write(p []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() if w.file == nil { return 0, os.ErrClosed } n, err := w.file.Write(p) w.curSize += int64(n) if w.curSize > w.maxSize { w.rotate() } return n, err } func (w *RotatingWriter) rotate() { w.file.Close() backup := w.path + ".1" os.Remove(backup) if err := os.Rename(w.path, backup); err != nil { slog.Warn("logrotate: rename failed", "error", err, "path", w.path, "backup", backup) } f, err := os.OpenFile(w.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { // If we cannot open the new log file, w.file will be nil. // Write() checks for nil and returns os.ErrClosed instead of panicking. w.file = nil w.curSize = 0 return } w.file = f w.curSize = 0 } func (w *RotatingWriter) Close() error { w.mu.Lock() defer w.mu.Unlock() if w.file != nil { return w.file.Close() } return nil }