restructured logger from budigt

This commit is contained in:
juancwu 2026-04-25 20:35:03 +00:00
commit b2fd12b1c8
11 changed files with 1227 additions and 1 deletions

113
file_rotation.go Normal file
View file

@ -0,0 +1,113 @@
package splinter
import (
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"time"
)
const rotationTimestampFormat = "20060102T150405.000000000Z"
// shouldRotate reports whether the current file has hit a rotation trigger.
// Caller must hold s.mu.
func (s *FileStream) shouldRotate() bool {
if s.cfg.MaxSizeMB > 0 && s.counter.n >= int64(s.cfg.MaxSizeMB)*1024*1024 {
return true
}
if s.cfg.MaxAge > 0 && time.Since(s.openedAt) >= s.cfg.MaxAge {
return true
}
return false
}
// rotate closes the current file, renames it to a timestamped backup, opens
// a fresh file, and kicks off async compression + pruning. Caller holds s.mu.
func (s *FileStream) rotate() error {
if err := s.file.Close(); err != nil {
return err
}
ts := time.Now().UTC().Format(rotationTimestampFormat)
ext := filepath.Ext(s.path)
base := s.path[:len(s.path)-len(ext)]
backupPath := fmt.Sprintf("%s.%s%s", base, ts, ext)
if err := os.Rename(s.path, backupPath); err != nil {
return err
}
f, err := os.OpenFile(s.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return err
}
s.attach(f, 0)
s.openedAt = time.Now()
go s.compressAndPrune(backupPath, base, ext)
return nil
}
// compressAndPrune runs in the background after rotation: gzips the new
// backup (if configured) and deletes any stale backups beyond MaxBackups.
// Errors are swallowed since this is best-effort housekeeping.
func (s *FileStream) compressAndPrune(backupPath, base, ext string) {
if s.cfg.Compress {
_ = gzipFile(backupPath)
}
s.pruneBackups(base, ext)
}
// gzipFile compresses src to src+".gz", then removes src on success.
func gzipFile(src string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile(src+".gz", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
return err
}
gz := gzip.NewWriter(out)
if _, err := io.Copy(gz, in); err != nil {
gz.Close()
out.Close()
os.Remove(src + ".gz")
return err
}
if err := gz.Close(); err != nil {
out.Close()
os.Remove(src + ".gz")
return err
}
if err := out.Close(); err != nil {
os.Remove(src + ".gz")
return err
}
return os.Remove(src)
}
// pruneBackups removes the oldest rotated files when the total exceeds
// MaxBackups. Both raw and gzipped backups are considered.
func (s *FileStream) pruneBackups(base, ext string) {
raw, _ := filepath.Glob(fmt.Sprintf("%s.*%s", base, ext))
gz, _ := filepath.Glob(fmt.Sprintf("%s.*%s.gz", base, ext))
all := append(raw, gz...)
if len(all) <= s.cfg.MaxBackups {
return
}
// Lex sort puts older timestamps first; trailing ".gz" sorts after the
// raw form, so any transient duplicate during compression is handled
// naturally on the next rotation.
sort.Strings(all)
for _, p := range all[:len(all)-s.cfg.MaxBackups] {
_ = os.Remove(p)
}
}