diff --git a/README.md b/README.md index d5439d4..82a0a76 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,30 @@ object. Currently only supported event type is FiledRotated ) ``` +## ForceNewFile + +Ensure a new file is created every time New() is called. If the base file name +already exists, an implicit rotation is performed. + +```go + rotatelogs.New( + "/var/log/myapp/log.%Y%m%d", + rotatelogs.ForceNewFile(), + ) +``` + +## ForceNewFile + +Ensure a new file is created every time New() is called. If the base file name +already exists, an implicit rotation is performed. + +```go + rotatelogs.New( + "/var/log/myapp/log.%Y%m%d", + rotatelogs.ForceNewFile(), + ) +``` + # Rotating files forcefully If you want to rotate files forcefully before the actual rotation time has reached, diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..02fb5f6 --- /dev/null +++ b/example_test.go @@ -0,0 +1,56 @@ +package rotatelogs_test + +import ( + "fmt" + "io/ioutil" + "os" + rotatelogs "github.com/lestrrat-go/file-rotatelogs" +) + +func ExampleForceNewFile () { + logDir, err := ioutil.TempDir("", "rotatelogs_test") + if err != nil { + fmt.Println("could not create log directory ", err) + return + } + logPath := fmt.Sprintf("%s/test.log", logDir) + + for i := 0; i < 2; i++ { + writer, err := rotatelogs.New(logPath, + rotatelogs.ForceNewFile(), + ) + if err != nil { + fmt.Println("Could not open log file ", err) + return + } + + n, err := writer.Write([]byte("test")) + if err != nil || n != 4 { + fmt.Println("Write failed ", err, " number written ", n) + return + } + err = writer.Close() + if err != nil { + fmt.Println("Close failed ", err) + return + } + } + + files, err := ioutil.ReadDir(logDir) + if err != nil { + fmt.Println("ReadDir failed ", err) + return + } + for _, file := range files { + fmt.Println(file.Name(), file.Size()) + } + + err = os.RemoveAll(logDir) + if err != nil { + fmt.Println("RemoveAll failed ", err) + return + } + // OUTPUT: + // test.log 4 + // test.log.1 4 +} diff --git a/interface.go b/interface.go index c025ad4..fcd0f58 100644 --- a/interface.go +++ b/interface.go @@ -46,6 +46,7 @@ type RotateLogs struct { pattern *strftime.Strftime rotationTime time.Duration rotationCount uint + forceNewFile bool } // Clock is the interface used by the RotateLogs diff --git a/options.go b/options.go index b7e0569..49cc342 100644 --- a/options.go +++ b/options.go @@ -13,6 +13,7 @@ const ( optkeyMaxAge = "max-age" optkeyRotationTime = "rotation-time" optkeyRotationCount = "rotation-count" + optkeyForceNewFile = "force-new-file" ) // WithClock creates a new Option that sets a clock @@ -72,3 +73,10 @@ func WithRotationCount(n uint) Option { func WithHandler(h Handler) Option { return option.New(optkeyHandler, h) } + +// ForceNewFile ensures a new file is created every time New() +// is called. If the base file name already exists, an implicit +// rotation is performed +func ForceNewFile() Option { + return option.New(optkeyForceNewFile, true) +} diff --git a/rotatelogs.go b/rotatelogs.go index e67f69a..3059474 100644 --- a/rotatelogs.go +++ b/rotatelogs.go @@ -41,6 +41,7 @@ func New(p string, options ...Option) (*RotateLogs, error) { var linkName string var maxAge time.Duration var handler Handler + var forceNewFile bool for _, o := range options { switch o.Name() { @@ -62,6 +63,8 @@ func New(p string, options ...Option) (*RotateLogs, error) { rotationCount = o.Value().(uint) case optkeyHandler: handler = o.Value().(Handler) + case optkeyForceNewFile: + forceNewFile = true } } @@ -83,6 +86,7 @@ func New(p string, options ...Option) (*RotateLogs, error) { pattern: pattern, rotationTime: rotationTime, rotationCount: rotationCount, + forceNewFile: forceNewFile, }, nil } @@ -135,24 +139,38 @@ func (rl *RotateLogs) getWriter_nolock(bailOnRotateFail, useGenerationalNames bo // to log to, which may be newer than rl.currentFilename baseFn := rl.genFilename() filename := baseFn + var forceNewFile bool if baseFn != rl.curBaseFn { generation = 0 + // even though this is the first write after calling New(), + // check if a new file needs to be created + if rl.forceNewFile { + forceNewFile = true + } } else { if !useGenerationalNames { // nothing to do return rl.outFh, nil } - // This is used when we *REALLY* want to rotate a log. - // instead of just using the regular strftime pattern, we - // create a new file name using generational names such as - // "foo.1", "foo.2", "foo.3", etc + forceNewFile = true + generation++ + } + if forceNewFile { + // A new file has been requested. Instead of just using the + // regular strftime pattern, we create a new file name using + // generational names such as "foo.1", "foo.2", "foo.3", etc + var name string for { - generation++ - name := fmt.Sprintf("%s.%d", filename, generation) + if generation == 0 { + name = filename + } else { + name = fmt.Sprintf("%s.%d", filename, generation) + } if _, err := os.Stat(name); err != nil { filename = name break } + generation++ } } // make sure the dir is existed, eg: diff --git a/rotatelogs_test.go b/rotatelogs_test.go index 7890f63..181b226 100644 --- a/rotatelogs_test.go +++ b/rotatelogs_test.go @@ -428,3 +428,104 @@ func TestGHIssue23(t *testing.T) { } } } + +func TestForceNewFile(t *testing.T) { + dir, err := ioutil.TempDir("", "file-rotatelogs-force-new-file") + if !assert.NoError(t, err, `creating temporary directory should succeed`) { + return + } + defer os.RemoveAll(dir) + + t.Run("Force a new file", func(t *testing.T) { + + rl, err := rotatelogs.New( + filepath.Join(dir, "force-new-file.log"), + rotatelogs.ForceNewFile(), + ) + if !assert.NoError(t, err, "rotatelogs.New should succeed") { + return + } + rl.Write([]byte("Hello, World!")) + rl.Close() + + for i := 0; i < 10; i++ { + baseFn := filepath.Join(dir, "force-new-file.log") + rl, err := rotatelogs.New( + baseFn, + rotatelogs.ForceNewFile(), + ) + if !assert.NoError(t, err, "rotatelogs.New should succeed") { + return + } + rl.Write([]byte("Hello, World")) + rl.Write([]byte(fmt.Sprintf("%d", i))) + rl.Close() + + fn := filepath.Base(rl.CurrentFileName()) + suffix := strings.TrimPrefix(fn, "force-new-file.log") + expectedSuffix := fmt.Sprintf(".%d", i+1) + if !assert.True(t, suffix == expectedSuffix, "expected suffix %s found %s", expectedSuffix, suffix) { + return + } + assert.FileExists(t, rl.CurrentFileName(), "file does not exist %s", rl.CurrentFileName()) + content, err := ioutil.ReadFile(rl.CurrentFileName()) + if !assert.NoError(t, err, "ioutil.ReadFile %s should succeed", rl.CurrentFileName()) { + return + } + str := fmt.Sprintf("Hello, World%d", i) + if !assert.Equal(t, str, string(content), "read %s from file %s, not expected %s", string(content), rl.CurrentFileName(), str) { + return + } + + assert.FileExists(t, baseFn, "file does not exist %s", baseFn) + content, err = ioutil.ReadFile(baseFn) + if !assert.NoError(t, err, "ioutil.ReadFile should succeed") { + return + } + if !assert.Equal(t, "Hello, World!", string(content), "read %s from file %s, not expected Hello, World!", string(content), baseFn) { + return + } + } + + }) + + t.Run("Force a new file with Rotate", func(t *testing.T) { + + baseFn := filepath.Join(dir, "force-new-file-rotate.log") + rl, err := rotatelogs.New( + baseFn, + rotatelogs.ForceNewFile(), + ) + if !assert.NoError(t, err, "rotatelogs.New should succeed") { + return + } + rl.Write([]byte("Hello, World!")) + + for i := 0; i < 10; i++ { + if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") { + return + } + rl.Write([]byte("Hello, World")) + rl.Write([]byte(fmt.Sprintf("%d", i))) + assert.FileExists(t, rl.CurrentFileName(), "file does not exist %s", rl.CurrentFileName()) + content, err := ioutil.ReadFile(rl.CurrentFileName()) + if !assert.NoError(t, err, "ioutil.ReadFile %s should succeed", rl.CurrentFileName()) { + return + } + str := fmt.Sprintf("Hello, World%d", i) + if !assert.Equal(t, str, string(content), "read %s from file %s, not expected %s", string(content), rl.CurrentFileName(), str) { + return + } + + assert.FileExists(t, baseFn, "file does not exist %s", baseFn) + content, err = ioutil.ReadFile(baseFn) + if !assert.NoError(t, err, "ioutil.ReadFile should succeed") { + return + } + if !assert.Equal(t, "Hello, World!", string(content), "read %s from file %s, not expected Hello, World!", string(content), baseFn) { + return + } + } + }) +} +