timberjack

package module
v1.3.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 21, 2025 License: MIT Imports: 18 Imported by: 7

README

timberjack Go Reference Go Report Card Audit Version Coverage Status Mentioned in Awesome Go

Timberjack is a Go package for writing logs to rolling files.

Timberjack is a forked and enhanced version of lumberjack, adding time-based rotation, clock-scheduled rotation, and opt-in compression (gzip or zstd). Package timberjack provides a rolling logger with support for size-based and time-based log rotation.

Installation

go get github.com/DeRuina/timberjack

Import

import "github.com/DeRuina/timberjack"

Timberjack is a pluggable component that manages log file writing and rotation. It works with any logger that writes to an io.Writer, including the standard library’s log package.

⚠️ Timberjack assumes one process writes to a given file. Reusing the same config from multiple processes on the same machine may lead to unexpected behavior.

Example

To use timberjack with the standard library's log package, including interval-based and scheduled minute/daily rotation:

import (
	"log"
	"time"
	"github.com/DeRuina/timberjack"
)

func main() {
	logger := &timberjack.Logger{
        Filename:          "/var/log/myapp/foo.log",    // Choose an appropriate path
        MaxSize:            500,                        // megabytes
        MaxBackups:         3,                          // backups
        MaxAge:             28,                         // days
        Compression:        "gzip",                     // "none" | "gzip" | "zstd" (preferred over legacy Compress)
        LocalTime:          true,                       // default: false (use UTC)
        RotationInterval:   24 * time.Hour,             // Rotate daily if no other rotation met
        RotateAtMinutes:    []int{0, 15, 30, 45},       // Also rotate at HH:00, HH:15, HH:30, HH:45
        RotateAt:           []string{"00:00", "12:00"}, // Also rotate at 00:00 and 12:00 each day
        BackupTimeFormat:   "2006-01-02-15-04-05",      // Rotated files will have format <logfilename>-2006-01-02-15-04-05-<reason>.log
        AppendTimeAfterExt: true,                       // put timestamp after ".log" (foo.log-<timestamp>-<reason>)
        FileMode:           0o644,                      // Custom permissions for newly created files. If unset or 0, defaults to 640.
	}
	log.SetOutput(logger)
	defer logger.Close() // Ensure logger is closed on application exit to stop goroutines

	log.Println("Application started")
	// ... your application logic ...
	log.Println("Application shutting down")
}

Manual rotation (e.g. on SIGHUP):

import (
    "log"
    "os"
    "os/signal"
    "syscall"

    "github.com/DeRuina/timberjack"
)


func main() {
    l := &timberjack.Logger{ Filename: "/var/log/myapp/foo.log" }
    log.SetOutput(l)
    defer l.Close()

    // Manual rotation on SIGHUP
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGHUP)

    go func() {
        for range c {
            // 1) Classic behavior: auto-pick "time" (if due) or "size"
            // _ = l.Rotate()

            // 2) New: tag the backup with your own reason
            _ = l.RotateWithReason("reload")
        }
    }()

    // ...
}

Logger Configuration

type Logger struct {
    Filename          string        // File to write logs to
    MaxSize           int           // Max size (MB) before rotation (default: 100)
    MaxAge            int           // Max age (days) to retain old logs
    MaxBackups        int           // Max number of backups to keep
    LocalTime         bool          // Use local time in rotated filenames

    // Compression controls post-rotation compression:
    //   "none" | "gzip" | "zstd"
    // Unknown/empty default to "none", unless the legacy Compress is true (see below).
    Compression       string

    // Deprecated: prefer Compression.
    // If Compression is empty and Compress is true = gzip compression.
    // Back-compat shim for old configs; will be removed in v2.
    Compress          bool


    RotationInterval  time.Duration // Rotate after this duration (if > 0)
    RotateAtMinutes   []int         // Specific minutes within an hour (0–59) to trigger rotation
    RotateAt          []string      // Specific daily times (HH:MM, 24-hour) to trigger rotation
    BackupTimeFormat  string        // Optional. If unset or invalid, defaults to 2006-01-02T15-04-05.000 (with fallback warning)
    AppendTimeAfterExt    bool      // if true, name backups like foo.log-<timestamp>-<reason> defaults to foo-<timestamp>-<reason>.log
    FileMode          os.FileMode   // Use custom permissions for newly created files. If zero (unset or 0), defaults to 640.
}

