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

Commit 31912eb

Browse files
authored
Merge pull request #17 from lestrrat-go/topic/issue-16
Fix handling of MaxAge/RotationCount
2 parents 2116cfc + 366f3c8 commit 31912eb

File tree

6 files changed

+202
-121
lines changed

6 files changed

+202
-121
lines changed

interface.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type RotateLogs struct {
2020
outFh *os.File
2121
pattern *strftime.Strftime
2222
rotationTime time.Duration
23-
rotationCount int
23+
rotationCount uint
2424
}
2525

2626
// Clock is the interface used by the RotateLogs
@@ -41,9 +41,6 @@ var Local = clockFn(time.Now)
4141
// Option is used to pass optional arguments to
4242
// the RotateLogs constructor
4343
type Option interface {
44-
Configure(*RotateLogs) error
44+
Name() string
45+
Value() interface {}
4546
}
46-
47-
// OptionFn is a type of Option that is represented
48-
// by a single function that gets called for Configure()
49-
type OptionFn func(*RotateLogs) error

internal/option/option.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package option
2+
3+
type Interface interface {
4+
Name() string
5+
Value() interface{}
6+
}
7+
8+
type Option struct {
9+
name string
10+
value interface{}
11+
}
12+
13+
func New(name string, value interface{}) *Option {
14+
return &Option{
15+
name: name,
16+
value: value,
17+
}
18+
}
19+
20+
func (o *Option) Name() string {
21+
return o.name
22+
}
23+
func (o *Option) Value() interface{} {
24+
return o.value
25+
}

internal_test.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,3 @@ func TestGenFilename(t *testing.T) {
3939
}
4040
}
4141
}
42-
43-
func TestWithLocation(t *testing.T) {
44-
// Not really a test, but well...
45-
loc, _ := time.LoadLocation("Asia/Tokyo")
46-
opt := WithLocation(loc)
47-
var rl RotateLogs
48-
opt.Configure(&rl)
49-
t.Logf("%s", rl.clock.Now())
50-
}
51-

options.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package rotatelogs
2+
3+
import (
4+
"time"
5+
6+
"github.com/lestrrat-go/file-rotatelogs/internal/option"
7+
)
8+
9+
const (
10+
optkeyClock = "clock"
11+
optkeyLinkName = "link-name"
12+
optkeyMaxAge = "max-age"
13+
optkeyRotationTime = "rotation-time"
14+
optkeyRotationCount = "rotation-count"
15+
)
16+
17+
// WithClock creates a new Option that sets a clock
18+
// that the RotateLogs object will use to determine
19+
// the current time.
20+
//
21+
// By default rotatelogs.Local, which returns the
22+
// current time in the local time zone, is used. If you
23+
// would rather use UTC, use rotatelogs.UTC as the argument
24+
// to this option, and pass it to the constructor.
25+
func WithClock(c Clock) Option {
26+
return option.New(optkeyClock, c)
27+
}
28+
29+
// WithLocation creates a new Option that sets up a
30+
// "Clock" interface that the RotateLogs object will use
31+
// to determine the current time.
32+
//
33+
// This optin works by always returning the in the given
34+
// location.
35+
func WithLocation(loc *time.Location) Option {
36+
return option.New(optkeyClock, clockFn(func() time.Time {
37+
return time.Now().In(loc)
38+
}))
39+
}
40+
41+
// WithLinkName creates a new Option that sets the
42+
// symbolic link name that gets linked to the current
43+
// file name being used.
44+
func WithLinkName(s string) Option {
45+
return option.New(optkeyLinkName, s)
46+
}
47+
48+
// WithMaxAge creates a new Option that sets the
49+
// max age of a log file before it gets purged from
50+
// the file system.
51+
func WithMaxAge(d time.Duration) Option {
52+
return option.New(optkeyMaxAge, d)
53+
}
54+
55+
// WithRotationTime creates a new Option that sets the
56+
// time between rotation.
57+
func WithRotationTime(d time.Duration) Option {
58+
return option.New(optkeyRotationTime, d)
59+
}
60+
61+
// WithRotationCount creates a new Option that sets the
62+
// number of files should be kept before it gets
63+
// purged from the file system.
64+
func WithRotationCount(n uint) Option {
65+
return option.New(optkeyRotationCount, n)
66+
}

rotatelogs.go

Lines changed: 71 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -22,108 +22,64 @@ func (c clockFn) Now() time.Time {
2222
return c()
2323
}
2424

