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

Commit

Permalink
Allow relative paths to any path inside the main log dir
Browse files Browse the repository at this point in the history
  • Loading branch information
lestrrat committed Aug 19, 2020
1 parent 45e7ae1 commit 407a8a9
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 87 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ always check at the same location for log files even if the logs were rotated
$ tail -f /var/log/myapp/current
```

Links that share the same parent directory with the main log path will get a
special treatment: namely, linked paths will be *RELATIVE* to the main log file.

| Main log file name | Link name | Linked path |
|---------------------|---------------------|-----------------------|
| /path/to/log.%Y%m%d | /path/to/log | log.YYYYMMDD |
| /path/to/log.%Y%m%d | /path/to/nested/log | ../log.YYYYMMDD |
| /path/to/log.%Y%m%d | /foo/bar/baz/log | /path/to/log.YYYYMMDD |

If not provided, no link will be written.

## RotationTime (default: 86400 sec)
Expand Down
26 changes: 24 additions & 2 deletions rotatelogs.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,14 +279,36 @@ func (rl *RotateLogs) rotate_nolock(filename string) error {

if rl.linkName != "" {
tmpLinkName := filename + `_symlink`

// Change how the link name is generated based on where the
// target location is. if the location is directly underneath
// the main filename's parent directory, then we create a
// symlink with a relative path
linkDest := filename
if filepath.Dir(rl.linkName) == filepath.Dir(filename) {
linkDest = filepath.Base(filename)
linkDir := filepath.Dir(rl.linkName)

baseDir := filepath.Dir(filename)
if strings.Contains(rl.linkName, baseDir) {
tmp, err := filepath.Rel(linkDir, filename)
if err != nil {
return errors.Wrapf(err, `failed to evaluate relative path from %#v to %#v`, baseDir, rl.linkName)
}

linkDest = tmp
}

if err := os.Symlink(linkDest, tmpLinkName); err != nil {
return errors.Wrap(err, `failed to create new symlink`)
}

// the directory where rl.linkName should be created must exist
_, err := os.Stat(linkDir)
if err != nil { // Assume err != nil means the directory doesn't exist
if err := os.MkdirAll(linkDir, 0755); err != nil {
return errors.Wrapf(err, `failed to create directory %s`, linkDir)
}
}

if err := os.Rename(tmpLinkName, rl.linkName); err != nil {
return errors.Wrap(err, `failed to rename new symlink`)
}
Expand Down
220 changes: 135 additions & 85 deletions rotatelogs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,104 +30,155 @@ func TestSatisfiesIOCloser(t *testing.T) {
}