How Rotation Works

  1. Size-Based: If a write operation causes the current log file to exceed MaxSize, the file is rotated before the write. The backup filename will include -size as the reason.
  2. Time-Based (Interval): If RotationInterval is set (e.g., 24 * time.Hour for daily rotation) and this duration has passed since the last rotation (of any type that updates the interval timer), the file is rotated upon the next write. The backup filename will include -time as the reason.
  3. Scheduled (Clock-Aligned): If RotateAtMinutes and/or RotateAt are configured (e.g., []int{0,30} → rotate at HH:00 and HH:30; or []string{"00:00"} → rotate at midnight), a background goroutine triggers rotation at those times. These rotations use -time as the reason.
  4. Manual:
    • Logger.Rotate() forces rotation now. The backup reason will be "time" if an interval rotation is due, otherwise "size".
    • Logger.RotateWithReason("your-reason") forces rotation and tags the backup with your sanitized reason (see below). If the provided reason is empty after sanitization, it falls back to the same behavior as Rotate().

Rotated files are renamed using the pattern:

By default, rotated files are named:

<name>-<timestamp>-<reason>.log

For example:

/var/log/myapp/foo-2025-04-30T15-00-00.000-size.log
/var/log/myapp/foo-2025-04-30T22-15-42.123-time.log
/var/log/myapp/foo-2025-05-01T10-30-00.000-time.log.gz  (if compressed)

If you prefer the extension to stay attached to the live name (better shell TAB completion),

set AppendTimeAfterExt: true:

<name>.log-<timestamp>-<reason>

For example:

/var/log/myapp/foo.log-2025-04-30T15-00-00.000-size
/var/log/myapp/foo.log-2025-04-30T22-15-42.123-time
/var/log/myapp/foo.log-2025-05-01T10-30-00.000-time.gz (if compressed)

Manual rotation with a custom reason _ = logger.RotateWithReason("reload-now v2")

For example:

foo-2025-05-01T10-30-00.000-reload-now-v2.log
Compression
  • Pick the algorithm with Compression: "none" | "gzip" | "zstd".
  • Precedence: If Compression is set, it wins. If it’s empty, legacy Compress: true means gzip; else no compression.
  • Outputs use .gz or .zst suffix accordingly.
  • Compression happens after rotation in a background goroutine.
  • Deprecation: Compress is kept only for backward compatibility with old configs. It’s ignored when Compression is set. It will be removed in v2.
Cleanup

On each new log file creation, timberjack:

  • Deletes backups exceeding MaxBackups (keeps the newest rotations).
  • Deletes backups older than MaxAge days.
  • Compresses uncompressed backups if compression is enabled.
Rotation modes at a glance
Mode Configure with Trigger Anchor Background goroutine? Rotates with zero writes? Updates lastRotationTime Backup suffix Notes
Size-based MaxSize A write would exceed MaxSize N/A No No No -size Always active. A single write larger than MaxSize returns an error.
Interval-based RotationInterval > 0 On next write after now - lastRotationTime ≥ RotationInterval Duration since last rotation No No Yes (to now) -time “Every N” rotations; not aligned to the wall clock.
Scheduled minute-based RotateAtMinutes (e.g. []int{0,30}) At each HH:MM where minute matches Clock minute marks Yes Yes Yes -time Expands minutes across all 24 hours. Invalid minutes are ignored with a warning. De-duplicated vs RotateAt.
Scheduled daily fixed time RotateAt (e.g. []string{"00:00","12:00"}) At each listed HH:MM daily Clock minute marks Yes Yes Yes -time Ideal for “rotate at midnight”. De-duplicated vs RotateAtMinutes.
Manual Logger.Rotate() When called Immediate No N/A No -time if an interval rotation is due; otherwise -size Handy for SIGHUP.
Manual (custom reason) Logger.RotateWithReason(s) When called Immediate No N/A No -<sanitized reason> Falls back to Rotate() behavior if s sanitizes to empty.

