From 5aff0e0bbff1e54aac06d37b0d58245eb6c208b1 Mon Sep 17 00:00:00 2001 From: Nate Finch Date: Mon, 16 Jun 2014 06:48:32 -0400 Subject: [PATCH] add locks and the Rotate() function. Remove IsWriteTooLong function. Fix a bug in the link to time constants. --- README.md | 48 +++++++++++++------------- lumberjack.go | 86 ++++++++++++++++++++++++++-------------------- lumberjack_test.go | 78 ++++++++++++++++++++++++++++++++++------- 3 files changed, 137 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 103c9e5..6c1a59e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://travis-ci.org/natefinch/lumberjack.png)](https://travis-ci.org/natefinch/lumberjack) -Lumberjack is a Go package for writing logs to rolling files. +### Lumberjack is a Go package for writing logs to rolling files. Lumberjack is intended to be one part of a logging infrastructure. It is not an all-in-one solution, but instead is a pluggable @@ -25,11 +25,7 @@ into the SetOutput function when your application starts: MaxAge: lumberjack.Week * 4, )) -Note that lumberjack assumes whatever is writing to it will use locks to prevent -concurrent writes (the standard library log package already does this). -Lumberjack does not implement its own lock. - -Lumberjack also assumes that only one process is writing to the output files. +Lumberjack assumes that only one process is writing to the output files. Using the same lumberjack configuration from multiple processes on the same machine will result in improper behavior. @@ -39,63 +35,53 @@ machine will result in improper behavior. ## Constants ``` go const ( - // Some helper constants to make your declarations easier to read. Megabyte = 1024 * 1024 Gigabyte = 1024 * Megabyte - // note that lumberjack days and weeks may not exactly conform to calendar - // days and weeks due to daylight savings, leap seconds, etc. Day = 24 * time.Hour Week = 7 * Day ) ``` -## func IsWriteTooLong -``` go -func IsWriteTooLong(err error) bool -``` -IsWriteTooLong reports whether the given error indicates a Write with data that -exceeds the Logger's MaxSize. - - ## type Logger ``` go type Logger struct { // Dir determines the directory in which to store log files. // It defaults to os.TempDir() if empty. - Dir string + Dir string `json:"dir" yaml:"dir"` // NameFormat is the time formatting layout used to generate filenames. // It defaults to "2006-01-02T15-04-05.000.log". - NameFormat string + NameFormat string `json:"nameformat" yaml:"nameformat"` // MaxSize is the maximum size in bytes of the log file before it gets // rolled. It defaults to 100 megabytes. - MaxSize int64 + MaxSize int64 `json:"maxsize" yaml:"maxsize"` // MaxAge is the maximum time to retain old log files based on // FileInfo.ModTime. The default is not to remove old log files based on // age. - MaxAge time.Duration + MaxAge time.Duration `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 int + MaxBackups int `json:"maxbackups" yaml:"maxbackups"` // LocalTime determines if the time used for formatting the filename is the // computer's local time. The default is to use UTC time. - LocalTime bool + LocalTime bool `json:"localtime" yaml:"localtime"` + + sync.Mutex // contains filtered or unexported fields } ``` Logger is an io.WriteCloser that writes to a log file in the given directory with the given NameFormat. NameFormat should include a time formatting layout in it that produces a valid unique filename for the OS. For more -about time formatting layouts, read a href="http://golang.org/pkg/time/#pkg-">http://golang.org/pkg/time/#pkg- -constants. +about time formatting layouts, read a href="http://golang.org/pkg/time/#pkg-constants">http://golang.org/pkg/time/#pkg-constants. The date encoded in the filename by NameFormat is used to determine which log files are most recent in several situations. @@ -137,6 +123,18 @@ Close implements io.Closer, and closes the current logfile. +### func (\*Logger) Rotate +``` go +func (l *Logger) Rotate() error +``` +Rotate causes Logger to close the existing log file and immediately create a +new one. This is a helper function for applications that want to initiate +rotations outside of the normal rotation rules, such as in response to +SIGHUP. After rotating, this initiates a cleanup of old log files according +to the normal rules. + + + ### func (\*Logger) Write ``` go func (l *Logger) Write(p []byte) (n int, err error) diff --git a/lumberjack.go b/lumberjack.go index 1d9c040..f469c60 100644 --- a/lumberjack.go +++ b/lumberjack.go @@ -19,10 +19,7 @@ // MaxAge: lumberjack.Week * 4, // )) // -// Note that lumberjack assumes whatever is writing to it will use locks to -// prevent concurrent writes. Lumberjack does not implement its own lock. -// -// Lumberjack also assumes that only one process is writing to the output files. +// Lumberjack assumes that only one process is writing to the output files. // Using the same lumberjack configuration from multiple processes on the same // machine will result in improper behavior. package lumberjack @@ -34,6 +31,7 @@ import ( "os" "path/filepath" "sort" + "sync" "time" ) @@ -59,8 +57,7 @@ var _ io.WriteCloser = &Logger{} // Logger is an io.WriteCloser that writes to a log file in the given directory // with the given NameFormat. NameFormat should include a time formatting // layout in it that produces a valid unique filename for the OS. For more -// about time formatting layouts, read http://golang.org/pkg/time/#pkg- -// constants. +// about time formatting layouts, read http://golang.org/pkg/time/#pkg-constants. // // The date encoded in the filename by NameFormat is used to determine which log // files are most recent in several situations. @@ -87,32 +84,33 @@ var _ io.WriteCloser = &Logger{} type Logger struct { // Dir determines the directory in which to store log files. // It defaults to os.TempDir() if empty. - Dir string + Dir string `json:"dir" yaml:"dir"` // NameFormat is the time formatting layout used to generate filenames. // It defaults to "2006-01-02T15-04-05.000.log". - NameFormat string + NameFormat string `json:"nameformat" yaml:"nameformat"` // MaxSize is the maximum size in bytes of the log file before it gets // rolled. It defaults to 100 megabytes. - MaxSize int64 + MaxSize int64 `json:"maxsize" yaml:"maxsize"` // MaxAge is the maximum time to retain old log files based on // FileInfo.ModTime. The default is not to remove old log files based on // age. - MaxAge time.Duration + MaxAge time.Duration `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 int + MaxBackups int `json:"maxbackups" yaml:"maxbackups"` // LocalTime determines if the time used for formatting the filename is the // computer's local time. The default is to use UTC time. - LocalTime bool + LocalTime bool `json:"localtime" yaml:"localtime"` size int64 file *os.File + sync.Mutex } // currentTime is only used for testing. Normally it's the time.Now() function. @@ -123,11 +121,13 @@ var currentTime = time.Now // PathFormat. If the length of the write is greater than MaxSize, an error is // returned that satisfies IsWriteTooLong. func (l *Logger) Write(p []byte) (n int, err error) { + l.Lock() + defer l.Unlock() writeLen := int64(len(p)) if writeLen > l.max() { - return 0, writeTooLongError{fmt.Errorf( + return 0, fmt.Errorf( "write length %d exceeds maximum file size %d", writeLen, l.max(), - )} + ) } f := l.file rotate := l.size+writeLen > l.max() @@ -158,6 +158,40 @@ func (l *Logger) Write(p []byte) (n int, err error) { return n, err } +// Close implements io.Closer, and closes the current logfile. +func (l *Logger) Close() error { + l.Lock() + defer l.Unlock() + if l.file != nil { + err := l.file.Close() + l.file = nil + return err + } + return nil +} + +// Rotate causes Logger to close the existing log file and immediately create a +// new one. This is a helper function for applications that want to initiate +// rotations outside of the normal rotation rules, such as in response to +// SIGHUP. After rotating, this initiates a cleanup of old log files according +// to the normal rules. +func (l *Logger) Rotate() error { + l.Lock() + defer l.Unlock() + if l.file != nil { + if err := l.file.Close(); err != nil { + return err + } + l.file = nil + } + var err error + l.file, err = l.openNew() + if err != nil { + return err + } + return l.cleanup() +} + // openNew opens a new log file for writing. func (l *Logger) openNew() (*os.File, error) { err := os.MkdirAll(l.dir(), 0744) @@ -306,16 +340,6 @@ func (l *Logger) isLogFile(f os.FileInfo) bool { return err == nil } -// Close implements io.Closer, and closes the current logfile. -func (l *Logger) Close() error { - if l.file != nil { - err := l.file.Close() - l.file = nil - return err - } - return nil -} - func (l *Logger) max() int64 { if l.MaxSize == 0 { return defaultMaxSize @@ -362,17 +386,3 @@ func (b byFormatTime) time(i int) time.Time { } return t } - -// IsWriteTooLong reports whether the given error indicates a Write with data -// that exceeds the Logger's MaxSize. -func IsWriteTooLong(err error) bool { - if err == nil { - return false - } - _, ok := err.(writeTooLongError) - return ok -} - -type writeTooLongError struct { - error -} diff --git a/lumberjack_test.go b/lumberjack_test.go index 900d0cc..96b1260 100644 --- a/lumberjack_test.go +++ b/lumberjack_test.go @@ -1,7 +1,7 @@ package lumberjack import ( - "errors" + "fmt" "io/ioutil" "os" "path/filepath" @@ -85,18 +85,12 @@ func TestWriteTooLong(t *testing.T) { defer l.Close() b := []byte("booooooooooooooo!") n, err := l.Write(b) - assert(IsWriteTooLong(err), t, - "Should have gotten write too long error, instead got %s (%T)", err, err) + notNil(err, t) equals(0, n, t) + equals(err.Error(), + fmt.Sprintf("write length %d exceeds maximum file size %d", len(b), l.MaxSize), t) _, err = os.Stat(logFile(dir)) assert(os.IsNotExist(err), t, "File exists, but should not have been created") - - newerr := errors.New("foo") - - assert(!IsWriteTooLong(nil), t, - "Nil error should not return true for IsWriteTooLong, but did.") - assert(!IsWriteTooLong(newerr), t, - "Different error should not return true for IsWriteTooLong, but did.") } func TestMakeLogDir(t *testing.T) { @@ -151,9 +145,9 @@ func TestDefaultFilename(t *testing.T) { fileCount(dir, 1, t) } -func TestRotate(t *testing.T) { +func TestAutoRotate(t *testing.T) { currentTime = fakeTime - dir := makeTempDir("TestRotate", t) + dir := makeTempDir("TestAutoRotate", t) defer os.RemoveAll(dir) l := &Logger{ @@ -405,6 +399,66 @@ func TestDefaultDirAndName(t *testing.T) { existsWithLen(f2, n, t) } +func TestRotate(t *testing.T) { + currentTime = fakeTime + dir := makeTempDir("TestRotate", t) + defer os.RemoveAll(dir) + + l := &Logger{ + Dir: dir, + NameFormat: format, + MaxBackups: 1, + MaxSize: Megabyte, + } + defer l.Close() + b := []byte("boo!") + n, err := l.Write(b) + isNil(err, t) + equals(len(b), n, t) + + filename := logFile(dir) + existsWithLen(filename, n, t) + fileCount(dir, 1, t) + + // set the current time one day later + defer newFakeTime(Day)() + + err = l.Rotate() + isNil(err, t) + + // we need to wait a little bit since the files get deleted on a different + // goroutine. + <-time.After(10 * time.Millisecond) + + filename2 := logFile(dir) + existsWithLen(filename2, 0, t) + existsWithLen(filename, n, t) + fileCount(dir, 2, t) + + // set the current time one day later + defer newFakeTime(Day)() + + err = l.Rotate() + isNil(err, t) + + // we need to wait a little bit since the files get deleted on a different + // goroutine. + <-time.After(10 * time.Millisecond) + + filename3 := logFile(dir) + existsWithLen(filename3, 0, t) + existsWithLen(filename2, 0, t) + fileCount(dir, 2, t) + + b2 := []byte("foooooo!") + n, err = l.Write(b2) + isNil(err, t) + equals(len(b2), n, t) + + // this will use the new fake time + existsWithLen(filename3, n, 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.