func TestLogRotate(t *testing.T) {
dir, err := ioutil.TempDir("", "file-rotatelogs-test")
if !assert.NoError(t, err, "creating temporary directory should succeed") {
return
}
defer os.RemoveAll(dir)
testCases := []struct {
Name string
FixArgs func([]rotatelogs.Option, string) []rotatelogs.Option
CheckExtras func(*testing.T, *rotatelogs.RotateLogs, string) bool
}{
{
Name: "Basic Usage",
},
{
Name: "With Symlink",
FixArgs: func(options []rotatelogs.Option, dir string) []rotatelogs.Option {
linkName := filepath.Join(dir, "log")
return append(options, rotatelogs.WithLinkName(linkName))
},
CheckExtras: func(t *testing.T, rl *rotatelogs.RotateLogs, dir string) bool {
linkName := filepath.Join(dir, "log")
linkDest, err := os.Readlink(linkName)
if !assert.NoError(t, err, `os.Readlink(%#v) should succeed`, linkName) {
return false
}

// Change current time, so we can safely purge old logs
dummyTime := time.Now().Add(-7 * 24 * time.Hour)
dummyTime = dummyTime.Add(time.Duration(-1 * dummyTime.Nanosecond()))
clock := clockwork.NewFakeClockAt(dummyTime)
linkName := filepath.Join(dir, "log")
rl, err := rotatelogs.New(
filepath.Join(dir, "log%Y%m%d%H%M%S"),
rotatelogs.WithClock(clock),
rotatelogs.WithMaxAge(24*time.Hour),
rotatelogs.WithLinkName(linkName),
)
if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
return
}
defer rl.Close()
expectedLinkDest := filepath.Base(rl.CurrentFileName())
t.Logf("expecting relative link: %s", expectedLinkDest)
if !assert.Equal(t, linkDest, expectedLinkDest, `Symlink destination should match expected filename (%#v != %#v)`, expectedLinkDest, linkDest) {
return false
}
return true
},
},
{
Name: "With Symlink (multiple levels)",
FixArgs: func(options []rotatelogs.Option, dir string) []rotatelogs.Option {
linkName := filepath.Join(dir, "nest1", "nest2", "log")
return append(options, rotatelogs.WithLinkName(linkName))
},
CheckExtras: func(t *testing.T, rl *rotatelogs.RotateLogs, dir string) bool {
linkName := filepath.Join(dir, "nest1", "nest2", "log")
linkDest, err := os.Readlink(linkName)
if !assert.NoError(t, err, `os.Readlink(%#v) should succeed`, linkName) {
return false
}

str := "Hello, World"
n, err := rl.Write([]byte(str))
if !assert.NoError(t, err, "rl.Write should succeed") {
return
expectedLinkDest := filepath.Join("..", "..", filepath.Base(rl.CurrentFileName()))
t.Logf("expecting relative link: %s", expectedLinkDest)
if !assert.Equal(t, linkDest, expectedLinkDest, `Symlink destination should match expected filename (%#v != %#v)`, expectedLinkDest, linkDest) {
return false
}
return true
},
},
}

if !assert.Len(t, str, n, "rl.Write should succeed") {
return
}
for i, tc := range testCases {
i := i // avoid lint errors
tc := tc // avoid lint errors
t.Run(tc.Name, func(t *testing.T) {
dir, err := ioutil.TempDir("", fmt.Sprintf("file-rotatelogs-test%d", i))
if !assert.NoError(t, err, "creating temporary directory should succeed") {
return
}
defer os.RemoveAll(dir)

fn := rl.CurrentFileName()
if fn == "" {
t.Errorf("Could not get filename %s", fn)
}
// Change current time, so we can safely purge old logs
dummyTime := time.Now().Add(-7 * 24 * time.Hour)
dummyTime = dummyTime.Add(time.Duration(-1 * dummyTime.Nanosecond()))
clock := clockwork.NewFakeClockAt(dummyTime)

content, err := ioutil.ReadFile(fn)
if err != nil {
t.Errorf("Failed to read file %s: %s", fn, err)
}
options := []rotatelogs.Option{rotatelogs.WithClock(clock), rotatelogs.WithMaxAge(24 * time.Hour)}
if fn := tc.FixArgs; fn != nil {
options = fn(options, dir)
}

if string(content) != str {
t.Errorf(`File content does not match (was "%s")`, content)
}
rl, err := rotatelogs.New(filepath.Join(dir, "log%Y%m%d%H%M%S"), options...)
if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
return
}
defer rl.Close()

err = os.Chtimes(fn, dummyTime, dummyTime)
if err != nil {
t.Errorf("Failed to change access/modification times for %s: %s", fn, err)
}
str := "Hello, World"
n, err := rl.Write([]byte(str))
if !assert.NoError(t, err, "rl.Write should succeed") {
return
}

fi, err := os.Stat(fn)
if err != nil {
t.Errorf("Failed to stat %s: %s", fn, err)
}
if !assert.Len(t, str, n, "rl.Write should succeed") {
return
}

if !fi.ModTime().Equal(dummyTime) {
t.Errorf("Failed to chtime for %s (expected %s, got %s)", fn, fi.ModTime(), dummyTime)
}
fn := rl.CurrentFileName()
if fn == "" {
t.Errorf("Could not get filename %s", fn)
}

clock.Advance(time.Duration(7 * 24 * time.Hour))
content, err := ioutil.ReadFile(fn)
if err != nil {
t.Errorf("Failed to read file %s: %s", fn, err)
}

// This next Write() should trigger Rotate()
rl.Write([]byte(str))
newfn := rl.CurrentFileName()
if newfn == fn {
t.Errorf(`New file name and old file name should not match ("%s" != "%s")`, fn, newfn)
}
if string(content) != str {
t.Errorf(`File content does not match (was "%s")`, content)
}

content, err = ioutil.ReadFile(newfn)
if err != nil {
t.Errorf("Failed to read file %s: %s", newfn, err)
}
err = os.Chtimes(fn, dummyTime, dummyTime)
if err != nil {
t.Errorf("Failed to change access/modification times for %s: %s", fn, err)
}

if string(content) != str {
t.Errorf(`File content does not match (was "%s")`, content)
}
fi, err := os.Stat(fn)
if err != nil {
t.Errorf("Failed to stat %s: %s", fn, err)
}

time.Sleep(time.Second)
if !fi.ModTime().Equal(dummyTime) {
t.Errorf("Failed to chtime for %s (expected %s, got %s)", fn, fi.ModTime(), dummyTime)
}

// fn was declared above, before mocking CurrentTime
// Old files should have been unlinked
_, err = os.Stat(fn)
if !assert.Error(t, err, "os.Stat should have failed") {
return
}
clock.Advance(time.Duration(7 * 24 * time.Hour))

linkDest, err := os.Readlink(linkName)
if err != nil {
t.Errorf("Failed to readlink %s: %s", linkName, err)
}
// This next Write() should trigger Rotate()
rl.Write([]byte(str))
newfn := rl.CurrentFileName()
if newfn == fn {
t.Errorf(`New file name and old file name should not match ("%s" != "%s")`, fn, newfn)
}

expectedLinkDest := newfn
if filepath.Dir(newfn) == filepath.Dir(linkName) {
expectedLinkDest = filepath.Base(newfn)
}
if linkDest != expectedLinkDest {
t.Errorf(`Symlink destination does not match expected filename ("%s" != "%s")`, expectedLinkDest, linkDest)
content, err = ioutil.ReadFile(newfn)
if err != nil {
t.Errorf("Failed to read file %s: %s", newfn, err)
}

if string(content) != str {
t.Errorf(`File content does not match (was "%s")`, content)
}

time.Sleep(time.Second)

// fn was declared above, before mocking CurrentTime
// Old files should have been unlinked
_, err = os.Stat(fn)
if !assert.Error(t, err, "os.Stat should have failed") {
return
}

if fn := tc.CheckExtras; fn != nil {
if !fn(t, rl, dir) {
return
}
}
})
}
}