Time zone: scheduling and filename timestamps use UTC by default, or local time if LocalTime: true. Sanitized reason: lowercase; [a-z0-9_-] only, trims edge, max 32.

⚠️ Rotation Notes & Warnings

  • BackupTimeFormat Values must be valid and should not change after initialization
    The BackupTimeFormat value must be valid and must follow the timestamp layout rules specified here: https://pkg.go.dev/time#pkg-constants. BackupTimeFormat supports more formats but it's recommended to use standard formats. If an invalid BackupTimeFormat is configured, Timberjack logs a warning to os.Stderr and falls back to the default format: 2006-01-02T15-04-05.000. Rotation will still work, but the resulting filenames may not match your expectations.

  • Invalid RotateAtMinutes/RotateAt Values
    Values outside the valid range (0–59) for RotateAtMinutes or invalid time (HH:MM) for RotateAt or duplicates in RotateAtMinutes/RotateAt are ignored with a warning to stderr. Rotation continues with the valid schedule.

  • Logger Must Be Closed Always call logger.Close() when done logging. This shuts down internal goroutines used for scheduled rotation and cleanup. Failing to close the logger can result in orphaned background processes, open file handles, and memory leaks.

  • Size-Based Rotation Is Always Active Regardless of RotationInterval or RotateAtMinutes/RotateAt, size-based rotation is always enforced. If a write causes the log to exceed MaxSize (default: 100MB), it triggers an immediate rotation.

  • If Only RotationInterval Is Set The logger rotates after the configured time has passed since the last rotation, regardless of file size.

  • If Only RotateAtMinutes/RotateAt Is Set The logger rotates at the clock times specified, regardless of file size or duration passed. This is handled by a background goroutine. Rotated logs can be empty if no write has occurred.

  • If Both Are Set
    Both time-based strategies (RotationInterval and RotateAtMinutes) are evaluated. Whichever condition occurs first triggers rotation. However:

    • Both update the internal lastRotationTime field.
    • This means if a rotation happens due to RotateAtMinutes/RotateAt, it resets the interval timer, potentially delaying or preventing a RotationInterval-based rotation.

    This behavior ensures you won’t get redundant rotations, but it may make RotationInterval feel unpredictable if RotateAtMinutes/RotateAt is also configured.

Contributing

We welcome contributions! Please see our contributing guidelines before submitting a pull request.

License

MIT

Documentation

Overview

Package timberjack provides a rolling logger with size-based and time-based rotation.

Timberjack is a simple, pluggable component for log rotation. It can rotate the active log file when any of the following occur:

  • the file grows beyond MaxSize (size-based)
  • the configured RotationInterval elapses (interval-based)
  • a scheduled time is reached via RotateAt or RotateAtMinutes (clock-based)
  • rotation is triggered explicitly via Rotate() (manual)

Rotated files can optionally be compressed with gzip or zstd. Cleanup is handled automatically. Old log files are removed based on MaxBackups and MaxAge.

Import:

import "github.com/DeRuina/timberjack"

Timberjack works with any logger that writes to an io.Writer, including the standard library’s log package.

Concurrency note: timberjack assumes a single process writes to the target files. Reusing the same Logger configuration across multiple processes on the same machine may lead to improper behavior.

Source code: https://github.com/DeRuina/timberjack

Example

To use timberjack with the standard library's log package, just pass it into the SetOutput function when your application starts.

log.SetOutput(&Logger{
	Filename:         "/var/log/myapp/foo.log",
	MaxSize:          500,            // megabytes
	MaxBackups:       3,              // number of backups
	MaxAge:           28,             // days
	Compress:         true,           // disabled by default
	LocalTime:        true,           // use the local timezone
	RotationInterval: time.Hour * 24, // rotate daily
})

Index

Examples

Constants

This section is empty.

Variables

