OSDN Git Service

log into file (#357)
[bytom/vapor.git] / vendor / github.com / lestrrat-go / file-rotatelogs / rotatelogs.go
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.
5 package rotatelogs
6
7 import (
8         "fmt"
9         "io"
10         "os"
11         "path/filepath"
12         "regexp"
13         "strings"
14         "sync"
15         "time"
16
17         strftime "github.com/lestrrat-go/strftime"
18         "github.com/pkg/errors"
19 )
20
21 func (c clockFn) Now() time.Time {
22         return c()
23 }
24
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) {
28         globPattern := p
29         for _, re := range patternConversionRegexps {
30                 globPattern = re.ReplaceAllString(globPattern, "*")
31         }
32
33         pattern, err := strftime.New(p)
34         if err != nil {
35                 return nil, errors.Wrap(err, `invalid strftime pattern`)
36         }
37
38         var clock Clock = Local
39         rotationTime := 24 * time.Hour
40         var rotationCount uint
41         var linkName string
42         var maxAge time.Duration
43         var handler Handler
44         var forceNewFile bool
45
46         for _, o := range options {
47                 switch o.Name() {
48                 case optkeyClock:
49                         clock = o.Value().(Clock)
50                 case optkeyLinkName:
51                         linkName = o.Value().(string)
52                 case optkeyMaxAge:
53                         maxAge = o.Value().(time.Duration)
54                         if maxAge < 0 {
55                                 maxAge = 0
56                         }
57                 case optkeyRotationTime:
58                         rotationTime = o.Value().(time.Duration)
59                         if rotationTime < 0 {
60                                 rotationTime = 0
61                         }
62                 case optkeyRotationCount:
63                         rotationCount = o.Value().(uint)
64                 case optkeyHandler:
65                         handler = o.Value().(Handler)
66                 case optkeyForceNewFile:
67                         forceNewFile = true
68                 }
69         }
70
71         if maxAge > 0 && rotationCount > 0 {
72                 return nil, errors.New("options MaxAge and RotationCount cannot be both set")
73         }
74
75         if maxAge == 0 && rotationCount == 0 {
76                 // if both are 0, give maxAge a sane default
77                 maxAge = 7 * 24 * time.Hour
78         }
79
80         return &RotateLogs{
81                 clock:         clock,
82                 eventHandler:  handler,
83                 globPattern:   globPattern,
84                 linkName:      linkName,
85                 maxAge:        maxAge,
86                 pattern:       pattern,
87                 rotationTime:  rotationTime,
88                 rotationCount: rotationCount,
89                 forceNewFile:  forceNewFile,
90         }, nil
91 }
92
93 func (rl *RotateLogs) genFilename() string {
94         now := rl.clock.Now()
95
96         // XXX HACK: Truncate only happens in UTC semantics, apparently.
97         // observed values for truncating given time with 86400 secs:
98         //
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
101         //
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
105         // the local zone
106         var base time.Time
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())
111         } else {
112                 base = now.Truncate(time.Duration(rl.rotationTime))
113         }
114         return rl.pattern.FormatString(base)
115 }
116
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
123         rl.mutex.Lock()
124         defer rl.mutex.Unlock()
125
126         out, err := rl.getWriter_nolock(false, false)
127         if err != nil {
128                 return 0, errors.Wrap(err, `failed to acquite target io.Writer`)
129         }
130
131         return out.Write(p)
132 }
133
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()
141         filename := baseFn
142         var forceNewFile bool
143         if baseFn != rl.curBaseFn {
144                 generation = 0
145                 // even though this is the first write after calling New(),
146                 // check if a new file needs to be created
147                 if rl.forceNewFile {
148                         forceNewFile = true
149                 }
150         } else {
151                 if !useGenerationalNames {
152                         // nothing to do
153                         return rl.outFh, nil
154                 }
155                 forceNewFile = true
156                 generation++
157         }
158         if forceNewFile {
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
162                 var name string
163                 for {
164                         if generation == 0 {
165                                 name = filename
166                         } else {
167                                 name = fmt.Sprintf("%s.%d", filename, generation)
168                         }
169                         if _, err := os.Stat(name); err != nil {
170                                 filename = name
171                                 break
172                         }
173                         generation++
174                 }
175         }
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)
181         }
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)
184         if err != nil {
185                 return nil, errors.Errorf("failed to open file %s: %s", rl.pattern, err)
186         }
187
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
193                         // your log.
194                         //
195                         // We only return this error when explicitly needed (as specified by bailOnRotateFail)
196                         //
197                         // However, we *NEED* to close `fh` here
198                         if fh != nil { // probably can't happen, but being paranoid
199                                 fh.Close()
200                         }
201                         return nil, err
202                 }
203                 fmt.Fprintf(os.Stderr, "%s\n", err.Error())
204         }
205
206         rl.outFh.Close()
207         rl.outFh = fh
208         rl.curBaseFn = baseFn
209         rl.curFn = filename
210         rl.generation = generation
211
212         if h := rl.eventHandler; h != nil {
213                 go h.Handle(&FileRotatedEvent{
214                         prev:    previousFn,
215                         current: filename,
216                 })
217         }
218         return fh, nil
219 }
220
221 // CurrentFileName returns the current file name that
222 // the RotateLogs object is writing to
223 func (rl *RotateLogs) CurrentFileName() string {
224         rl.mutex.RLock()
225         defer rl.mutex.RUnlock()
226         return rl.curFn
227 }
228
229 var patternConversionRegexps = []*regexp.Regexp{
230         regexp.MustCompile(`%[%+A-Za-z]`),
231         regexp.MustCompile(`\*+`),
232 }
233
234 type cleanupGuard struct {
235         enable bool
236         fn     func()
237         mutex  sync.Mutex
238 }
239
240 func (g *cleanupGuard) Enable() {
241         g.mutex.Lock()
242         defer g.mutex.Unlock()
243         g.enable = true
244 }
245 func (g *cleanupGuard) Run() {
246         g.fn()
247 }
248
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
252 //
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
255 // SIGHUP
256 func (rl *RotateLogs) Rotate() error {
257         rl.mutex.Lock()
258         defer rl.mutex.Unlock()
259         if _, err := rl.getWriter_nolock(true, true); err != nil {
260                 return err
261         }
262         return nil
263 }
264
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)
268         if err != nil {
269                 // Can't lock, just return
270                 return err
271         }
272
273         var guard cleanupGuard
274         guard.fn = func() {
275                 fh.Close()
276                 os.Remove(lockfn)
277         }
278         defer guard.Run()
279
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`)
284                 }
285
286                 if err := os.Rename(tmpLinkName, rl.linkName); err != nil {
287                         return errors.Wrap(err, `failed to rename new symlink`)
288                 }
289         }
290
291         if rl.maxAge <= 0 && rl.rotationCount <= 0 {
292                 return errors.New("panic: maxAge and rotationCount are both set")
293         }
294
295         matches, err := filepath.Glob(rl.globPattern)
296         if err != nil {
297                 return err
298         }
299
300         cutoff := rl.clock.Now().Add(-1 * rl.maxAge)
301         var toUnlink []string
302         for _, path := range matches {
303                 // Ignore lock files
304                 if strings.HasSuffix(path, "_lock") || strings.HasSuffix(path, "_symlink") {
305                         continue
306                 }
307
308                 fi, err := os.Stat(path)
309                 if err != nil {
310                         continue
311                 }
312
313                 fl, err := os.Lstat(path)
314                 if err != nil {
315                         continue
316                 }
317
318                 if rl.maxAge > 0 && fi.ModTime().After(cutoff) {
319                         continue
320                 }
321
322                 if rl.rotationCount > 0 && fl.Mode()&os.ModeSymlink == os.ModeSymlink {
323                         continue
324                 }
325                 toUnlink = append(toUnlink, path)
326         }
327
328         if rl.rotationCount > 0 {
329                 // Only delete if we have more than rotationCount
330                 if rl.rotationCount >= uint(len(toUnlink)) {
331                         return nil
332                 }
333
334                 toUnlink = toUnlink[:len(toUnlink)-int(rl.rotationCount)]
335         }
336
337         if len(toUnlink) <= 0 {
338                 return nil
339         }
340
341         guard.Enable()
342         go func() {
343                 // unlink files on a separate goroutine
344                 for _, path := range toUnlink {
345                         os.Remove(path)
346                 }
347         }()
348
349         return nil
350 }
351
352 // Close satisfies the io.Closer interface. You must
353 // call this method if you performed any writes to
354 // the object.
355 func (rl *RotateLogs) Close() error {
356         rl.mutex.Lock()
357         defer rl.mutex.Unlock()
358
359         if rl.outFh == nil {
360                 return nil
361         }
362
363         rl.outFh.Close()
364         rl.outFh = nil
365         return nil
366 }