Julien Sellier

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