View Source
var (

	// empty BackupTimeFormatField
	ErrEmptyBackupTimeFormatField = errors.New("empty backupformat field")
)

Functions

This section is empty.

Types

type Logger

type Logger struct {
	// Filename is the file to write logs to.  Backup log files will be retained
	// in the same directory.  It uses <processname>-timberjack.log in
	// os.TempDir() if empty.
	Filename string `json:"filename" yaml:"filename"`

	// MaxSize is the maximum size in megabytes of the log file before it gets
	// rotated. It defaults to 100 megabytes.
	MaxSize int `json:"maxsize" yaml:"maxsize"`

	// MaxAge is the maximum number of days to retain old log files based on the
	// timestamp encoded in their filename.  Note that a day is defined as 24
	// hours and may not exactly correspond to calendar days due to daylight
	// savings, leap seconds, etc. The default is not to remove old log files
	// based on age.
	MaxAge int `json:"maxage" yaml:"maxage"`

	// MaxBackups is the maximum number of old log files to retain.  The default
	// is to retain all old log files (though MaxAge may still cause them to get
	// deleted.) MaxBackups counts distinct rotation events (timestamps).
	MaxBackups int `json:"maxbackups" yaml:"maxbackups"`

	// LocalTime determines if the time used for formatting the timestamps in
	// backup files is the computer's local time.  The default is to use UTC
	// time.
	LocalTime bool `json:"localtime" yaml:"localtime"`

	// Deprecated: use Compression instead ("none" | "gzip" | "zstd").
	Compress bool `json:"compress,omitempty" yaml:"compress,omitempty"`

	// Compression selects the algorithm. If empty, legacy Compress is used.
	// Allowed values: "none", "gzip", "zstd". Unknown => "none" (with a warning).
	Compression string `json:"compression,omitempty" yaml:"compression,omitempty"`

	// RotationInterval is the maximum duration between log rotations.
	// If the elapsed time since the last rotation exceeds this interval,
	// the log file is rotated, even if the file size has not reached MaxSize.
	// The minimum recommended value is 1 minute. If set to 0, time-based rotation is disabled.
	//
	// Example: RotationInterval = time.Hour * 24 will rotate logs daily.
	RotationInterval time.Duration `json:"rotationinterval" yaml:"rotationinterval"`

	// BackupTimeFormat defines the layout for the timestamp appended to rotated file names.
	// While other formats are allowed, it is recommended to follow the standard Go time layout
	// (https://pkg.go.dev/time#pkg-constants). Use the ValidateBackupTimeFormat() method to check
	// if the value is valid. It is recommended to call this method before using the Logger instance.
	//
	// WARNING: This field is assumed to be constant after initialization.
	// WARNING: If invalid value is supplied then default format `2006-01-02T15-04-05.000` will be used.
	//
	// Example:
	// BackupTimeFormat = `2006-01-02-15-04-05`
	// will generate rotated backup files in the format:
	// <logfilename>-2006-01-02-15-04-05-<rotationCriterion>-timberjack.log
	// where `rotationCriterion` could be `time` or `size`.
	BackupTimeFormat string `json:"backuptimeformat" yaml:"backuptimeformat"`

	// RotateAtMinutes defines specific minutes within an hour (0-59) to trigger a rotation.
	// For example, []int{0} for top of the hour, []int{0, 30} for top and half-past the hour.
	// Rotations are aligned to the clock minute (second 0).
	// This operates in addition to RotationInterval and MaxSize.
	// If multiple rotation conditions are met, the first one encountered typically triggers.
	RotateAtMinutes []int `json:"rotateAtMinutes" yaml:"rotateAtMinutes"`

	// RotateAt defines specific time within a day to trigger a rotation.
	// For example, []string{'00:00'} for midnight, []string{'00:00', '12:00'} for
	// midnight and midday.
	// Rotations are aligned to the clock minute (second 0).
	// This operates in addition to RotationInterval and MaxSize.
	// If multiple rotation conditions are met, the first one encountered typically triggers.
	RotateAt []string `json:"rotateAt" yaml:"rotateAt"`

	// AppendTimeAfterExt controls where the timestamp/reason go.
	// false (default):  <name>-<timestamp>-<reason>.log
	// true:             <name>.log-<timestamp>-<reason>
	AppendTimeAfterExt bool `json:"appendTimeAfterExt" yaml:"appendTimeAfterExt"`

	// FileMode sets the permissions to use when creating new log files.
	// It will be inherited by rotated files.
	// If zero, the default of 0o640 is used.
	//
	// Note that a local umask may alter the final permissions.
	// Also, on non-Linux systems this might not have the desired effect.
	FileMode os.FileMode `json:"filemode" yaml:"filemode"`
	// contains filtered or unexported fields
}

