1 // package rotatelogs is a port of File-RotateLogs from Perl
2 // (https://metacpan.org/release/File-RotateLogs), and it allows
3 // you to automatically rotate output files when you write to them
4 // according to the filename pattern that you can specify.
17 strftime "github.com/lestrrat-go/strftime"
18 "github.com/pkg/errors"
21 func (c clockFn) Now() time.Time {
25 // New creates a new RotateLogs object. A log filename pattern
26 // must be passed. Optional `Option` parameters may be passed
27 func New(p string, options ...Option) (*RotateLogs, error) {
29 for _, re := range patternConversionRegexps {
30 globPattern = re.ReplaceAllString(globPattern, "*")
33 pattern, err := strftime.New(p)
35 return nil, errors.Wrap(err, `invalid strftime pattern`)
38 var clock Clock = Local
39 rotationTime := 24 * time.Hour
40 var rotationCount uint
42 var maxAge time.Duration
46 for _, o := range options {
49 clock = o.Value().(Clock)
51 linkName = o.Value().(string)
53 maxAge = o.Value().(time.Duration)
57 case optkeyRotationTime:
58 rotationTime = o.Value().(time.Duration)
62 case optkeyRotationCount:
63 rotationCount = o.Value().(uint)
65 handler = o.Value().(Handler)
66 case optkeyForceNewFile:
71 if maxAge > 0 && rotationCount > 0 {
72 return nil, errors.New("options MaxAge and RotationCount cannot be both set")
75 if maxAge == 0 && rotationCount == 0 {
76 // if both are 0, give maxAge a sane default
77 maxAge = 7 * 24 * time.Hour
82 eventHandler: handler,
83 globPattern: globPattern,
87 rotationTime: rotationTime,
88 rotationCount: rotationCount,
89 forceNewFile: forceNewFile,
93 func (rl *RotateLogs) genFilename() string {
96 // XXX HACK: Truncate only happens in UTC semantics, apparently.
97 // observed values for truncating given time with 86400 secs:
99 // before truncation: 2018/06/01 03:54:54 2018-06-01T03:18:00+09:00
100 // after truncation: 2018/06/01 03:54:54 2018-05-31T09:00:00+09:00
102 // This is really annoying when we want to truncate in local time
103 // so we hack: we take the apparent local time in the local zone,
104 // and pretend that it's in UTC. do our math, and put it back to
107 if now.Location() != time.UTC {
108 base = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), now.Nanosecond(), time.UTC)
109 base = base.Truncate(time.Duration(rl.rotationTime))
110 base = time.Date(base.Year(), base.Month(), base.Day(), base.Hour(), base.Minute(), base.Second(), base.Nanosecond(), base.Location())
112 base = now.Truncate(time.Duration(rl.rotationTime))
114 return rl.pattern.FormatString(base)
117 // Write satisfies the io.Writer interface. It writes to the
118 // appropriate file handle that is currently being used.
119 // If we have reached rotation time, the target file gets
120 // automatically rotated, and also purged if necessary.
121 func (rl *RotateLogs) Write(p []byte) (n int, err error) {
122 // Guard against concurrent writes
124 defer rl.mutex.Unlock()
126 out, err := rl.getWriter_nolock(false, false)
128 return 0, errors.Wrap(err, `failed to acquite target io.Writer`)
134 // must be locked during this operation
135 func (rl *RotateLogs) getWriter_nolock(bailOnRotateFail, useGenerationalNames bool) (io.Writer, error) {
136 generation := rl.generation
137 previousFn := rl.curFn
138 // This filename contains the name of the "NEW" filename
139 // to log to, which may be newer than rl.currentFilename
140 baseFn := rl.genFilename()
142 var forceNewFile bool
143 if baseFn != rl.curBaseFn {
145 // even though this is the first write after calling New(),
146 // check if a new file needs to be created
151 if !useGenerationalNames {
159 // A new file has been requested. Instead of just using the
160 // regular strftime pattern, we create a new file name using
161 // generational names such as "foo.1", "foo.2", "foo.3", etc
167 name = fmt.Sprintf("%s.%d", filename, generation)
169 if _, err := os.Stat(name); err != nil {
176 // make sure the dir is existed, eg:
177 // ./foo/bar/baz/hello.log must make sure ./foo/bar/baz is existed
178 dirname := filepath.Dir(filename)
179 if err := os.MkdirAll(dirname, 0755); err != nil {
180 return nil, errors.Wrapf(err, "failed to create directory %s", dirname)
182 // if we got here, then we need to create a file
183 fh, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
185 return nil, errors.Errorf("failed to open file %s: %s", rl.pattern, err)
188 if err := rl.rotate_nolock(filename); err != nil {
189 err = errors.Wrap(err, "failed to rotate")
190 if bailOnRotateFail {
191 // Failure to rotate is a problem, but it's really not a great
192 // idea to stop your application just because you couldn't rename
195 // We only return this error when explicitly needed (as specified by bailOnRotateFail)
197 // However, we *NEED* to close `fh` here
198 if fh != nil { // probably can't happen, but being paranoid
203 fmt.Fprintf(os.Stderr, "%s\n", err.Error())
208 rl.curBaseFn = baseFn
210 rl.generation = generation
212 if h := rl.eventHandler; h != nil {
213 go h.Handle(&FileRotatedEvent{
221 // CurrentFileName returns the current file name that
222 // the RotateLogs object is writing to
223 func (rl *RotateLogs) CurrentFileName() string {
225 defer rl.mutex.RUnlock()
229 var patternConversionRegexps = []*regexp.Regexp{
230 regexp.MustCompile(`%[%+A-Za-z]`),
231 regexp.MustCompile(`\*+`),
234 type cleanupGuard struct {
240 func (g *cleanupGuard) Enable() {
242 defer g.mutex.Unlock()
245 func (g *cleanupGuard) Run() {
249 // Rotate forcefully rotates the log files. If the generated file name
250 // clash because file already exists, a numeric suffix of the form
251 // ".1", ".2", ".3" and so forth are appended to the end of the log file
253 // Thie method can be used in conjunction with a signal handler so to
254 // emulate servers that generate new log files when they receive a
256 func (rl *RotateLogs) Rotate() error {
258 defer rl.mutex.Unlock()
259 if _, err := rl.getWriter_nolock(true, true); err != nil {
265 func (rl *RotateLogs) rotate_nolock(filename string) error {
266 lockfn := filename + `_lock`
267 fh, err := os.OpenFile(lockfn, os.O_CREATE|os.O_EXCL, 0644)
269 // Can't lock, just return
273 var guard cleanupGuard
280 if rl.linkName != "" {
281 tmpLinkName := filename + `_symlink`
282 if err := os.Symlink(filename, tmpLinkName); err != nil {
283 return errors.Wrap(err, `failed to create new symlink`)
286 if err := os.Rename(tmpLinkName, rl.linkName); err != nil {
287 return errors.Wrap(err, `failed to rename new symlink`)
291 if rl.maxAge <= 0 && rl.rotationCount <= 0 {
292 return errors.New("panic: maxAge and rotationCount are both set")
295 matches, err := filepath.Glob(rl.globPattern)
300 cutoff := rl.clock.Now().Add(-1 * rl.maxAge)
301 var toUnlink []string
302 for _, path := range matches {
304 if strings.HasSuffix(path, "_lock") || strings.HasSuffix(path, "_symlink") {
308 fi, err := os.Stat(path)
313 fl, err := os.Lstat(path)
318 if rl.maxAge > 0 && fi.ModTime().After(cutoff) {
322 if rl.rotationCount > 0 && fl.Mode()&os.ModeSymlink == os.ModeSymlink {
325 toUnlink = append(toUnlink, path)
328 if rl.rotationCount > 0 {
329 // Only delete if we have more than rotationCount
330 if rl.rotationCount >= uint(len(toUnlink)) {
334 toUnlink = toUnlink[:len(toUnlink)-int(rl.rotationCount)]
337 if len(toUnlink) <= 0 {
343 // unlink files on a separate goroutine
344 for _, path := range toUnlink {
352 // Close satisfies the io.Closer interface. You must
353 // call this method if you performed any writes to
355 func (rl *RotateLogs) Close() error {
357 defer rl.mutex.Unlock()