123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 |
- // package rotatelogs is a port of File-RotateLogs from Perl
- // (https://metacpan.org/release/File-RotateLogs), and it allows
- // you to automatically rotate output files when you write to them
- // according to the filename pattern that you can specify.
- package rotatelogs
- import (
- "fmt"
- "io"
- "os"
- "path/filepath"
- "regexp"
- "strings"
- "sync"
- "time"
- strftime "github.com/lestrrat-go/strftime"
- "github.com/pkg/errors"
- )
- func (c clockFn) Now() time.Time {
- return c()
- }
- // New creates a new RotateLogs object. A log filename pattern
- // must be passed. Optional `Option` parameters may be passed
- func New(p string, options ...Option) (*RotateLogs, error) {
- globPattern := p
- for _, re := range patternConversionRegexps {
- globPattern = re.ReplaceAllString(globPattern, "*")
- }
- pattern, err := strftime.New(p)
- if err != nil {
- return nil, errors.Wrap(err, `invalid strftime pattern`)
- }
- var clock Clock = Local
- rotationTime := 24 * time.Hour
- var rotationSize int64
- var rotationCount uint
- var linkName string
- var maxAge time.Duration
- var handler Handler
- var forceNewFile bool
- for _, o := range options {
- switch o.Name() {
- case optkeyClock:
- clock = o.Value().(Clock)
- case optkeyLinkName:
- linkName = o.Value().(string)
- case optkeyMaxAge:
- maxAge = o.Value().(time.Duration)
- if maxAge < 0 {
- maxAge = 0
- }
- case optkeyRotationTime:
- rotationTime = o.Value().(time.Duration)
- if rotationTime < 0 {
- rotationTime = 0
- }
- case optkeyRotationSize:
- rotationSize = o.Value().(int64)
- if rotationSize < 0 {
- rotationSize = 0
- }
- case optkeyRotationCount:
- rotationCount = o.Value().(uint)
- case optkeyHandler:
- handler = o.Value().(Handler)
- case optkeyForceNewFile:
- forceNewFile = true
- }
- }
- if maxAge > 0 && rotationCount > 0 {
- return nil, errors.New("options MaxAge and RotationCount cannot be both set")
- }
- if maxAge == 0 && rotationCount == 0 {
- // if both are 0, give maxAge a sane default
- maxAge = 7 * 24 * time.Hour
- }
- return &RotateLogs{
- clock: clock,
- eventHandler: handler,
- globPattern: globPattern,
- linkName: linkName,
- maxAge: maxAge,
- pattern: pattern,
- rotationTime: rotationTime,
- rotationSize: rotationSize,
- rotationCount: rotationCount,
- forceNewFile: forceNewFile,
- }, nil
- }
- func (rl *RotateLogs) genFilename() string {
- now := rl.clock.Now()
- // XXX HACK: Truncate only happens in UTC semantics, apparently.
- // observed values for truncating given time with 86400 secs:
- //
- // before truncation: 2018/06/01 03:54:54 2018-06-01T03:18:00+09:00
- // after truncation: 2018/06/01 03:54:54 2018-05-31T09:00:00+09:00
- //
- // This is really annoying when we want to truncate in local time
- // so we hack: we take the apparent local time in the local zone,
- // and pretend that it's in UTC. do our math, and put it back to
- // the local zone
- var base time.Time
- if now.Location() != time.UTC {
- base = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), now.Nanosecond(), time.UTC)
- base = base.Truncate(time.Duration(rl.rotationTime))
- base = time.Date(base.Year(), base.Month(), base.Day(), base.Hour(), base.Minute(), base.Second(), base.Nanosecond(), base.Location())
- } else {
- base = now.Truncate(time.Duration(rl.rotationTime))
- }
- return rl.pattern.FormatString(base)
- }
- // Write satisfies the io.Writer interface. It writes to the
- // appropriate file handle that is currently being used.
- // If we have reached rotation time, the target file gets
- // automatically rotated, and also purged if necessary.
- func (rl *RotateLogs) Write(p []byte) (n int, err error) {
- // Guard against concurrent writes
- rl.mutex.Lock()
- defer rl.mutex.Unlock()
- out, err := rl.getWriter_nolock(false, false)
- if err != nil {
- return 0, errors.Wrap(err, `failed to acquite target io.Writer`)
- }
- return out.Write(p)
- }
- // must be locked during this operation
- func (rl *RotateLogs) getWriter_nolock(bailOnRotateFail, useGenerationalNames bool) (io.Writer, error) {
- generation := rl.generation
- previousFn := rl.curFn
- // This filename contains the name of the "NEW" filename
- // to log to, which may be newer than rl.currentFilename
- baseFn := rl.genFilename()
- filename := baseFn
- var forceNewFile bool
- fi, err := os.Stat(rl.curFn)
- sizeRotation := false
- if err == nil && rl.rotationSize > 0 && rl.rotationSize <= fi.Size() {
- forceNewFile = true
- sizeRotation = true
- }
- if baseFn != rl.curBaseFn {
- generation = 0
- // even though this is the first write after calling New(),
- // check if a new file needs to be created
- if rl.forceNewFile {
- forceNewFile = true
- }
- } else {
- if !useGenerationalNames && !sizeRotation {
- // nothing to do
- return rl.outFh, nil
- }
- forceNewFile = true
- generation++
- }
- if forceNewFile {
- // A new file has been requested. Instead of just using the
- // regular strftime pattern, we create a new file name using
- // generational names such as "foo.1", "foo.2", "foo.3", etc
- var name string
- for {
- if generation == 0 {
- name = filename
- } else {
- name = fmt.Sprintf("%s.%d", filename, generation)
- }
- if _, err := os.Stat(name); err != nil {
- filename = name
- break
- }
- generation++
- }
- }
- // make sure the dir is existed, eg:
- // ./foo/bar/baz/hello.log must make sure ./foo/bar/baz is existed
- dirname := filepath.Dir(filename)
- if err := os.MkdirAll(dirname, 0755); err != nil {
- return nil, errors.Wrapf(err, "failed to create directory %s", dirname)
- }
- // if we got here, then we need to create a file
- fh, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
- if err != nil {
- return nil, errors.Errorf("failed to open file %s: %s", rl.pattern, err)
- }
- if err := rl.rotate_nolock(filename); err != nil {
- err = errors.Wrap(err, "failed to rotate")
- if bailOnRotateFail {
- // Failure to rotate is a problem, but it's really not a great
- // idea to stop your application just because you couldn't rename
- // your log.
- //
- // We only return this error when explicitly needed (as specified by bailOnRotateFail)
- //
- // However, we *NEED* to close `fh` here
- if fh != nil { // probably can't happen, but being paranoid
- fh.Close()
- }
- return nil, err
- }
- fmt.Fprintf(os.Stderr, "%s\n", err.Error())
- }
- rl.outFh.Close()
- rl.outFh = fh
- rl.curBaseFn = baseFn
- rl.curFn = filename
- rl.generation = generation
- if h := rl.eventHandler; h != nil {
- go h.Handle(&FileRotatedEvent{
- prev: previousFn,
- current: filename,
- })
- }
- return fh, nil
- }
- // CurrentFileName returns the current file name that
- // the RotateLogs object is writing to
- func (rl *RotateLogs) CurrentFileName() string {
- rl.mutex.RLock()
- defer rl.mutex.RUnlock()
- return rl.curFn
- }
- var patternConversionRegexps = []*regexp.Regexp{
- regexp.MustCompile(`%[%+A-Za-z]`),
- regexp.MustCompile(`\*+`),
- }
- type cleanupGuard struct {
- enable bool
- fn func()
- mutex sync.Mutex
- }
- func (g *cleanupGuard) Enable() {
- g.mutex.Lock()
- defer g.mutex.Unlock()
- g.enable = true
- }
- func (g *cleanupGuard) Run() {
- g.fn()
- }
- // Rotate forcefully rotates the log files. If the generated file name
- // clash because file already exists, a numeric suffix of the form
- // ".1", ".2", ".3" and so forth are appended to the end of the log file
- //
- // Thie method can be used in conjunction with a signal handler so to
- // emulate servers that generate new log files when they receive a
- // SIGHUP
- func (rl *RotateLogs) Rotate() error {
- rl.mutex.Lock()
- defer rl.mutex.Unlock()
- if _, err := rl.getWriter_nolock(true, true); err != nil {
- return err
- }
- return nil
- }
- func (rl *RotateLogs) rotate_nolock(filename string) error {
- lockfn := filename + `_lock`
- fh, err := os.OpenFile(lockfn, os.O_CREATE|os.O_EXCL, 0644)
- if err != nil {
- // Can't lock, just return
- return err
- }
- var guard cleanupGuard
- guard.fn = func() {
- fh.Close()
- os.Remove(lockfn)
- }
- defer guard.Run()
- 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
- 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`)
- }
- }
- if rl.maxAge <= 0 && rl.rotationCount <= 0 {
- return errors.New("panic: maxAge and rotationCount are both set")
- }
- matches, err := filepath.Glob(rl.globPattern)
- if err != nil {
- return err
- }
- cutoff := rl.clock.Now().Add(-1 * rl.maxAge)
- var toUnlink []string
- for _, path := range matches {
- // Ignore lock files
- if strings.HasSuffix(path, "_lock") || strings.HasSuffix(path, "_symlink") {
- continue
- }
- fi, err := os.Stat(path)
- if err != nil {
- continue
- }
- fl, err := os.Lstat(path)
- if err != nil {
- continue
- }
- if rl.maxAge > 0 && fi.ModTime().After(cutoff) {
- continue
- }
- if rl.rotationCount > 0 && fl.Mode()&os.ModeSymlink == os.ModeSymlink {
- continue
- }
- toUnlink = append(toUnlink, path)
- }
- if rl.rotationCount > 0 {
- // Only delete if we have more than rotationCount
- if rl.rotationCount >= uint(len(toUnlink)) {
- return nil
- }
- toUnlink = toUnlink[:len(toUnlink)-int(rl.rotationCount)]
- }
- if len(toUnlink) <= 0 {
- return nil
- }
- guard.Enable()
- go func() {
- // unlink files on a separate goroutine
- for _, path := range toUnlink {
- os.Remove(path)
- }
- }()
- return nil
- }
- // Close satisfies the io.Closer interface. You must
- // call this method if you performed any writes to
- // the object.
- func (rl *RotateLogs) Close() error {
- rl.mutex.Lock()
- defer rl.mutex.Unlock()
- if rl.outFh == nil {
- return nil
- }
- rl.outFh.Close()
- rl.outFh = nil
- return nil
- }
|