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