Expand Down Expand Up @@ -400,13 +451,13 @@ func TestGHIssue23(t *testing.T) {
Clock rotatelogs.Clock
}{
{
Expected: filepath.Join(dir, strings.ToLower(strings.Replace(locName, "/", "_", -1)) + ".201806010000.log"),
Expected: filepath.Join(dir, strings.ToLower(strings.Replace(locName, "/", "_", -1))+".201806010000.log"),
Clock: ClockFunc(func() time.Time {
return time.Date(2018, 6, 1, 3, 18, 0, 0, loc)
}),
},
{
Expected: filepath.Join(dir, strings.ToLower(strings.Replace(locName, "/", "_", -1)) + ".201712310000.log"),
Expected: filepath.Join(dir, strings.ToLower(strings.Replace(locName, "/", "_", -1))+".201712310000.log"),
Clock: ClockFunc(func() time.Time {
return time.Date(2017, 12, 31, 23, 52, 0, 0, loc)
}),
Expand Down Expand Up @@ -491,7 +542,7 @@ func TestForceNewFile(t *testing.T) {
}
}

})
})

t.Run("Force a new file with Rotate", func(t *testing.T) {

Expand All @@ -510,7 +561,7 @@ func TestForceNewFile(t *testing.T) {
return
}
rl.Write([]byte("Hello, World"))
rl.Write([]byte(fmt.Sprintf("%d", i)))
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()) {
Expand All @@ -532,4 +583,3 @@ func TestForceNewFile(t *testing.T) {
}
})
}

0 comments on commit 407a8a9

Please sign in to comment.