package output
import (
"errors"
"io"
"os"
"path/filepath"
"sync"
)
// FileOptions controls the rotating file writer. Max size is the only
// rotation trigger; time-based rotation is out of scope because local
// development does not need it.
type FileOptions struct {
// Path is the filename to write to. Empty means "./lambdalog.log".
Path string
// MaxBytes triggers rotation when the active file reaches this size.
// 0 disables rotation.
MaxBytes int64
// MaxBackups caps the number of rotated files kept on disk. Older
// backups beyond MaxBackups are deleted.
MaxBackups int
}
// File returns a rotating file writer intended for local development. In
// production, prefer Stdout or CloudWatch.
func File(opts FileOptions) (io.WriteCloser, error) {
if opts.Path == "" {
opts.Path = "./lambdalog.log"
}
if err := os.MkdirAll(filepath.Dir(opts.Path), 0o755); err != nil {
return nil, err
}
f, err := os.OpenFile(opts.Path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return nil, err
}
info, err := f.Stat()
if err != nil {
_ = f.Close()
return nil, err
}
return &fileWriter{opts: opts, f: f, size: info.Size()}, nil
}
type fileWriter struct {
opts FileOptions
mu sync.Mutex
f *os.File
size int64
}
// Write appends p and rotates if the size limit is exceeded.
func (w *fileWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
if w.f == nil {
return 0, errors.New("output: file writer closed")
}
if w.opts.MaxBytes > 0 && w.size+int64(len(p)) > w.opts.MaxBytes {
if err := w.rotateLocked(); err != nil {
return 0, err
}
}
n, err := w.f.Write(p)
w.size += int64(n)
return n, err
}
// Close closes the underlying file.
func (w *fileWriter) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
if w.f == nil {
return nil
}
err := w.f.Close()
w.f = nil
return err
}
func (w *fileWriter) rotateLocked() error {
if err := w.f.Close(); err != nil {
return err
}
rotated := w.opts.Path + ".1"
_ = os.Remove(rotated)
if err := os.Rename(w.opts.Path, rotated); err != nil {
return err
}
// Shift older backups by one; discard anything past MaxBackups.
for i := w.opts.MaxBackups; i >= 2; i-- {
prev := w.opts.Path + "." + itoa(i-1)
next := w.opts.Path + "." + itoa(i)
_ = os.Rename(prev, next)
}
f, err := os.OpenFile(w.opts.Path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return err
}
w.f = f
w.size = 0
return nil
}
func itoa(n int) string {
if n < 10 {
return string(rune('0' + n))
}
return itoa(n/10) + string(rune('0'+n%10))
}