25-
func (o OptionFn) Configure(rl *RotateLogs) error {
26-
return o(rl)
27-
}
28-
29-
// WithClock creates a new Option that sets a clock
30-
// that the RotateLogs object will use to determine
31-
// the current time.
32-
//
33-
// By default rotatelogs.Local, which returns the
34-
// current time in the local time zone, is used. If you
35-
// would rather use UTC, use rotatelogs.UTC as the argument
36-
// to this option, and pass it to the constructor.
37-
func WithClock(c Clock) Option {
38-
return OptionFn(func(rl *RotateLogs) error {
39-
rl.clock = c
40-
return nil
41-
})
42-
}
43-
44-
// WithLocation creates a new Option that sets up a
45-
// "Clock" interface that the RotateLogs object will use
46-
// to determine the current time.
47-
//
48-
// This optin works by always returning the in the given
49-
// location.
50-
func WithLocation(loc *time.Location) Option {
51-
return WithClock(clockFn(func() time.Time {
52-
return time.Now().In(loc)
53-
}))
54-
}
55-
56-
// WithLinkName creates a new Option that sets the
57-
// symbolic link name that gets linked to the current
58-
// file name being used.
59-
func WithLinkName(s string) Option {
60-
return OptionFn(func(rl *RotateLogs) error {
61-
rl.linkName = s
62-
return nil
63-
})
64-
}
65-
66-
// WithMaxAge creates a new Option that sets the
67-
// max age of a log file before it gets purged from
68-
// the file system.
69-
func WithMaxAge(d time.Duration) Option {
70-
return OptionFn(func(rl *RotateLogs) error {
71-
if rl.rotationCount > 0 && d > 0 {
72-
return errors.New("attempt to set MaxAge when RotationCount is also given")
73-
}
74-
rl.maxAge = d
75-
return nil
76-
})
77-
}
78-
79-
// WithRotationTime creates a new Option that sets the
80-
// time between rotation.
81-
func WithRotationTime(d time.Duration) Option {
82-
return OptionFn(func(rl *RotateLogs) error {
83-
rl.rotationTime = d
84-
return nil
85-
})
86-
}
87-
88-
// WithRotationCount creates a new Option that sets the
89-
// number of files should be kept before it gets
90-
// purged from the file system.
91-
func WithRotationCount(n int) Option {
92-
return OptionFn(func(rl *RotateLogs) error {
93-
if rl.maxAge > 0 && n > 0 {
94-
return errors.New("attempt to set RotationCount when MaxAge is also given")
95-
}
96-
rl.rotationCount = n
97-
return nil
98-
})
99-
}
100-
10125
// New creates a new RotateLogs object. A log filename pattern
10226
// must be passed. Optional `Option` parameters may be passed
103-
func New(pattern string, options ...Option) (*RotateLogs, error) {
104-
globPattern := pattern
27+
func New(p string, options ...Option) (*RotateLogs, error) {
28+
globPattern := p
10529
for _, re := range patternConversionRegexps {
10630
globPattern = re.ReplaceAllString(globPattern, "*")
10731
}
10832

109-
strfobj, err := strftime.New(pattern)
33+
pattern, err := strftime.New(p)
11034
if err != nil {
11135
return nil, errors.Wrap(err, `invalid strftime pattern`)
11236
}
11337

114-
var rl RotateLogs
115-
rl.clock = Local
116-
rl.globPattern = globPattern
117-
rl.pattern = strfobj
118-
rl.rotationTime = 24 * time.Hour
119-
// Keeping forward compatibility, maxAge is prior to rotationCount.
120-
rl.maxAge = 7 * 24 * time.Hour
121-
rl.rotationCount = -1
122-
for _, opt := range options {
123-
opt.Configure(&rl)
38+
var clock Clock = Local
39+
rotationTime := 24 * time.Hour
40+
var rotationCount uint
41+
var linkName string
42+
var maxAge time.Duration
43+
44+
for _, o := range options {
45+
switch o.Name() {
46+
case optkeyClock:
47+
clock = o.Value().(Clock)
48+
case optkeyLinkName:
49+
linkName = o.Value().(string)
50+
case optkeyMaxAge:
51+
maxAge = o.Value().(time.Duration)
52+
if maxAge < 0 {
53+
maxAge = 0
54+
}
55+
case optkeyRotationTime:
56+
rotationTime = o.Value().(time.Duration)
57+
if rotationTime < 0 {
58+
rotationTime = 0
59+
}
60+
case optkeyRotationCount:
61+
rotationCount = o.Value().(uint)
62+
}
63+
}
64+
65+
if maxAge > 0 && rotationCount > 0 {
66+
return nil, errors.New("options MaxAge and RotationCount cannot be both set")
67+
}
68+
69+
if maxAge == 0 && rotationCount == 0 {
70+
// if both are 0, give maxAge a sane default
71+
maxAge = 7 * 24 * time.Hour
12472
}
12573

126-
return &rl, nil
74+
return &RotateLogs{
75+
clock: clock,
76+
globPattern: globPattern,
77+
linkName: linkName,
78+
maxAge: maxAge,
79+
pattern: pattern,
80+
rotationTime: rotationTime,
81+
rotationCount: rotationCount,
82+
}, nil
12783
}
12884

12985
func (rl *RotateLogs) genFilename() string {
@@ -142,7 +98,7 @@ func (rl *RotateLogs) Write(p []byte) (n int, err error) {
14298
rl.mutex.Lock()
14399
defer rl.mutex.Unlock()
144100

145-
out, err := rl.getTargetWriter()
101+
out, err := rl.getWriter_nolock(false)
146102
if err != nil {
147103
return 0, errors.Wrap(err, `failed to acquite target io.Writer`)
148104
}
@@ -151,7 +107,7 @@ func (rl *RotateLogs) Write(p []byte) (n int, err error) {
151107
}
152108

153109
// must be locked during this operation
154-
func (rl *RotateLogs) getTargetWriter() (io.Writer, error) {
110+
func (rl *RotateLogs) getWriter_nolock(bailOnRotateFail bool) (io.Writer, error) {
155111
// This filename contains the name of the "NEW" filename
156112
// to log to, which may be newer than rl.currentFilename
157113
filename := rl.genFilename()
@@ -166,12 +122,16 @@ func (rl *RotateLogs) getTargetWriter() (io.Writer, error) {
166122
return nil, errors.Errorf("failed to open file %s: %s", rl.pattern, err)
167123
}
168124

169-
if err := rl.rotate(filename); err != nil {
170-
// Failure to rotate is a problem, but it's really not a great
171-
// idea to stop your application just because you couldn't rename
172-
// your log. For now, we're just going to punt it and write to
173-
// os.Stderr
174-
fmt.Fprintf(os.Stderr, "failed to rotate: %s\n", err)
125+
if err := rl.rotate_nolock(filename); err != nil {
126+
err = errors.Wrap(err, "failed to rotate")
127+
if bailOnRotateFail {
128+
// Failure to rotate is a problem, but it's really not a great
129+
// idea to stop your application just because you couldn't rename
130+
// your log.
131+
// We only return this error when explicitly needed.
132+
return nil, err
133+
}
134+
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
175135
}
176136

177137
rl.outFh.Close()
@@ -209,7 +169,17 @@ func (g *cleanupGuard) Run() {
209169
g.fn()
210170
}
211171

212-
func (rl *RotateLogs) rotate(filename string) error {
172+
// Rotate forcefully rotates the log files.
173+
func (rl *RotateLogs) Rotate() error {
174+
rl.mutex.Lock()
175+
defer rl.mutex.Unlock()
176+
if _, err := rl.getWriter_nolock(true); err != nil {
177+
return err
178+
}
179+
return nil
180+
}
181+
182+
func (rl *RotateLogs) rotate_nolock(filename string) error {
213183
lockfn := filename + `_lock`
214184
fh, err := os.OpenFile(lockfn, os.O_CREATE|os.O_EXCL, 0644)
215185
if err != nil {
@@ -274,11 +244,11 @@ func (rl *RotateLogs) rotate(filename string) error {
274244

275245
if rl.rotationCount > 0 {
276246
// Only delete if we have more than rotationCount
277-
if rl.rotationCount >= len(toUnlink) {
247+
if rl.rotationCount >= uint(len(toUnlink)) {
278248
return nil
279249
}
280250

281-
toUnlink = toUnlink[:len(toUnlink)-rl.rotationCount]
251+
toUnlink = toUnlink[:len(toUnlink)-int(rl.rotationCount)]
282252
}
283253

284254
if len(toUnlink) <= 0 {

0 commit comments

Comments
 (0)