Last update: 2026-02-15
Rotating Logfile in Go
In pkg/logfile:
package logfile
import (
"fmt"
"log/slog"
"os"
"path/filepath"
"sync"
"time"
)
type File struct {
mu *sync.RWMutex
f *os.File
dirpath string
key string
logger *slog.Logger
}
func Open(dirpath, key string, logger *slog.Logger) (a *File, err error) {
f, err := openFile(dirpath, key, time.Now().UTC())
if err != nil {
return nil, fmt.Errorf("open file: %w", err)
}
a = &File{mu: &sync.RWMutex{}, f: f, dirpath: dirpath, key: key, logger: logger}
go a.run()
return a, nil
}
func (f *File) Stop() (err error) {
f.mu.Lock()
defer f.mu.Unlock()
return f.syncAndClose()
}
func (f *File) Write(b []byte) (n int, err error) {
f.mu.Lock()
defer f.mu.Unlock()
return f.f.Write(b)
}
func (f *File) run() {
var err error
for {
time.Sleep(30 * time.Second)
if f.f.Name() == fpath(f.dirpath, f.key, time.Now().UTC()) {
continue
}
err = f.rotate()
if err != nil {
f.logger.Error("rotate logfile", "error", err)
}
}
}
func (f *File) syncAndClose() (err error) {
err = f.f.Sync()
if err != nil {
return fmt.Errorf("sync logfile: %w", err)
}
err = f.f.Close()
if err != nil {
return fmt.Errorf("close logfile: %w", err)
}
return nil
}
func (f *File) rotate() (err error) {
f.mu.Lock()
defer f.mu.Unlock()
// Open new file.
newFile, err := openFile(f.dirpath, f.key, time.Now().UTC())
if err != nil {
return fmt.Errorf("open new file: %w", err)
}
// Sync/close current file and switch to new file.
err = f.syncAndClose()
if err != nil {
return fmt.Errorf("close current file: %w", err)
}
f.f = newFile
return nil
}
func fname(key string, date time.Time) string {
return date.UTC().Format(time.DateOnly) + "." + key + ".log"
}
func fpath(dirpath, key string, date time.Time) string {
return filepath.Join(dirpath, fname(key, date))
}
func openFile(dirpath, key string, date time.Time) (f *os.File, err error) {
return os.OpenFile(fpath(dirpath, key, date), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
}
In main.go:
f, err := logfile.Open("/var/log/myapp", "app", logger)
if err != nil {
panic(fmt.Errorf("open app logfile: %w", err))
}
defer f.Stop()
rotatingLogger := slog.New(slog.NewTextHandler(f, &slog.HandlerOptions{Level: slog.LevelDebug}))
Limitations & Next Steps
- Use a channel instead of a mutex
- Support rotating on size threshold