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

Commit 0f8d75a

Browse files
committed
Make Rotate generate unique filenames in case of a name clash
Without this, you still can't really Rotate manually.
1 parent 31912eb commit 0f8d75a

File tree

4 files changed

+121
-7
lines changed

4 files changed

+121
-7
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,28 @@ Note: MaxAge should be disabled by specifing `WithMaxAge(-1)` explicitly.
163163
rotatelogs.WithRotationCount(7),
164164
)
165165
```
166+
167+
# Rotating files forcefully
168+
169+
If you want to rotate files forcefully before the actual rotation time has reached,
170+
you may use the `Rotate()` method. This method forcefully rotates the logs, but
171+
if the generated file name clashes, then a numeric suffix is added so that
172+
the new file will forcefully appear on disk.
173+
174+
For example, suppose you had a pattern of '%Y.log' with a rotation time of
175+
`86400` so that it only gets rotated every year, but for whatever reason you
176+
wanted to rotate the logs now, you could install a signal handler to
177+
trigger this rotation:
178+
179+
```go
180+
rl := rotatelogs.New(...)
181+
182+
signal.Notify(ch, syscall.SIGHUP)
183+
184+
go func(ch chan os.Signal) {
185+
<-ch
186+
rl.Rotate()
187+
}()
188+
```
189+
190+
And you will get a log file name in like `2018.log.1`, `2018.log.2`, etc.

interface.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type RotateLogs struct {
1414
clock Clock
1515
curFn string
1616
globPattern string
17+
generation int
1718
linkName string
1819
maxAge time.Duration
1920
mutex sync.RWMutex

rotatelogs.go

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ func (rl *RotateLogs) Write(p []byte) (n int, err error) {
9898
rl.mutex.Lock()
9999
defer rl.mutex.Unlock()
100100

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

109109
// must be locked during this operation
110-
func (rl *RotateLogs) getWriter_nolock(bailOnRotateFail bool) (io.Writer, error) {
110+
func (rl *RotateLogs) getWriter_nolock(bailOnRotateFail, useGenerationalNames bool) (io.Writer, error) {
111+
generation := rl.generation
112+
111113
// This filename contains the name of the "NEW" filename
112114
// to log to, which may be newer than rl.currentFilename
113115
filename := rl.genFilename()
114-
if rl.curFn == filename {
115-
// nothing to do
116-
return rl.outFh, nil
116+
if rl.curFn != filename {
117+
generation = 0
118+
} else {
119+
if !useGenerationalNames {
120+
// nothing to do
121+
return rl.outFh, nil
122+
}
123+
// This is used when we *REALLY* want to rotate a log.
124+
// instead of just using the regular strftime pattern, we
125+
// create a new file name using generational names such as
126+
// "foo.1", "foo.2", "foo.3", etc
127+
for {
128+
generation++
129+
name := fmt.Sprintf("%s.%d", filename, generation)
130+
if _, err := os.Stat(name); err != nil {
131+
filename = name
132+
break
133+
}
134+
}
117135
}
118136

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

141160
return fh, nil
142161
}
@@ -169,11 +188,17 @@ func (g *cleanupGuard) Run() {
169188
g.fn()
170189
}
171190

172-
// Rotate forcefully rotates the log files.
191+
// Rotate forcefully rotates the log files. If the generated file name
192+
// clash because file already exists, a numeric suffix of the form
193+
// ".1", ".2", ".3" and so forth are appended to the end of the log file
194+
//
195+
// Thie method can be used in conjunction with a signal handler so to
196+
// emulate servers that generate new log files when they receive a
197+
// SIGHUP
173198
func (rl *RotateLogs) Rotate() error {
174199
rl.mutex.Lock()
175200
defer rl.mutex.Unlock()
176-
if _, err := rl.getWriter_nolock(true); err != nil {
201+
if _, err := rl.getWriter_nolock(true, true); err != nil {
177202
return err
178203
}
179204
return nil

rotatelogs_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,66 @@ func TestGHIssue16(t *testing.T) {
295295
}
296296
defer rl.Close()
297297
}
298+
299+
func TestRotationGenerationalNames(t *testing.T) {
300+
dir, err := ioutil.TempDir("", "file-rotatelogs-generational")
301+
if !assert.NoError(t, err, `creating temporary directory should succeed`) {
302+
return
303+
}
304+
defer os.RemoveAll(dir)
305+
306+
t.Run("Rotate over unchanged pattern", func(t *testing.T) {
307+
rl, err := rotatelogs.New(
308+
filepath.Join(dir, "unchaged-pattern.log"),
309+
)
310+
if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
311+
return
312+
}
313+
314+
seen := map[string]struct{}{}
315+
for i := 0; i < 10; i++ {
316+
rl.Write([]byte("Hello, World!"))
317+
if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") {
318+
return
319+
}
320+
321+
// Because every call to Rotate should yield a new log file,
322+
// and the previous files already exist, the filenames should share
323+
// the same prefix and have a unique suffix
324+
fn := filepath.Base(rl.CurrentFileName())
325+
if !assert.True(t, strings.HasPrefix(fn, "unchaged-pattern.log"), "prefix for all filenames should match") {
326+
return
327+
}
328+
suffix := strings.TrimPrefix(fn, "unchanged-pattern.log")
329+
if _, ok := seen[suffix]; !assert.False(t, ok, `filename suffix %s should be unique`, suffix) {
330+
return
331+
}
332+
seen[suffix] = struct{}{}
333+
}
334+
defer rl.Close()
335+
})
336+
t.Run("Rotate over pattern change over every second", func(t *testing.T) {
337+
rl, err := rotatelogs.New(
338+
filepath.Join(dir, "every-second-pattern-%Y%m%d%H%M%S.log"),
339+
rotatelogs.WithRotationTime(time.Nanosecond),
340+
)
341+
if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
342+
return
343+
}
344+
345+
for i := 0; i < 10; i++ {
346+
time.Sleep(time.Second)
347+
rl.Write([]byte("Hello, World!"))
348+
if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") {
349+
return
350+
}
351+
352+
// because every new Write should yield a new logfile,
353+
// every rorate should be create a filename ending with a .1
354+
if !assert.True(t, strings.HasSuffix(rl.CurrentFileName(), ".1"), "log name should end with .1") {
355+
return
356+
}
357+
}
358+
defer rl.Close()
359+
})
360+
}

0 commit comments

Comments
 (0)