1 package rotatelogs_test
14 "github.com/jonboulle/clockwork"
15 rotatelogs "github.com/lestrrat-go/file-rotatelogs"
16 "github.com/pkg/errors"
17 "github.com/stretchr/testify/assert"
20 func TestSatisfiesIOWriter(t *testing.T) {
22 w, _ = rotatelogs.New("/foo/bar")
26 func TestSatisfiesIOCloser(t *testing.T) {
28 c, _ = rotatelogs.New("/foo/bar")
32 func TestLogRotate(t *testing.T) {
33 dir, err := ioutil.TempDir("", "file-rotatelogs-test")
34 if !assert.NoError(t, err, "creating temporary directory should succeed") {
37 defer os.RemoveAll(dir)
39 // Change current time, so we can safely purge old logs
40 dummyTime := time.Now().Add(-7 * 24 * time.Hour)
41 dummyTime = dummyTime.Add(time.Duration(-1 * dummyTime.Nanosecond()))
42 clock := clockwork.NewFakeClockAt(dummyTime)
43 linkName := filepath.Join(dir, "log")
44 rl, err := rotatelogs.New(
45 filepath.Join(dir, "log%Y%m%d%H%M%S"),
46 rotatelogs.WithClock(clock),
47 rotatelogs.WithMaxAge(24*time.Hour),
48 rotatelogs.WithLinkName(linkName),
50 if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
56 n, err := rl.Write([]byte(str))
57 if !assert.NoError(t, err, "rl.Write should succeed") {
61 if !assert.Len(t, str, n, "rl.Write should succeed") {
65 fn := rl.CurrentFileName()
67 t.Errorf("Could not get filename %s", fn)
70 content, err := ioutil.ReadFile(fn)
72 t.Errorf("Failed to read file %s: %s", fn, err)
75 if string(content) != str {
76 t.Errorf(`File content does not match (was "%s")`, content)
79 err = os.Chtimes(fn, dummyTime, dummyTime)
81 t.Errorf("Failed to change access/modification times for %s: %s", fn, err)
84 fi, err := os.Stat(fn)
86 t.Errorf("Failed to stat %s: %s", fn, err)
89 if !fi.ModTime().Equal(dummyTime) {
90 t.Errorf("Failed to chtime for %s (expected %s, got %s)", fn, fi.ModTime(), dummyTime)
93 clock.Advance(time.Duration(7 * 24 * time.Hour))
95 // This next Write() should trigger Rotate()
97 newfn := rl.CurrentFileName()
99 t.Errorf(`New file name and old file name should not match ("%s" != "%s")`, fn, newfn)
102 content, err = ioutil.ReadFile(newfn)
104 t.Errorf("Failed to read file %s: %s", newfn, err)
107 if string(content) != str {
108 t.Errorf(`File content does not match (was "%s")`, content)
111 time.Sleep(time.Second)
113 // fn was declared above, before mocking CurrentTime
114 // Old files should have been unlinked
116 if !assert.Error(t, err, "os.Stat should have failed") {
120 linkDest, err := os.Readlink(linkName)
122 t.Errorf("Failed to readlink %s: %s", linkName, err)
125 if linkDest != newfn {
126 t.Errorf(`Symlink destination does not match expected filename ("%s" != "%s")`, newfn, linkDest)
130 func CreateRotationTestFile(dir string, base time.Time, d time.Duration, n int) {
132 for i := 0; i < n; i++ {
134 suffix := timestamp.Format("20060102150405")
135 path := filepath.Join(dir, "log"+suffix)
136 ioutil.WriteFile(path, []byte("rotation test file\n"), os.ModePerm)
137 os.Chtimes(path, timestamp, timestamp)
138 timestamp = timestamp.Add(d)
142 func TestLogRotationCount(t *testing.T) {
143 dir, err := ioutil.TempDir("", "file-rotatelogs-rotationcount-test")
144 if !assert.NoError(t, err, "creating temporary directory should succeed") {
147 defer os.RemoveAll(dir)
149 dummyTime := time.Now().Add(-7 * 24 * time.Hour)
150 dummyTime = dummyTime.Add(time.Duration(-1 * dummyTime.Nanosecond()))
151 clock := clockwork.NewFakeClockAt(dummyTime)
153 t.Run("Either maxAge or rotationCount should be set", func(t *testing.T) {
154 rl, err := rotatelogs.New(
155 filepath.Join(dir, "log%Y%m%d%H%M%S"),
156 rotatelogs.WithClock(clock),
157 rotatelogs.WithMaxAge(time.Duration(0)),
158 rotatelogs.WithRotationCount(0),
160 if !assert.NoError(t, err, `Both of maxAge and rotationCount is disabled`) {
166 t.Run("Either maxAge or rotationCount should be set", func(t *testing.T) {
167 rl, err := rotatelogs.New(
168 filepath.Join(dir, "log%Y%m%d%H%M%S"),
169 rotatelogs.WithClock(clock),
170 rotatelogs.WithMaxAge(1),
171 rotatelogs.WithRotationCount(1),
173 if !assert.Error(t, err, `Both of maxAge and rotationCount is enabled`) {
181 t.Run("Only latest log file is kept", func(t *testing.T) {
182 rl, err := rotatelogs.New(
183 filepath.Join(dir, "log%Y%m%d%H%M%S"),
184 rotatelogs.WithClock(clock),
185 rotatelogs.WithMaxAge(-1),
186 rotatelogs.WithRotationCount(1),
188 if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
193 n, err := rl.Write([]byte("dummy"))
194 if !assert.NoError(t, err, "rl.Write should succeed") {
197 if !assert.Len(t, "dummy", n, "rl.Write should succeed") {
200 time.Sleep(time.Second)
201 files, err := filepath.Glob(filepath.Join(dir, "log*"))
202 if !assert.Equal(t, 1, len(files), "Only latest log is kept") {
207 t.Run("Old log files are purged except 2 log files", func(t *testing.T) {
208 CreateRotationTestFile(dir, dummyTime, time.Duration(time.Hour), 5)
209 rl, err := rotatelogs.New(
210 filepath.Join(dir, "log%Y%m%d%H%M%S"),
211 rotatelogs.WithClock(clock),
212 rotatelogs.WithMaxAge(-1),
213 rotatelogs.WithRotationCount(2),
215 if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
220 n, err := rl.Write([]byte("dummy"))
221 if !assert.NoError(t, err, "rl.Write should succeed") {
224 if !assert.Len(t, "dummy", n, "rl.Write should succeed") {
227 time.Sleep(time.Second)
228 files, err := filepath.Glob(filepath.Join(dir, "log*"))
229 if !assert.Equal(t, 2, len(files), "One file is kept") {
236 func TestLogSetOutput(t *testing.T) {
237 dir, err := ioutil.TempDir("", "file-rotatelogs-test")
239 t.Errorf("Failed to create temporary directory: %s", err)
241 defer os.RemoveAll(dir)
243 rl, err := rotatelogs.New(filepath.Join(dir, "log%Y%m%d%H%M%S"))
244 if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
250 defer log.SetOutput(os.Stderr)
252 str := "Hello, World"
255 fn := rl.CurrentFileName()
257 t.Errorf("Could not get filename %s", fn)
260 content, err := ioutil.ReadFile(fn)
262 t.Errorf("Failed to read file %s: %s", fn, err)
265 if !strings.Contains(string(content), str) {
266 t.Errorf(`File content does not contain "%s" (was "%s")`, str, content)
270 func TestGHIssue16(t *testing.T) {
272 if v := recover(); v != nil {
273 assert.NoError(t, errors.Errorf("%s", v), "error should be nil")
277 dir, err := ioutil.TempDir("", "file-rotatelogs-gh16")
278 if !assert.NoError(t, err, `creating temporary directory should succeed`) {
281 defer os.RemoveAll(dir)
283 rl, err := rotatelogs.New(
284 filepath.Join(dir, "log%Y%m%d%H%M%S"),
285 rotatelogs.WithLinkName("./test.log"),
286 rotatelogs.WithRotationTime(10*time.Second),
287 rotatelogs.WithRotationCount(3),
288 rotatelogs.WithMaxAge(-1),
290 if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
294 if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") {
300 func TestRotationGenerationalNames(t *testing.T) {
301 dir, err := ioutil.TempDir("", "file-rotatelogs-generational")
302 if !assert.NoError(t, err, `creating temporary directory should succeed`) {
305 defer os.RemoveAll(dir)
307 t.Run("Rotate over unchanged pattern", func(t *testing.T) {
308 rl, err := rotatelogs.New(
309 filepath.Join(dir, "unchanged-pattern.log"),
311 if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
315 seen := map[string]struct{}{}
316 for i := 0; i < 10; i++ {
317 rl.Write([]byte("Hello, World!"))
318 if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") {
322 // Because every call to Rotate should yield a new log file,
323 // and the previous files already exist, the filenames should share
324 // the same prefix and have a unique suffix
325 fn := filepath.Base(rl.CurrentFileName())
326 if !assert.True(t, strings.HasPrefix(fn, "unchanged-pattern.log"), "prefix for all filenames should match") {
329 rl.Write([]byte("Hello, World!"))
330 suffix := strings.TrimPrefix(fn, "unchanged-pattern.log")
331 expectedSuffix := fmt.Sprintf(".%d", i+1)
332 if !assert.True(t, suffix == expectedSuffix, "expected suffix %s found %s", expectedSuffix, suffix) {
335 assert.FileExists(t, rl.CurrentFileName(), "file does not exist %s", rl.CurrentFileName())
336 stat, err := os.Stat(rl.CurrentFileName())
338 if !assert.True(t, stat.Size() == 13, "file %s size is %d, expected 13", rl.CurrentFileName(), stat.Size()) {
342 assert.Failf(t, "could not stat file %s", rl.CurrentFileName())
346 if _, ok := seen[suffix]; !assert.False(t, ok, `filename suffix %s should be unique`, suffix) {
349 seen[suffix] = struct{}{}
353 t.Run("Rotate over pattern change over every second", func(t *testing.T) {
354 rl, err := rotatelogs.New(
355 filepath.Join(dir, "every-second-pattern-%Y%m%d%H%M%S.log"),
356 rotatelogs.WithRotationTime(time.Nanosecond),
358 if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
362 for i := 0; i < 10; i++ {
363 time.Sleep(time.Second)
364 rl.Write([]byte("Hello, World!"))
365 if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") {
369 // because every new Write should yield a new logfile,
370 // every rorate should be create a filename ending with a .1
371 if !assert.True(t, strings.HasSuffix(rl.CurrentFileName(), ".1"), "log name should end with .1") {
379 type ClockFunc func() time.Time
381 func (f ClockFunc) Now() time.Time {
385 func TestGHIssue23(t *testing.T) {
386 dir, err := ioutil.TempDir("", "file-rotatelogs-generational")
387 if !assert.NoError(t, err, `creating temporary directory should succeed`) {
390 defer os.RemoveAll(dir)
392 for _, locName := range []string{"Asia/Tokyo", "Pacific/Honolulu"} {
393 loc, _ := time.LoadLocation(locName)
396 Clock rotatelogs.Clock
399 Expected: filepath.Join(dir, strings.ToLower(strings.Replace(locName, "/", "_", -1)) + ".201806010000.log"),
400 Clock: ClockFunc(func() time.Time {
401 return time.Date(2018, 6, 1, 3, 18, 0, 0, loc)
405 Expected: filepath.Join(dir, strings.ToLower(strings.Replace(locName, "/", "_", -1)) + ".201712310000.log"),
406 Clock: ClockFunc(func() time.Time {
407 return time.Date(2017, 12, 31, 23, 52, 0, 0, loc)
411 for _, test := range tests {
412 t.Run(fmt.Sprintf("location = %s, time = %s", locName, test.Clock.Now().Format(time.RFC3339)), func(t *testing.T) {
413 template := strings.ToLower(strings.Replace(locName, "/", "_", -1)) + ".%Y%m%d%H%M.log"
414 rl, err := rotatelogs.New(
415 filepath.Join(dir, template),
416 rotatelogs.WithClock(test.Clock), // we're not using WithLocation, but it's the same thing
418 if !assert.NoError(t, err, "rotatelogs.New should succeed") {
422 t.Logf("expected %s", test.Expected)
424 if !assert.Equal(t, test.Expected, rl.CurrentFileName(), "file names should match") {
432 func TestForceNewFile(t *testing.T) {
433 dir, err := ioutil.TempDir("", "file-rotatelogs-force-new-file")
434 if !assert.NoError(t, err, `creating temporary directory should succeed`) {
437 defer os.RemoveAll(dir)
439 t.Run("Force a new file", func(t *testing.T) {
441 rl, err := rotatelogs.New(
442 filepath.Join(dir, "force-new-file.log"),
443 rotatelogs.ForceNewFile(),
445 if !assert.NoError(t, err, "rotatelogs.New should succeed") {
448 rl.Write([]byte("Hello, World!"))
451 for i := 0; i < 10; i++ {
452 baseFn := filepath.Join(dir, "force-new-file.log")
453 rl, err := rotatelogs.New(
455 rotatelogs.ForceNewFile(),
457 if !assert.NoError(t, err, "rotatelogs.New should succeed") {
460 rl.Write([]byte("Hello, World"))
461 rl.Write([]byte(fmt.Sprintf("%d", i)))
464 fn := filepath.Base(rl.CurrentFileName())
465 suffix := strings.TrimPrefix(fn, "force-new-file.log")
466 expectedSuffix := fmt.Sprintf(".%d", i+1)
467 if !assert.True(t, suffix == expectedSuffix, "expected suffix %s found %s", expectedSuffix, suffix) {
470 assert.FileExists(t, rl.CurrentFileName(), "file does not exist %s", rl.CurrentFileName())
471 content, err := ioutil.ReadFile(rl.CurrentFileName())
472 if !assert.NoError(t, err, "ioutil.ReadFile %s should succeed", rl.CurrentFileName()) {
475 str := fmt.Sprintf("Hello, World%d", i)
476 if !assert.Equal(t, str, string(content), "read %s from file %s, not expected %s", string(content), rl.CurrentFileName(), str) {
480 assert.FileExists(t, baseFn, "file does not exist %s", baseFn)
481 content, err = ioutil.ReadFile(baseFn)
482 if !assert.NoError(t, err, "ioutil.ReadFile should succeed") {
485 if !assert.Equal(t, "Hello, World!", string(content), "read %s from file %s, not expected Hello, World!", string(content), baseFn) {
492 t.Run("Force a new file with Rotate", func(t *testing.T) {
494 baseFn := filepath.Join(dir, "force-new-file-rotate.log")
495 rl, err := rotatelogs.New(
497 rotatelogs.ForceNewFile(),
499 if !assert.NoError(t, err, "rotatelogs.New should succeed") {
502 rl.Write([]byte("Hello, World!"))
504 for i := 0; i < 10; i++ {
505 if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") {
508 rl.Write([]byte("Hello, World"))
509 rl.Write([]byte(fmt.Sprintf("%d", i)))
510 assert.FileExists(t, rl.CurrentFileName(), "file does not exist %s", rl.CurrentFileName())
511 content, err := ioutil.ReadFile(rl.CurrentFileName())
512 if !assert.NoError(t, err, "ioutil.ReadFile %s should succeed", rl.CurrentFileName()) {
515 str := fmt.Sprintf("Hello, World%d", i)
516 if !assert.Equal(t, str, string(content), "read %s from file %s, not expected %s", string(content), rl.CurrentFileName(), str) {
520 assert.FileExists(t, baseFn, "file does not exist %s", baseFn)
521 content, err = ioutil.ReadFile(baseFn)
522 if !assert.NoError(t, err, "ioutil.ReadFile should succeed") {
525 if !assert.Equal(t, "Hello, World!", string(content), "read %s from file %s, not expected Hello, World!", string(content), baseFn) {