Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add RotationInterval & FileNameFormat support #215

Open
wants to merge 1 commit into
base: v2.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 53 additions & 4 deletions lumberjack.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ const (
defaultMaxSize = 100
)

const (
placeholderFilename = "%FILENAME%"
placeholderExtension = "%EXT%"
)

// ensure we always implement io.WriteCloser
var _ io.WriteCloser = (*Logger)(nil)

Expand Down Expand Up @@ -107,12 +112,25 @@ type Logger struct {
// using gzip. The default is not to perform compression.
Compress bool `json:"compress" yaml:"compress"`

// RotationInterval is the time duration after which the log file should be rotated.
// The default is set to 0, and the rotation will not be based on time.
RotationInterval time.Duration `json:"rotationtime" yaml:"rotationtime"`

// FileNameFormat allows customization of the filename format.
// Use placeholders like %FILENAME%, %EXT%, 2006, 01, 02, 15, 04, 05, 000.
//
// Example: "%FILENAME%.2006-01-02-15-04%EXT%"
FileNameFormat string `json:"filenameformat" yaml:"filenameformat"`

size int64
file *os.File
mu sync.Mutex

millCh chan bool
startMill sync.Once

// nextRotateTime is the next scheduled time for rotation based RotationInterval.
nextRotateTime time.Time
}

var (
Expand Down Expand Up @@ -149,7 +167,7 @@ func (l *Logger) Write(p []byte) (n int, err error) {
}
}

if l.size+writeLen > l.max() {
if l.shouldRotate(writeLen) {
if err := l.rotate(); err != nil {
return 0, err
}
Expand Down Expand Up @@ -200,9 +218,33 @@ func (l *Logger) rotate() error {
return err
}
l.mill()
l.updateNextRotateTime()
return nil
}

// shouldRotate checks if rotation is needed based on time.
func (l *Logger) shouldRotate(writeLen int64) bool {
if l.size+writeLen > l.max() {
return true
}
if l.RotationInterval > 0 {
if l.nextRotateTime.IsZero() {
l.updateNextRotateTime()
}
if currentTime().After(l.nextRotateTime) {
return true
}
}
return false
}

// updateNextRotateTime sets or updates the nextRotateTime based on the current time and RotationInterval, if RotationInterval is enabled and non-zero.
func (l *Logger) updateNextRotateTime() {
if l.RotationInterval > 0 {
l.nextRotateTime = currentTime().Truncate(l.RotationInterval).Add(l.RotationInterval)
}
}

// openNew opens a new log file for writing, moving any old log file out of the
// way. This methods assumes the file has already been closed.
func (l *Logger) openNew() error {
Expand All @@ -218,7 +260,7 @@ func (l *Logger) openNew() error {
// Copy the mode off the old logfile.
mode = info.Mode()
// move the existing file
newname := backupName(name, l.LocalTime)
newname := l.backupName(name, l.LocalTime)
if err := os.Rename(name, newname); err != nil {
return fmt.Errorf("can't rename log file: %s", err)
}
Expand All @@ -244,7 +286,7 @@ func (l *Logger) openNew() error {
// backupName creates a new filename from the given name, inserting a timestamp
// between the filename and the extension, using the local time if requested
// (otherwise UTC).
func backupName(name string, local bool) string {
func (l *Logger) backupName(name string, local bool) string {
dir := filepath.Dir(name)
filename := filepath.Base(name)
ext := filepath.Ext(filename)
Expand All @@ -255,7 +297,14 @@ func backupName(name string, local bool) string {
}

timestamp := t.Format(backupTimeFormat)
return filepath.Join(dir, fmt.Sprintf("%s-%s%s", prefix, timestamp, ext))
fileFormat := fmt.Sprintf("%s-%s%s", prefix, timestamp, ext)

if l.FileNameFormat != "" {
formattedName := t.Format(l.FileNameFormat)
fileFormat = strings.ReplaceAll(formattedName, placeholderFilename, prefix)
fileFormat = strings.ReplaceAll(fileFormat, placeholderExtension, ext)
}
return filepath.Join(dir, fileFormat)
}

// openExistingOrNew opens the logfile if it exists and if the current write
Expand Down
75 changes: 75 additions & 0 deletions lumberjack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,81 @@ func TestJson(t *testing.T) {
equals(true, l.Compress, t)
}

func TestRotationInterval(t *testing.T) {
currentTime = fakeTime
dir := makeTempDir("TestRotationInterval", t)
defer os.RemoveAll(dir)

filename := logFile(dir)
l := &Logger{
Filename: filename,
MaxSize: 10,
RotationInterval: time.Minute,
}
defer l.Close()

b := []byte("foo")
n, err := l.Write(b)
isNil(err, t)
equals(len(b), n, t)
existsWithContent(filename, b, t)
fileCount(dir, 1, t)

// Advance time by 1 minute and 1 second
fakeCurrentTime = fakeCurrentTime.Add(time.Minute + time.Second)

// Write new content, which should trigger rotation
// since the rotation interval has passed
b2 := []byte("bar-min!")
n, err = l.Write(b2)
isNil(err, t)
equals(len(b2), n, t)

existsWithContent(filename, b2, t)
existsWithContent(backupFile(dir), b, t)
fileCount(dir, 2, t)
}

func TestCustomFilenameFormat(t *testing.T) {
currentTime = fakeTime
dir := makeTempDir("TestCustomFilenameFormat", t)
defer os.RemoveAll(dir)

customDateFormat := "2006-01-02"
customFormat := "%FILENAME%%EXT%." + customDateFormat
filename := logFile(dir)
l := &Logger{
Filename: filename,
MaxSize: 10,
FileNameFormat: customFormat,
LocalTime: true,
}
defer l.Close()

b := []byte("bar")
n, err := l.Write(b)
isNil(err, t)
equals(len(b), n, t)
existsWithContent(filename, b, t)
fileCount(dir, 1, t)

// Advance time to trigger rotation.
newFakeTime()
l.rotate()

b2 := []byte("foo")
n, err = l.Write(b2)
isNil(err, t)
equals(len(b2), n, t)

// Verify rotation happened with custom format
expectedBackupFilename := filepath.Join(dir, fmt.Sprintf("foobar.log.%s", currentTime().Format(customDateFormat)))
existsWithContent(expectedBackupFilename, b, t)
existsWithContent(filename, b2, t)
fileCount(dir, 2, t)
}


// makeTempDir creates a file with a semi-unique name in the OS temp directory.
// It should be based on the name of the test, to keep parallel tests from
// colliding, and must be cleaned up after the test is finished.
Expand Down