Documentation
¶
Overview ¶
Package logrotate provides a rotating file writer for logs.
It is a pluggable io.WriteCloser sitting at the bottom of a logging stack — pass it to log.SetOutput, slog.NewJSONHandler or zapcore.AddSync — that rotates the file it writes to by size, by wall-clock schedule, or on demand, and prunes old backups by count, age and total disk usage, with optional background compression.
w, err := logrotate.New("/var/log/myapp/app.log",
logrotate.WithMaxSize(64*logrotate.MB),
logrotate.WithRotateEvery(24*time.Hour),
logrotate.WithMaxBackups(14),
logrotate.WithMaxTotalSize(1*logrotate.GB),
logrotate.WithCompress(),
)
if err != nil {
// configuration and permission errors surface here, not mid-flight
}
defer w.Close()
log.SetOutput(w)
The active file keeps its configured name at all times; rotation renames it to "app-<timestamp>.log" (compressed to "app-<timestamp>.log.gz") in the same directory. Time-based rotation is evaluated on write rather than by a background timer: an idle process creates no empty files, backups are stamped with the boundary that ended their period, and after a restart the previous period is recovered from the file's modification time.
logrotate assumes a single process writes to a given file. It has no third-party dependencies.
Example ¶
The zero-configuration setup: rotate at 100 MB, keep every backup.
package main
import (
"log"
"os"
"path/filepath"
"github.com/libtnb/logrotate"
)
func main() {
w, err := logrotate.New(filepath.Join(os.TempDir(), "example", "app.log"))
if err != nil {
log.Fatal(err)
}
defer func() { _ = w.Close() }()
log.SetOutput(w)
log.Println("application started")
}
Output:
Example (Production) ¶
A production setup: daily rotation at midnight plus a size cap, two weeks of compressed backups bounded to 1 GB of disk.
package main
import (
"log"
"log/slog"
"time"
"github.com/libtnb/logrotate"
)
func main() {
w, err := logrotate.New("/var/log/myapp/app.log",
logrotate.WithMaxSize(64*logrotate.MB),
logrotate.WithRotateEvery(24*time.Hour),
logrotate.WithMaxBackups(14),
logrotate.WithMaxAge(14*logrotate.Day),
logrotate.WithMaxTotalSize(1*logrotate.GB),
logrotate.WithCompress(),
logrotate.WithErrorHandler(func(err error) {
slog.Warn("log maintenance", "err", err)
}),
)
if err != nil {
log.Fatal(err)
}
defer func() { _ = w.Close() }()
logger := slog.New(slog.NewJSONHandler(w, nil))
logger.Info("application started")
}
Output:
Index ¶
- Constants
- Variables
- type Clock
- type Compressor
- type GzipCompressor
- type Option
- func WithBackupTimeFormat(layout string) Option
- func WithClock(clock Clock) Option
- func WithCompress() Option
- func WithCompressor(comp Compressor) Option
- func WithErrorHandler(fn func(error)) Option
- func WithFileMode(mode fs.FileMode) Option
- func WithLocation(loc *time.Location) Option
- func WithMaxAge(d time.Duration) Option
- func WithMaxBackups(n int) Option
- func WithMaxSize(n int64) Option
- func WithMaxTotalSize(n int64) Option
- func WithRotateAt(times ...string) Option
- func WithRotateEvery(d time.Duration) Option
- type Writer
Examples ¶
Constants ¶
const ( KB int64 = 1 << 10 MB int64 = 1 << 20 GB int64 = 1 << 30 )
Byte size units for WithMaxSize and WithMaxTotalSize.
const Day = 24 * time.Hour
Day is a convenience duration for retention options, e.g. WithMaxAge(14*logrotate.Day). Retention compares plain durations; no calendar or DST semantics are implied.
Variables ¶
var ErrClosed = errors.New("logrotate: writer is closed")
ErrClosed is returned by Write, Rotate and Reopen after Close.
Functions ¶
This section is empty.
Types ¶
type Clock ¶
Clock supplies the current time. The default clock uses time.Now; tests can substitute a fake via WithClock.
type Compressor ¶
type Compressor interface {
// Compress reads src to EOF and writes the compressed form to dst.
Compress(dst io.Writer, src io.Reader) error
// Extension is the suffix appended to compressed backups, e.g. ".gz".
// It must start with a dot.
Extension() string
}
Compressor compresses rotated backups. Implementations are invoked from a single background goroutine, one file at a time.
The interface is deliberately stream-based so third-party algorithms plug in without this package depending on them; see the package example for a zstd adapter.
type GzipCompressor ¶
type GzipCompressor struct {
// Level is a compress/gzip compression level. The zero value selects
// gzip.DefaultCompression.
Level int
}
GzipCompressor implements Compressor with compress/gzip.
func (GzipCompressor) Extension ¶
func (GzipCompressor) Extension() string
type Option ¶
type Option func(*config)
Option configures a Writer. Options are applied by New and validated together; an invalid combination makes New return an error instead of degrading silently at runtime.
func WithBackupTimeFormat ¶
WithBackupTimeFormat sets the time layout embedded in backup file names. The default is "2006-01-02T15-04-05.000". The layout is validated by New: it must survive a format/parse round-trip and may not produce path separators. A layout coarser than the rotation frequency is fine; colliding names get a numeric sequence suffix (name-2006-01-02.1.log).
func WithClock ¶
WithClock substitutes the time source, letting tests drive rotation deterministically.
func WithCompress ¶
func WithCompress() Option
WithCompress gzip-compresses rotated backups in the background. Compression is atomic: a backup is written to a temporary file and renamed into place, so a crash never leaves a half-written archive behind.
func WithCompressor ¶
func WithCompressor(comp Compressor) Option
WithCompressor compresses rotated backups with a custom Compressor, for example a zstd implementation. Passing nil disables compression (the default).
Example ¶
Trade compression speed for ratio with a tuned built-in compressor. Any algorithm plugs in the same way; see the README for a zstd adapter.
package main
import (
"log"
"os"
"path/filepath"
"github.com/libtnb/logrotate"
)
func main() {
w, err := logrotate.New(filepath.Join(os.TempDir(), "example", "app.log"),
logrotate.WithCompressor(logrotate.GzipCompressor{Level: 9}),
)
if err != nil {
log.Fatal(err)
}
defer func() { _ = w.Close() }()
}
Output:
func WithErrorHandler ¶
WithErrorHandler registers fn to receive errors from background maintenance (compression, retention cleanup) and from deferred rotations that could not be reported through a Write return value. fn is called from the background goroutine and must not call back into the Writer. Without a handler such errors are dropped.
func WithFileMode ¶
WithFileMode sets the permission bits for newly created log files. The mode is applied exactly, unaffected by the process umask. When unset, a new file inherits the permissions of the file it replaces, or 0o600 if there is none. On Windows, which only models a read-only flag, non-writable modes map to read-only and everything else is best-effort.
func WithLocation ¶
WithLocation sets the time zone for backup timestamps and time-based rotation boundaries: WithLocation(time.Local) follows the host zone, and any fixed zone works regardless of where the process runs. The default is UTC.
func WithMaxAge ¶
WithMaxAge removes backups whose rotation timestamp is older than d. 0, the default, disables age-based removal.
func WithMaxBackups ¶
WithMaxBackups limits how many rotated backups are retained; the oldest are removed first. 0, the default, keeps all backups. A backup and its compressed form count as one.
func WithMaxSize ¶
WithMaxSize sets the maximum size, in bytes, the log file may reach before being rotated. The KB, MB and GB constants help readability, e.g. WithMaxSize(64*logrotate.MB). A value of 0 disables size-based rotation. The default is 100*MB.
A single record larger than the limit is still written in full (rejecting log records would lose data); it gets a file of its own, which is rotated out immediately after the write.
func WithMaxTotalSize ¶
WithMaxTotalSize caps the combined size, in bytes, of all retained backups; the oldest are removed until the rest fit. The active log file does not count against the cap, so worst-case disk usage is roughly maxSize + maxTotalSize. 0, the default, disables the cap.
The cap is strict and reflects actual bytes on disk: if even the newest backup exceeds it on its own, that backup is removed too, and a backup whose compression failed counts at its uncompressed size until compression succeeds.
func WithRotateAt ¶
WithRotateAt rotates the log daily at the given wall-clock times, each in "HH:MM" 24-hour form (e.g. "00:00", "03:30"). Times are interpreted in the configured location (UTC unless WithLocation is set) and may be combined with WithRotateEvery; the earliest upcoming boundary wins. Repeated options accumulate.
The same lazy-boundary semantics as WithRotateEvery apply.
func WithRotateEvery ¶
WithRotateEvery rotates the log on wall-clock boundaries every d, anchored at midnight in the configured location (UTC unless WithLocation is set): with d of one hour rotation happens on the hour, with 24h at midnight. The final interval of a day is shortened when d does not divide 24h evenly, so every day has the same boundary sequence. d must be between 1s and 24h; for calendar times of day use WithRotateAt instead.
Boundaries are checked when writes occur, so an idle writer does not create empty files; the first write after a boundary triggers the rotation, and the backup is stamped with the boundary time rather than the write time. Across restarts the current period is recovered from the log file's modification time: a leftover file whose last write falls in a previous period is rotated out before it can receive new records.
type Writer ¶
type Writer struct {
// contains filtered or unexported fields
}
Writer is a rotating file writer implementing io.WriteCloser.
The file given to New is always the active log file; rotation renames it to a backup in the same directory ("name-<timestamp>.ext", plus a ".<n>" sequence on collision and the compressor extension once compressed) and reopens the original path, so external tools can rely on a stable name. Rotation happens when the file reaches its size limit, when a wall-clock boundary configured via WithRotateEvery or WithRotateAt has passed, or on an explicit Rotate call. Compression and retention (WithMaxBackups, WithMaxAge, WithMaxTotalSize) run on a single background goroutine and never block writes.
A Writer is safe for concurrent use by multiple goroutines. Like all single-writer rotation schemes, it assumes it is the only process writing to the file.
func New ¶
New creates a Writer appending to filename, creating the file and any missing parent directories on the spot so configuration or permission problems surface here rather than at the first log record.
If time-based rotation is configured and filename already exists, the current rotation period is recovered from the file's modification time; a leftover file last written in a previous period is rotated out immediately.
func (*Writer) Close ¶
Close closes the active file and stops the background maintenance goroutine, waiting for in-flight compression or cleanup to finish. Close is idempotent. After Close, Write returns ErrClosed.
func (*Writer) Reopen ¶
Reopen closes and reopens the active file path without renaming anything. It is meant for coordination with external tools that move or truncate the log themselves (e.g. logrotate(8)); after they signal the process, Reopen resumes logging into a fresh file at the original path.
Example ¶
Let an external tool such as logrotate(8) move the file, then reopen the original path on SIGUSR1 instead of rotating in-process.
package main
import (
"log"
"os"
"os/signal"
"path/filepath"
"syscall"
"github.com/libtnb/logrotate"
)
func main() {
w, err := logrotate.New(filepath.Join(os.TempDir(), "example", "app.log"),
logrotate.WithMaxSize(0), // rotation is fully delegated to the external tool
)
if err != nil {
log.Fatal(err)
}
defer func() { _ = w.Close() }()
log.SetOutput(w)
usr1 := make(chan os.Signal, 1)
signal.Notify(usr1, syscall.SIGUSR1)
go func() {
for range usr1 {
if err := w.Reopen(); err != nil {
log.Printf("reopen: %v", err)
}
}
}()
}
Output:
func (*Writer) Rotate ¶
Rotate rotates the log immediately regardless of size or schedule, for example in response to SIGHUP. The backup is stamped with the current time. Rotating an empty file produces no backup; the file is simply reused.
Example ¶
Rotate on SIGHUP, the conventional signal for reopening logs.
package main
import (
"log"
"os"
"os/signal"
"path/filepath"
"syscall"
"github.com/libtnb/logrotate"
)
func main() {
w, err := logrotate.New(filepath.Join(os.TempDir(), "example", "app.log"))
if err != nil {
log.Fatal(err)
}
defer func() { _ = w.Close() }()
log.SetOutput(w)
hup := make(chan os.Signal, 1)
signal.Notify(hup, syscall.SIGHUP)
go func() {
for range hup {
if err := w.Rotate(); err != nil {
log.Printf("rotate: %v", err)
}
}
}()
}
Output: