gitee.com/lh-her-team/common@v1.5.1/log/file-rotatelogs/rotatelogs.go (about)

     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  	pattern, err := strftime.New(p)
    33  	if err != nil {
    34  		return nil, errors.Wrap(err, `invalid strftime pattern`)
    35  	}
    36  	rl := &RotateLogs{}
    37  	rl.apply(globPattern, pattern, options...)
    38  	if rl.maxAge > 0 && rl.rotationCount > 0 {
    39  		return nil, errors.New("options MaxAge and RotationCount cannot be both set")
    40  	}
    41  	if rl.maxAge == 0 && rl.rotationCount == 0 {
    42  		// if both are 0, give maxAge a sane default
    43  		rl.maxAge = 7 * 24 * time.Hour
    44  	}
    45  	return rl, nil
    46  }
    47  
    48  func (rl *RotateLogs) apply(globPattern string, pattern *strftime.Strftime, options ...Option) {
    49  	var (
    50  		rotationSize  int64
    51  		rotationCount uint
    52  		linkName      string
    53  		maxAge        time.Duration
    54  		handler       Handler
    55  		forceNewFile  bool
    56  		clock         Clock = Local
    57  		rotationTime        = 24 * time.Hour
    58  	)
    59  	for _, o := range options {
    60  		switch o.Name() {
    61  		case optkeyClock:
    62  			clock, _ = o.Value().(Clock)
    63  		case optkeyLinkName:
    64  			linkName, _ = o.Value().(string)
    65  		case optkeyMaxAge:
    66  			maxAge, _ = o.Value().(time.Duration)
    67  			if maxAge < 0 {
    68  				maxAge = 0
    69  			}
    70  		case optkeyRotationTime:
    71  			rotationTime, _ = o.Value().(time.Duration)
    72  			if rotationTime < 0 {
    73  				rotationTime = 0
    74  			}
    75  		case optkeyRotationSize:
    76  			rotationSize, _ = o.Value().(int64)
    77  			if rotationSize < 0 {
    78  				rotationSize = 0
    79  			}
    80  		case optkeyRotationCount:
    81  			rotationCount, _ = o.Value().(uint)
    82  		case optkeyHandler:
    83  			handler, _ = o.Value().(Handler)
    84  		case optkeyForceNewFile:
    85  			forceNewFile = true
    86  		}
    87  	}
    88  	rl.clock = clock
    89  	rl.eventHandler = handler
    90  	rl.globPattern = globPattern
    91  	rl.linkName = linkName
    92  	rl.maxAge = maxAge
    93  	rl.pattern = pattern
    94  	rl.rotationTime = rotationTime
    95  	rl.rotationSize = rotationSize
    96  	rl.rotationCount = rotationCount
    97  	rl.forceNewFile = forceNewFile
    98  }
    99  
   100  func (rl *RotateLogs) genFilename() string {
   101  	now := rl.clock.Now()
   102  	// XXX HACK: Truncate only happens in UTC semantics, apparently.
   103  	// observed values for truncating given time with 86400 secs:
   104  	//
   105  	// before truncation: 2018/06/01 03:54:54 2018-06-01T03:18:00+09:00
   106  	// after  truncation: 2018/06/01 03:54:54 2018-05-31T09:00:00+09:00
   107  	//
   108  	// This is really annoying when we want to truncate in local time
   109  	// so we hack: we take the apparent local time in the local zone,
   110  	// and pretend that it's in UTC. do our math, and put it back to
   111  	// the local zone
   112  	var base time.Time
   113  	if now.Location() != time.UTC {
   114  		base = time.Date(
   115  			now.Year(),
   116  			now.Month(),
   117  			now.Day(),
   118  			now.Hour(),
   119  			now.Minute(),
   120  			now.Second(),
   121  			now.Nanosecond(),
   122  			time.UTC,
   123  		)
   124  		base = base.Truncate(time.Duration(rl.rotationTime))
   125  		base = time.Date(
   126  			base.Year(),
   127  			base.Month(),
   128  			base.Day(),
   129  			base.Hour(),
   130  			base.Minute(),
   131  			base.Second(),
   132  			base.Nanosecond(),
   133  			base.Location(),
   134  		)
   135  	} else {
   136  		base = now.Truncate(time.Duration(rl.rotationTime))
   137  	}
   138  	return rl.pattern.FormatString(base)
   139  }
   140  
   141  // Write satisfies the io.Writer interface. It writes to the
   142  // appropriate file handle that is currently being used.
   143  // If we have reached rotation time, the target file gets
   144  // automatically rotated, and also purged if necessary.
   145  func (rl *RotateLogs) Write(p []byte) (n int, err error) {
   146  	// Guard against concurrent writes
   147  	rl.mutex.Lock()
   148  	defer rl.mutex.Unlock()
   149  	out, err := rl.getWriterNoLock(false, false)
   150  	if err != nil {
   151  		return 0, errors.Wrap(err, `failed to acquite target io.Writer`)
   152  	}
   153  	return out.Write(p)
   154  }
   155  
   156  // must be locked during this operation
   157  func (rl *RotateLogs) getWriterNoLock(bailOnRotateFail, useGenerationalNames bool) (io.Writer, error) {
   158  	var (
   159  		baseFn               = rl.genFilename()
   160  		previousFn           = rl.curFn
   161  		filename, generation = rl.findNextFile(baseFn, useGenerationalNames)
   162  	)
   163  	if len(filename) == 0 {
   164  		return rl.outFh, nil
   165  	}
   166  	// make sure the dir is existed, eg:
   167  	// ./foo/bar/baz/hello.log must make sure ./foo/bar/baz is existed
   168  	dirname := filepath.Dir(filename)
   169  	if err := os.MkdirAll(dirname, 0755); err != nil {
   170  		return nil, errors.Wrapf(err, "failed to create directory %s", dirname)
   171  	}
   172  	// if we got here, then we need to create a file
   173  	fh, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
   174  	if err != nil {
   175  		return nil, errors.Errorf("failed to open file %s: %s", rl.pattern, err)
   176  	}
   177  	if err := rl.rotateNoLock(filename); err != nil {
   178  		err = errors.Wrap(err, "failed to rotate")
   179  		if bailOnRotateFail {
   180  			// Failure to rotate is a problem, but it's really not a great
   181  			// idea to stop your application just because you couldn't rename
   182  			// your log.
   183  			//
   184  			// We only return this error when explicitly needed (as specified by bailOnRotateFail)
   185  			//
   186  			// However, we *NEED* to close `fh` here
   187  			if fh != nil { // probably can't happen, but being paranoid
   188  				fh.Close()
   189  			}
   190  			return nil, err
   191  		}
   192  	}
   193  	rl.outFh.Close()
   194  	rl.outFh = fh
   195  	rl.curBaseFn = baseFn
   196  	rl.curFn = filename
   197  	rl.generation = generation
   198  	if h := rl.eventHandler; h != nil {
   199  		go h.Handle(&FileRotatedEvent{
   200  			prev:    previousFn,
   201  			current: filename,
   202  		})
   203  	}
   204  	return fh, nil
   205  }
   206  
   207  func (rl *RotateLogs) findNextFile(baseFn string, useGenerationalNames bool) (string, int) {
   208  	var (
   209  		forceNewFile bool
   210  		generation   = rl.generation
   211  		filename     = baseFn
   212  		sizeRotation = false
   213  	)
   214  	fi, err := os.Stat(rl.curFn)
   215  	if err == nil && rl.rotationSize > 0 && rl.rotationSize <= fi.Size() {
   216  		forceNewFile = true
   217  		sizeRotation = true
   218  	}
   219  	if baseFn != rl.curBaseFn {
   220  		generation = 0
   221  		// even though this is the first write after calling New(),
   222  		// check if a new file needs to be created
   223  		if rl.forceNewFile {
   224  			forceNewFile = true
   225  		}
   226  	} else {
   227  		if !useGenerationalNames && !sizeRotation {
   228  			// nothing to do
   229  			return "", -1
   230  		}
   231  		forceNewFile = true
   232  		generation++
   233  	}
   234  	if forceNewFile {
   235  		// A new file has been requested. Instead of just using the
   236  		// regular strftime pattern, we create a new file name using
   237  		// generational names such as "foo.1", "foo.2", "foo.3", etc
   238  		var name string
   239  		for {
   240  			if generation == 0 {
   241  				name = filename
   242  			} else {
   243  				name = fmt.Sprintf("%s.%d", filename, generation)
   244  			}
   245  			if _, err := os.Stat(name); err != nil {
   246  				filename = name
   247  				break
   248  			}
   249  			generation++
   250  		}
   251  	}
   252  	return filename, generation
   253  }
   254  
   255  // CurrentFileName returns the current file name that
   256  // the RotateLogs object is writing to
   257  func (rl *RotateLogs) CurrentFileName() string {
   258  	rl.mutex.RLock()
   259  	defer rl.mutex.RUnlock()
   260  	return rl.curFn
   261  }
   262  
   263  var patternConversionRegexps = []*regexp.Regexp{
   264  	regexp.MustCompile(`%[%+A-Za-z]`),
   265  	regexp.MustCompile(`\*+`),
   266  }
   267  
   268  type cleanupGuard struct {
   269  	enable bool
   270  	fn     func()
   271  	mutex  sync.Mutex
   272  }
   273  
   274  func (g *cleanupGuard) Enable() {
   275  	g.mutex.Lock()
   276  	defer g.mutex.Unlock()
   277  	g.enable = true
   278  }
   279  func (g *cleanupGuard) Run() {
   280  	g.fn()
   281  }
   282  
   283  // Rotate forcefully rotates the log files. If the generated file name
   284  // clash because file already exists, a numeric suffix of the form
   285  // ".1", ".2", ".3" and so forth are appended to the end of the log file
   286  //
   287  // Thie method can be used in conjunction with a signal handler so to
   288  // emulate servers that generate new log files when they receive a
   289  // SIGHUP
   290  func (rl *RotateLogs) Rotate() error {
   291  	rl.mutex.Lock()
   292  	defer rl.mutex.Unlock()
   293  	if _, err := rl.getWriterNoLock(true, true); err != nil {
   294  		return err
   295  	}
   296  	return nil
   297  }
   298  
   299  func (rl *RotateLogs) rotateNoLock(filename string) error {
   300  	lockfn := filename + `_lock`
   301  	fh, err := os.OpenFile(lockfn, os.O_CREATE|os.O_EXCL, 0644)
   302  	if err != nil {
   303  		// Can't lock, just return
   304  		return err
   305  	}
   306  	var guard = cleanupGuard{
   307  		fn: func() {
   308  			fh.Close()
   309  			os.Remove(lockfn)
   310  		},
   311  	}
   312  	defer guard.Run()
   313  	if err = rl.linkFile(filename); err != nil {
   314  		return err
   315  	}
   316  	if rl.maxAge <= 0 && rl.rotationCount <= 0 {
   317  		return errors.New("panic: maxAge and rotationCount are both set")
   318  	}
   319  	matches, err := filepath.Glob(rl.globPattern)
   320  	if err != nil {
   321  		return err
   322  	}
   323  	cutoff := rl.clock.Now().Add(-1 * rl.maxAge)
   324  	toUnlink := rl.getUnLinkFiles(matches, cutoff)
   325  	if len(toUnlink) <= 0 {
   326  		return nil
   327  	}
   328  	guard.Enable()
   329  	go func() {
   330  		// unlink files on a separate goroutine
   331  		for _, path := range toUnlink {
   332  			os.Remove(path)
   333  		}
   334  	}()
   335  	return nil
   336  }
   337  
   338  func (rl *RotateLogs) linkFile(filename string) error {
   339  	if rl.linkName != "" {
   340  		tmpLinkName := filename + `_symlink`
   341  		// Change how the link name is generated based on where the
   342  		// target location is. if the location is directly underneath
   343  		// the main filename's parent directory, then we create a
   344  		// symlink with a relative path
   345  		var (
   346  			linkDest = filename
   347  			linkDir  = filepath.Dir(rl.linkName)
   348  			baseDir  = filepath.Dir(filename)
   349  		)
   350  		if strings.Contains(rl.linkName, baseDir) {
   351  			tmp, err := filepath.Rel(linkDir, filename)
   352  			if err != nil {
   353  				return errors.Wrapf(err, `failed to evaluate relative path from %#v to %#v`, baseDir, rl.linkName)
   354  			}
   355  			linkDest = tmp
   356  		}
   357  		if err := os.Symlink(linkDest, tmpLinkName); err != nil {
   358  			return errors.Wrap(err, `failed to create new symlink`)
   359  		}
   360  		// the directory where rl.linkName should be created must exist
   361  		if _, err := os.Stat(linkDir); err != nil { // Assume err != nil means the directory doesn't exist
   362  			if err := os.MkdirAll(linkDir, 0755); err != nil {
   363  				return errors.Wrapf(err, `failed to create directory %s`, linkDir)
   364  			}
   365  		}
   366  		if err := os.Rename(tmpLinkName, rl.linkName); err != nil {
   367  			return errors.Wrap(err, `failed to rename new symlink`)
   368  		}
   369  	}
   370  	return nil
   371  }
   372  
   373  func (rl *RotateLogs) getUnLinkFiles(matches []string, cutoff time.Time) []string {
   374  	var toUnlink []string
   375  	for _, path := range matches {
   376  		// Ignore lock files
   377  		if strings.HasSuffix(path, "_lock") || strings.HasSuffix(path, "_symlink") {
   378  			continue
   379  		}
   380  		fi, err := os.Stat(path)
   381  		if err != nil {
   382  			continue
   383  		}
   384  		fl, err := os.Lstat(path)
   385  		if err != nil {
   386  			continue
   387  		}
   388  		if rl.maxAge > 0 && fi.ModTime().After(cutoff) {
   389  			continue
   390  		}
   391  		if rl.rotationCount > 0 && fl.Mode()&os.ModeSymlink == os.ModeSymlink {
   392  			continue
   393  		}
   394  		toUnlink = append(toUnlink, path)
   395  	}
   396  	if rl.rotationCount > 0 {
   397  		// Only delete if we have more than rotationCount
   398  		if rl.rotationCount >= uint(len(toUnlink)) {
   399  			return nil
   400  		}
   401  
   402  		toUnlink = toUnlink[:len(toUnlink)-int(rl.rotationCount)]
   403  	}
   404  	return toUnlink
   405  }
   406  
   407  // Close satisfies the io.Closer interface. You must
   408  // call this method if you performed any writes to
   409  // the object.
   410  func (rl *RotateLogs) Close() error {
   411  	rl.mutex.Lock()
   412  	defer rl.mutex.Unlock()
   413  	if rl.outFh == nil {
   414  		return nil
   415  	}
   416  	rl.outFh.Close()
   417  	rl.outFh = nil
   418  	return nil
   419  }