Logger is an io.WriteCloser that writes to the specified filename.

Logger opens or creates the logfile on the first Write. If the file exists and is smaller than MaxSize megabytes, timberjack will open and append to that file. If the file's size exceeds MaxSize, or if the configured RotationInterval has elapsed since the last rotation, the file is closed, renamed with a timestamp, and a new logfile is created using the original filename.

Thus, the filename you give Logger is always the "current" log file.

Backups use the log file name given to Logger, in the form: `name-timestamp-<reason>.ext` where `name` is the filename without the extension, `timestamp` is the time of rotation formatted as `2006-01-02T15-04-05.000`, `reason` is "size" or "time" (Rotate/auto), or a custom tag (RotateWithReason), and `ext` is the original extension. For example, if your Logger.Filename is `/var/log/foo/server.log`, a backup created at 6:30pm on Nov 11 2016 due to size would use the filename `/var/log/foo/server-2016-11-04T18-30-00.000-size.log`.

Cleaning Up Old Log Files

Whenever a new logfile is created, old log files may be deleted based on MaxBackups and MaxAge. The most recent files (according to the timestamp) will be retained up to MaxBackups (or all files if MaxBackups is 0). Any files with a timestamp older than MaxAge days are deleted, regardless of MaxBackups. Note that the timestamp is the rotation time, not necessarily the last write time.

If MaxBackups and MaxAge are both 0, no old log files will be deleted.

timberjack assumes only a single process is writing to the log files at a time.

func (*Logger) Close

func (l *Logger) Close() error

Close implements io.Closer, and closes the current logfile. It also signals any running goroutines (like scheduled rotation or mill) to stop.

func (*Logger) Rotate

func (l *Logger) Rotate() error

Rotate forces an immediate rotation using the legacy auto-reason logic. (empty reason => "time" if an interval rotation is due, otherwise "size")

Example

Example of how to rotate in response to SIGHUP.

l := &Logger{}
log.SetOutput(l)
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP)

go func() {
	for {
		<-c
		l.Rotate()
	}
}()

func (*Logger) RotateWithReason added in v1.3.7

func (l *Logger) RotateWithReason(reason string) error

RotateWithReason forces a rotation immediately and tags the backup filename with the provided reason (after sanitization). If the sanitized reason is empty, it falls back to the default behavior used by Rotate(): "time" if an interval rotation is due, otherwise "size".

NOTE: Like Rotate(), this does not modify lastRotationTime. If an interval rotation is already due, a subsequent write may still trigger another interval-based rotation.

func (*Logger) ValidateBackupTimeFormat added in v1.3.1

func (l *Logger) ValidateBackupTimeFormat() error

ValidateBackupTimeFormat checks if the configured BackupTimeFormat is a valid time layout. While other formats are allowed, it is recommended to follow the standard time layout rules as defined here: https://pkg.go.dev/time#pkg-constants

WARNING: Assumes that BackupTimeFormat value remains constant after initialization.

func (*Logger) Write

func (l *Logger) Write(p []byte) (n int, err error)

Write implements io.Writer. It writes the provided bytes to the current log file. If the log file exceeds MaxSize after writing, or if the configured RotationInterval has elapsed since the last rotation, or if a scheduled rotation time (RotateAtMinutes) has been reached, the file is closed, renamed to include a timestamp, and a new log file is created using the original filename. If the size of a single write exceeds MaxSize, the write is rejected and an error is returned.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL