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

Commit d086d55

Browse files
authored
Merge pull request #54 from lestrrat-go/topic/relative-path-link
Create symlinks with relative paths under certain circumstances
2 parents 73e5d42 + 407a8a9 commit d086d55

File tree

4 files changed

+172
-83
lines changed

4 files changed

+172
-83
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
language: go
22
sudo: false
33
go:
4-
- "1.10"
4+
- "1.14"
55
- tip

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ always check at the same location for log files even if the logs were rotated
123123
$ tail -f /var/log/myapp/current
124124
```
125125

126+
Links that share the same parent directory with the main log path will get a
127+
special treatment: namely, linked paths will be *RELATIVE* to the main log file.
128+
129+
| Main log file name | Link name | Linked path |
130+
|---------------------|---------------------|-----------------------|
131+
| /path/to/log.%Y%m%d | /path/to/log | log.YYYYMMDD |
132+
| /path/to/log.%Y%m%d | /path/to/nested/log | ../log.YYYYMMDD |
133+
| /path/to/log.%Y%m%d | /foo/bar/baz/log | /path/to/log.YYYYMMDD |
134+
126135
If not provided, no link will be written.
127136

128137
## RotationTime (default: 86400 sec)

rotatelogs.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,10 +279,36 @@ func (rl *RotateLogs) rotate_nolock(filename string) error {
279279

280280
if rl.linkName != "" {
281281
tmpLinkName := filename + `_symlink`
282-
if err := os.Symlink(filename, tmpLinkName); err != nil {
282+
283+
// Change how the link name is generated based on where the
284+
// target location is. if the location is directly underneath
285+
// the main filename's parent directory, then we create a
286+
// symlink with a relative path
287+
linkDest := filename
288+
linkDir := filepath.Dir(rl.linkName)
289+
290+
baseDir := filepath.Dir(filename)
291+
if strings.Contains(rl.linkName, baseDir) {
292+
tmp, err := filepath.Rel(linkDir, filename)
293+
if err != nil {
294+
return errors.Wrapf(err, `failed to evaluate relative path from %#v to %#v`, baseDir, rl.linkName)
295+
}
296+
297+
linkDest = tmp
298+
}
299+
300+
if err := os.Symlink(linkDest, tmpLinkName); err != nil {
283301
return errors.Wrap(err, `failed to create new symlink`)
284302
}
285303

304+
// the directory where rl.linkName should be created must exist
305+
_, err := os.Stat(linkDir)
306+
if err != nil { // Assume err != nil means the directory doesn't exist
307+
if err := os.MkdirAll(linkDir, 0755); err != nil {
308+
return errors.Wrapf(err, `failed to create directory %s`, linkDir)
309+
}
310+
}
311+
286312
if err := os.Rename(tmpLinkName, rl.linkName); err != nil {
287313
return errors.Wrap(err, `failed to rename new symlink`)
288314
}

rotatelogs_test.go

Lines changed: 135 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -30,100 +30,155 @@ func TestSatisfiesIOCloser(t *testing.T) {
3030
}
3131

3232
func TestLogRotate(t *testing.T) {
33-
dir, err := ioutil.TempDir("", "file-rotatelogs-test")
34-
if !assert.NoError(t, err, "creating temporary directory should succeed") {
35-
return
36-
}
37-
defer os.RemoveAll(dir)
33+
testCases := []struct {
34+
Name string
35+
FixArgs func([]rotatelogs.Option, string) []rotatelogs.Option
36+
CheckExtras func(*testing.T, *rotatelogs.RotateLogs, string) bool
37+
}{
38+
{
39+
Name: "Basic Usage",
40+
},
41+
{
42+
Name: "With Symlink",
43+
FixArgs: func(options []rotatelogs.Option, dir string) []rotatelogs.Option {
44+
linkName := filepath.Join(dir, "log")
45+
return append(options, rotatelogs.WithLinkName(linkName))
46+
},
47+
CheckExtras: func(t *testing.T, rl *rotatelogs.RotateLogs, dir string) bool {
48+
linkName := filepath.Join(dir, "log")
49+
linkDest, err := os.Readlink(linkName)
50+
if !assert.NoError(t, err, `os.Readlink(%#v) should succeed`, linkName) {
51+
return false
52+
}
3853

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),
49-
)
50-
if !assert.NoError(t, err, `rotatelogs.New should succeed`) {
51-
return
52-
}
53-
defer rl.Close()
54+
expectedLinkDest := filepath.Base(rl.CurrentFileName())
55+
t.Logf("expecting relative link: %s", expectedLinkDest)
56+
if !assert.Equal(t, linkDest, expectedLinkDest, `Symlink destination should match expected filename (%#v != %#v)`, expectedLinkDest, linkDest) {
57+
return false
58+
}
59+
return true
60+
},
61+
},
62+
{
63+
Name: "With Symlink (multiple levels)",
64+
FixArgs: func(options []rotatelogs.Option, dir string) []rotatelogs.Option {
65+
linkName := filepath.Join(dir, "nest1", "nest2", "log")
66+
return append(options, rotatelogs.WithLinkName(linkName))
67+
},
68+
CheckExtras: func(t *testing.T, rl *rotatelogs.RotateLogs, dir string) bool {
69+
linkName := filepath.Join(dir, "nest1", "nest2", "log")
70+
linkDest, err := os.Readlink(linkName)
71+
if !assert.NoError(t, err, `os.Readlink(%#v) should succeed`, linkName) {
72+
return false
73+
}
5474

55-
str := "Hello, World"
56-
n, err := rl.Write([]byte(str))
57-
if !assert.NoError(t, err, "rl.Write should succeed") {
58-
return
75+
expectedLinkDest := filepath.Join("..", "..", filepath.Base(rl.CurrentFileName()))
76+
t.Logf("expecting relative link: %s", expectedLinkDest)
77+
if !assert.Equal(t, linkDest, expectedLinkDest, `Symlink destination should match expected filename (%#v != %#v)`, expectedLinkDest, linkDest) {
78+
return false
79+
}
80+
return true
81+
},
82+
},
5983
}
6084

61-
if !assert.Len(t, str, n, "rl.Write should succeed") {
62-
return
63-
}
85+
for i, tc := range testCases {
86+
i := i // avoid lint errors
87+
tc := tc // avoid lint errors
88+
t.Run(tc.Name, func(t *testing.T) {
89+
dir, err := ioutil.TempDir("", fmt.Sprintf("file-rotatelogs-test%d", i))
90+
if !assert.NoError(t, err, "creating temporary directory should succeed") {
91+
return
92+
}
93+
defer os.RemoveAll(dir)
6494

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

70-
content, err := ioutil.ReadFile(fn)
71-
if err != nil {
72-
t.Errorf("Failed to read file %s: %s", fn, err)
73-
}
100+
options := []rotatelogs.Option{rotatelogs.WithClock(clock), rotatelogs.WithMaxAge(24 * time.Hour)}
101+
if fn := tc.FixArgs; fn != nil {
102+
options = fn(options, dir)
103+
}
74104

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

79-
err = os.Chtimes(fn, dummyTime, dummyTime)
80-
if err != nil {
81-
t.Errorf("Failed to change access/modification times for %s: %s", fn, err)
82-
}
111+
str := "Hello, World"
112+
n, err := rl.Write([]byte(str))
113+
if !assert.NoError(t, err, "rl.Write should succeed") {
114+
return
115+
}
83116

84-
fi, err := os.Stat(fn)
85-
if err != nil {
86-
t.Errorf("Failed to stat %s: %s", fn, err)
87-
}
117+
if !assert.Len(t, str, n, "rl.Write should succeed") {
118+
return
119+
}
88120

89-
if !fi.ModTime().Equal(dummyTime) {
90-
t.Errorf("Failed to chtime for %s (expected %s, got %s)", fn, fi.ModTime(), dummyTime)
91-
}
121+
fn := rl.CurrentFileName()
122+
if fn == "" {
123+
t.Errorf("Could not get filename %s", fn)
124+
}
92125

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

95-
// This next Write() should trigger Rotate()
96-
rl.Write([]byte(str))
97-
newfn := rl.CurrentFileName()
98-
if newfn == fn {
99-
t.Errorf(`New file name and old file name should not match ("%s" != "%s")`, fn, newfn)
100-
}
131+
if string(content) != str {
132+
t.Errorf(`File content does not match (was "%s")`, content)
133+
}
101134

102-
content, err = ioutil.ReadFile(newfn)
103-
if err != nil {
104-
t.Errorf("Failed to read file %s: %s", newfn, err)
105-
}
135+
err = os.Chtimes(fn, dummyTime, dummyTime)
136+
if err != nil {
137+
t.Errorf("Failed to change access/modification times for %s: %s", fn, err)
138+
}
106139

107-
if string(content) != str {
108-
t.Errorf(`File content does not match (was "%s")`, content)
109-
}
140+
fi, err := os.Stat(fn)
141+
if err != nil {
142+
t.Errorf("Failed to stat %s: %s", fn, err)
143+
}
110144

111-
time.Sleep(time.Second)
145+
if !fi.ModTime().Equal(dummyTime) {
146+
t.Errorf("Failed to chtime for %s (expected %s, got %s)", fn, fi.ModTime(), dummyTime)
147+
}
112148

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

120-
linkDest, err := os.Readlink(linkName)
121-
if err != nil {
122-
t.Errorf("Failed to readlink %s: %s", linkName, err)
123-
}
151+
// This next Write() should trigger Rotate()
152+
rl.Write([]byte(str))
153+
newfn := rl.CurrentFileName()
154+
if newfn == fn {
155+
t.Errorf(`New file name and old file name should not match ("%s" != "%s")`, fn, newfn)
156+
}
157+
158+
content, err = ioutil.ReadFile(newfn)
159+
if err != nil {
160+
t.Errorf("Failed to read file %s: %s", newfn, err)
161+
}
162+
163+
if string(content) != str {
164+
t.Errorf(`File content does not match (was "%s")`, content)
165+
}
166+
167+
time.Sleep(time.Second)
168+
169+
// fn was declared above, before mocking CurrentTime
170+
// Old files should have been unlinked
171+
_, err = os.Stat(fn)
172+
if !assert.Error(t, err, "os.Stat should have failed") {
173+
return
174+
}
124175

125-
if linkDest != newfn {
126-
t.Errorf(`Symlink destination does not match expected filename ("%s" != "%s")`, newfn, linkDest)
176+
if fn := tc.CheckExtras; fn != nil {
177+
if !fn(t, rl, dir) {
178+
return
179+
}
180+
}
181+
})
127182
}
128183
}
129184

