Skip to content
This repository has been archived by the owner on Jul 19, 2021. It is now read-only.

Commit

Permalink
Merge pull request #22 from lestrrat-go/topic/rotate
Browse files Browse the repository at this point in the history
Make `Rotate` generate unique filenames in case of a name clash
  • Loading branch information
lestrrat authored Apr 14, 2018
2 parents 31912eb + 0f8d75a commit 81893a5
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 7 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,28 @@ Note: MaxAge should be disabled by specifing `WithMaxAge(-1)` explicitly.
rotatelogs.WithRotationCount(7),
)
```

# Rotating files forcefully

If you want to rotate files forcefully before the actual rotation time has reached,
you may use the `Rotate()` method. This method forcefully rotates the logs, but
if the generated file name clashes, then a numeric suffix is added so that
the new file will forcefully appear on disk.

For example, suppose you had a pattern of '%Y.log' with a rotation time of
`86400` so that it only gets rotated every year, but for whatever reason you
wanted to rotate the logs now, you could install a signal handler to
trigger this rotation:

```go
rl := rotatelogs.New(...)

signal.Notify(ch, syscall.SIGHUP)

go func(ch chan os.Signal) {
<-ch
rl.Rotate()
}()
```

And you will get a log file name in like `2018.log.1`, `2018.log.2`, etc.
1 change: 1 addition & 0 deletions interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type RotateLogs struct {
clock Clock
curFn string
globPattern string
generation int
linkName string
maxAge time.Duration
mutex sync.RWMutex
Expand Down
39 changes: 32 additions & 7 deletions rotatelogs.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (rl *RotateLogs) Write(p []byte) (n int, err error) {
rl.mutex.Lock()
defer rl.mutex.Unlock()

out, err := rl.getWriter_nolock(false)
out, err := rl.getWriter_nolock(false, false)
if err != nil {
return 0, errors.Wrap(err, `failed to acquite target io.Writer`)
}
Expand All @@ -107,13 +107,31 @@ func (rl *RotateLogs) Write(p []byte) (n int, err error) {
}

// must be locked during this operation
func (rl *RotateLogs) getWriter_nolock(bailOnRotateFail bool) (io.Writer, error) {
func (rl *RotateLogs) getWriter_nolock(bailOnRotateFail, useGenerationalNames bool) (io.Writer, error) {
generation := rl.generation

// This filename contains the name of the "NEW" filename
// to log to, which may be newer than rl.currentFilename
filename := rl.genFilename()
if rl.curFn == filename {
// nothing to do
return rl.outFh, nil
if rl.curFn != filename {
generation = 0
} 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
for {
generation++
name := fmt.Sprintf("%s.%d", filename, generation)
if _, err := os.Stat(name); err != nil {
filename = name
break
}
}
}

// if we got here, then we need to create a file
Expand All @@ -137,6 +155,7 @@ func (rl *RotateLogs) getWriter_nolock(bailOnRotateFail bool) (io.Writer, error)
rl.outFh.Close()
rl.outFh = fh
rl.curFn = filename
rl.generation = generation

return fh, nil
}
Expand Down Expand Up @@ -169,11 +188,17 @@ func (g *cleanupGuard) Run() {
g.fn()
}

// Rotate forcefully rotates the log files.
// Rotate forcefully rotates the log files. If the generated file name
// clash because file already exists, a numeric suffix of the form
// ".1", ".2", ".3" and so forth are appended to the end of the log file
//
// Thie method can be used in conjunction with a signal handler so to
// emulate servers that generate new log files when they receive a
// SIGHUP
func (rl *RotateLogs) Rotate() error {
rl.mutex.Lock()
defer rl.mutex.Unlock()
if _, err := rl.getWriter_nolock(true); err != nil {
if _, err := rl.getWriter_nolock(true, true); err != nil {
return err
}
return nil
Expand Down
63 changes: 63 additions & 0 deletions rotatelogs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,66 @@ func TestGHIssue16(t *testing.T) {
}
defer rl.Close()
}

func TestRotationGenerationalNames(t *testing.T) {
dir, err := ioutil.TempDir("", "file-rotatelogs-generational")
if !assert.NoError(t, err, `creating temporary directory should succeed`) {
return
}
defer os.RemoveAll(dir)

t.Run("Rotate over unchanged pattern", func(t *testing.T) {
rl, err := rotatelogs.New(
filepath.Join(dir, "unchaged-pattern.log"),
)
if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
return
}

seen := map[string]struct{}{}
for i := 0; i < 10; i++ {
rl.Write([]byte("Hello, World!"))
if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") {
return
}

// Because every call to Rotate should yield a new log file,
// and the previous files already exist, the filenames should share
// the same prefix and have a unique suffix
fn := filepath.Base(rl.CurrentFileName())
if !assert.True(t, strings.HasPrefix(fn, "unchaged-pattern.log"), "prefix for all filenames should match") {
return
}
suffix := strings.TrimPrefix(fn, "unchanged-pattern.log")
if _, ok := seen[suffix]; !assert.False(t, ok, `filename suffix %s should be unique`, suffix) {
return
}
seen[suffix] = struct{}{}
}
defer rl.Close()
})
t.Run("Rotate over pattern change over every second", func(t *testing.T) {
rl, err := rotatelogs.New(
filepath.Join(dir, "every-second-pattern-%Y%m%d%H%M%S.log"),
rotatelogs.WithRotationTime(time.Nanosecond),
)
if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
return
}

for i := 0; i < 10; i++ {
time.Sleep(time.Second)
rl.Write([]byte("Hello, World!"))
if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") {
return
}

// because every new Write should yield a new logfile,
// every rorate should be create a filename ending with a .1
if !assert.True(t, strings.HasSuffix(rl.CurrentFileName(), ".1"), "log name should end with .1") {
return
}
}
defer rl.Close()
})
}

0 comments on commit 81893a5

Please sign in to comment.