Go-Rotate-Logs is a cross-platform helper for rolling your logfile, cleaning up old log files, and backing up your log files when they exceed your desired size. It works with any logging library that implements io.Writer.
- 🚀 High Performance: 54x faster than V1, zero allocations
- 🌍 Cross-Platform: Works on Linux, macOS (Intel & ARM), and Windows
- 🔄 Automatic Rotation: Rotate logs by size
- 🕒 Time-Based Naming: Add timestamps to log filenames
- 🧹 Auto Cleanup: Remove old log files automatically
- 📦 Sequential Backups: Numbered backup files (backup-0, backup-1, etc.)
- 🔒 Thread-Safe: Safe for concurrent writes
- 📝 io.Writer Compatible: Works with any Go logging library
✅ Fully tested on:
- Linux (AMD64, ARM64)
- macOS (Intel, Apple Silicon M1/M2/M3)
- Windows (AMD64)
✅ Automatic filename sanitization for Windows compatibility
✅ Performance improvements applied:
- 54x faster than original implementation
- Zero memory allocations per write
- Keeps file open between writes for maximum performance
✅ Critical bug fixes:
- Fixed file path bug in cleanup function
- Added
Close()method to prevent file handle leaks - Comprehensive test suite with 79.8% coverage
- Cross-platform compatibility verified (Linux, macOS, Windows)
New in this version: Always call Close() when you're done with the logger to prevent file handle leaks:
logFile := &rotate.RotateLogsWriter{
Config: rotate.Config{
Directory: "logs",
Filename: "app.log",
MaxSize: 10,
},
}
defer logFile.Close() // IMPORTANT: Always close!go get github.com/sutantodadang/go-rotate-logsimport (
"log"
rotate "github.com/sutantodadang/go-rotate-logs"
)
logFile := &rotate.RotateLogsWriter{
Config: rotate.Config{
Directory: "logs",
Filename: "app.log",
MaxSize: 10, // 10MB
},
}
defer logFile.Close() // IMPORTANT: Always close!
log.SetOutput(logFile)
log.Println("Hello, rotating logs!")All configuration options for RotateLogsWriter:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
Directory |
string | ✅ Yes | - | Directory where log files will be stored |
Filename |
string | ✅ Yes | - | Log filename (must end with .log) |
MaxSize |
int | ✅ Yes | - | Maximum file size in MB before rotation |
BackupName |
string | ⬜ No | "backup" |
Prefix for backup files (e.g., app-backup-0.log) |
UsingTime |
bool | ⬜ No | false |
Add timestamp to filename |
FormatTime |
string | ⬜ No | time.RFC3339 |
Time format for filename (requires UsingTime: true) |
CleanOldFiles |
bool | ⬜ No | false |
Enable automatic cleanup of old files |
MaxAge |
int | ⬜ No | 7 |
Days to keep old files (requires CleanOldFiles: true) |
FormatTime: "2006-01-02" // Date: 2025-11-03
FormatTime: "2006-01-02-15" // Date + Hour: 2025-11-03-14
FormatTime: time.RFC3339 // Full timestamp: 2025-11-03T14-30-00+07-00 (sanitized)
FormatTime: "200601021504" // Compact: 202511031430Note: Invalid filename characters (:, /, \) are automatically replaced with - for Windows compatibility.
import (
"log"
rotate "github.com/sutantodadang/go-rotate-logs"
)
logFile := &rotate.RotateLogsWriter{
Config: rotate.Config{
Directory: "logs",
Filename: "app.log",
MaxSize: 10, // 10MB
},
}
defer logFile.Close()
log.SetOutput(logFile)
log.Println("Application started")logFile := &rotate.RotateLogsWriter{
Config: rotate.Config{
Directory: "logs",
Filename: "app.log",
MaxSize: 10,
UsingTime: true,
FormatTime: "2006-01-02", // Creates: app-2025-11-03.log
},
}
defer logFile.Close()
log.SetOutput(logFile)logFile := &rotate.RotateLogsWriter{
Config: rotate.Config{
Directory: "logs",
Filename: "app.log",
MaxSize: 10,
CleanOldFiles: true,
MaxAge: 30, // Keep logs for 30 days
},
}
defer logFile.Close()
log.SetOutput(logFile)import (
"io"
"log"
"os"
rotate "github.com/sutantodadang/go-rotate-logs"
)
logFile := &rotate.RotateLogsWriter{
Config: rotate.Config{
Directory: "logs",
Filename: "app.log",
MaxSize: 10,
},
}
defer logFile.Close()
// Write to both file and stdout
mw := io.MultiWriter(logFile, os.Stdout)
log.SetOutput(mw)
log.Println("Logged to file and console!")import (
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
rotate "github.com/sutantodadang/go-rotate-logs"
)
logFile := &rotate.RotateLogsWriter{
Config: rotate.Config{
Directory: "logs",
Filename: "app.log",
MaxSize: 10,
UsingTime: true,
FormatTime: "2006-01-02",
CleanOldFiles: true,
MaxAge: 30,
},
}
defer logFile.Close()
mw := zerolog.MultiWriter(logFile, os.Stdout)
log.Logger = zerolog.New(mw).With().Timestamp().Caller().Logger()
log.Info().Msg("Application started")logFile := &rotate.RotateLogsWriter{
Config: rotate.Config{
Directory: "logs",
Filename: "concurrent.log",
MaxSize: 20,
},
}
defer logFile.Close()
logger := log.New(logFile, "", log.LstdFlags)
// Safe to use from multiple goroutines
for i := 0; i < 10; i++ {
go func(id int) {
logger.Printf("Goroutine %d is writing\n", id)
}(i)
}See examples/main.go for more complete examples.
The package includes comprehensive tests with 79.8% code coverage:
# Run all tests
go test -v
# Run with race detection
go test -race
# Run with coverage
go test -cover
# Run cross-platform tests
go test -v -run TestCrossPlatform
# Run benchmarks
go test -bench=Benchmark -benchmem -run=^$The current implementation is highly optimized:
- Fast: Keeps file open between writes (no repeated open/close)
- Efficient: Zero memory allocations per write operation
- Thread-Safe: Mutex-protected for concurrent access
- Cross-Platform: Works identically on Linux, macOS, and Windows
Tested on AMD Ryzen 5 7500F (6-Core), Windows 11:
BenchmarkRotateLogsWriter_Write-12 609350 1928 ns/op 0 B/op 0 allocs/op
BenchmarkRotateLogsWriter_LargeWrite-12 463408 2540 ns/op 0 B/op 0 allocs/op
- Small writes (22 bytes): ~1,928 ns/op, zero allocations
- Large writes (1 KB): ~2,540 ns/op, zero allocations
- 54x faster than the original implementation!
- Write Detection: Monitors total bytes written to current file
- Size Check: Before each write, checks if size would exceed
MaxSize - Rotation: When threshold exceeded:
- Current file renamed to
{filename}-backup-{N}.log(sequential numbering) - New file created with original name
- Counter increments for next rotation
- Current file renamed to
- Cleanup (optional): Removes files older than
MaxAgedays - Thread-Safe: All operations protected by mutex for concurrent access
Without time:
app.log # Current log
app-backup-0.log # First backup
app-backup-1.log # Second backup
app-backup-2.log # Third backup
With time (FormatTime: "2006-01-02"):
app-2025-11-03.log # Current log
app-2025-11-03-backup-0.log # First backup
app-2025-11-03-backup-1.log # Second backup
With time (FormatTime: time.RFC3339):
app-2025-11-03T14-30-00+07-00.log # Current log (sanitized)
app-2025-11-03T14-30-00+07-00-backup-0.log # First backup
- Always call
Close(): Usedefer logFile.Close()to prevent file handle leaks - Choose appropriate MaxSize: Balance between rotation frequency and file size
- Use time-based names for daily logs:
FormatTime: "2006-01-02" - Enable cleanup for long-running apps: Set
CleanOldFiles: trueand appropriateMaxAge - Use date format for readability: Prefer
"2006-01-02"overtime.RFC3339for simpler filenames
Q: Do I need to manually rotate logs?
A: No, rotation happens automatically when file size exceeds MaxSize.
Q: Can I use this with any logging library?
A: Yes! It implements io.Writer, so it works with standard library log, zerolog, logrus, zap, etc.
Q: Is it safe for concurrent use?
A: Yes, all operations are protected by a mutex.
Q: What happens to old backups?
A: They're numbered sequentially (0, 1, 2...). If CleanOldFiles: true, files older than MaxAge days are automatically deleted.
Q: Does it work on Windows?
A: Yes! Filenames are automatically sanitized for Windows compatibility (:, /, \ → -).
Q: How do I check for cleanup errors?
A: Use GetCleanupError() method to check if cleanup encountered any errors.
Contributions are welcome! The package includes:
- ✅ 16 comprehensive tests with 79.8% code coverage
- ✅ Cross-platform compatibility tests
- ✅ GitHub Actions CI for automated testing
- ✅ Race condition detection