@@ -396,13 +451,13 @@ func TestGHIssue23(t *testing.T) {
396451
Clock rotatelogs.Clock
397452
}{
398453
{
399-
Expected: filepath.Join(dir, strings.ToLower(strings.Replace(locName, "/", "_", -1)) + ".201806010000.log"),
454+
Expected: filepath.Join(dir, strings.ToLower(strings.Replace(locName, "/", "_", -1))+".201806010000.log"),
400455
Clock: ClockFunc(func() time.Time {
401456
return time.Date(2018, 6, 1, 3, 18, 0, 0, loc)
402457
}),
403458
},
404459
{
405-
Expected: filepath.Join(dir, strings.ToLower(strings.Replace(locName, "/", "_", -1)) + ".201712310000.log"),
460+
Expected: filepath.Join(dir, strings.ToLower(strings.Replace(locName, "/", "_", -1))+".201712310000.log"),
406461
Clock: ClockFunc(func() time.Time {
407462
return time.Date(2017, 12, 31, 23, 52, 0, 0, loc)
408463
}),
@@ -487,7 +542,7 @@ func TestForceNewFile(t *testing.T) {
487542
}
488543
}
489544

490-
})
545+
})
491546

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

@@ -506,7 +561,7 @@ func TestForceNewFile(t *testing.T) {
506561
return
507562
}
508563
rl.Write([]byte("Hello, World"))
509-
rl.Write([]byte(fmt.Sprintf("%d", i)))
564+
rl.Write([]byte(fmt.Sprintf("%d", i)))
510565
assert.FileExists(t, rl.CurrentFileName(), "file does not exist %s", rl.CurrentFileName())
511566
content, err := ioutil.ReadFile(rl.CurrentFileName())
512567
if !assert.NoError(t, err, "ioutil.ReadFile %s should succeed", rl.CurrentFileName()) {
@@ -528,4 +583,3 @@ func TestForceNewFile(t *testing.T) {
528583
}
529584
})
530585
}
531-

0 commit comments

Comments
 (0)