github.com/searKing/golang/go@v1.2.74/os/file_rotate.go (about)

     1  // Copyright 2021 The searKing Author. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package os
     6  
     7  import (
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"regexp"
    12  	"sort"
    13  	"strings"
    14  	"sync"
    15  	"time"
    16  
    17  	errors_ "github.com/searKing/golang/go/errors"
    18  	filepath_ "github.com/searKing/golang/go/path/filepath"
    19  	"github.com/searKing/golang/go/sync/atomic"
    20  	time_ "github.com/searKing/golang/go/time"
    21  )
    22  
    23  type RotateMode int
    24  
    25  const (
    26  	// RotateModeNew create new rotate file directly
    27  	RotateModeNew RotateMode = iota
    28  
    29  	// RotateModeCopyRename Make a copy of the log file, but don't change the original at all. This option can be
    30  	// used, for instance, to make a snapshot of the current log file, or when some other
    31  	// utility needs to truncate or parse the file. When this option is used, the create
    32  	// option will have no effect, as the old log file stays in place.
    33  	RotateModeCopyRename RotateMode = iota
    34  
    35  	// RotateModeCopyTruncate Truncate the original log file in place after creating a copy, instead of moving the
    36  	// old log file and optionally creating a new one. It can be used when some program can‐
    37  	// not be told to close its rotatefile and thus might continue writing (appending) to the
    38  	// previous log file forever. Note that there is a very small time slice between copying
    39  	// the file and truncating it, so some logging data might be lost. When this option is
    40  	// used, the create option will have no effect, as the old log file stays in place.
    41  	RotateModeCopyTruncate RotateMode = iota
    42  )
    43  
    44  // RotateFile logrotate reads everything about the log files it should be handling from the series of con‐
    45  // figuration files specified on the command line.  Each configuration file can set global
    46  // options (local definitions override global ones, and later definitions override earlier ones)
    47  // and specify rotatefiles to rotate. A simple configuration file looks like this:
    48  type RotateFile struct {
    49  	RotateMode           RotateMode
    50  	FilePathPrefix       string // FilePath = FilePathPrefix + now.Format(filePathRotateLayout)
    51  	FilePathRotateLayout string // Time layout to format rotate file
    52  
    53  	RotateFileGlob string // file glob to clean
    54  
    55  	// sets the symbolic link name that gets linked to the current file name being used.
    56  	FileLinkPath string
    57  
    58  	// Rotate files are rotated until RotateInterval expired before being removed
    59  	// take effects if only RotateInterval is bigger than 0.
    60  	RotateInterval time.Duration
    61  
    62  	// Rotate files are rotated if they grow bigger then size bytes.
    63  	// take effects if only RotateSize is bigger than 0.
    64  	RotateSize int64
    65  
    66  	// max age of a log file before it gets purged from the file system.
    67  	// Remove rotated logs older than duration. The age is only checked if the file is
    68  	// to be rotated.
    69  	// take effects if only MaxAge is bigger than 0.
    70  	MaxAge time.Duration
    71  
    72  	// Rotate files are rotated MaxCount times before being removed
    73  	// take effects if only MaxCount is bigger than 0.
    74  	MaxCount int
    75  
    76  	// Force File Rotate when start up
    77  	ForceNewFileOnStartup bool
    78  
    79  	// PreRotateHandler called before file rotate
    80  	// name means file path rotated
    81  	PreRotateHandler func(name string)
    82  
    83  	// PostRotateHandler called after file rotate
    84  	// name means file path rotated
    85  	PostRotateHandler func(name string)
    86  
    87  	cleaning      atomic.Bool
    88  	mu            sync.Mutex
    89  	usingSeq      int // file rotated by size limit meet
    90  	usingFilePath string
    91  	usingFile     *os.File
    92  }
    93  
    94  func NewRotateFile(layout string) *RotateFile {
    95  	return NewRotateFileWithStrftime(time_.LayoutTimeToSimilarStrftime(layout))
    96  }
    97  
    98  func NewRotateFileWithStrftime(strftimeLayout string) *RotateFile {
    99  	return &RotateFile{
   100  		FilePathRotateLayout: time_.LayoutStrftimeToSimilarTime(strftimeLayout),
   101  		RotateFileGlob:       fileGlobFromStrftimeLayout(strftimeLayout),
   102  		RotateInterval:       24 * time.Hour,
   103  	}
   104  }
   105  
   106  func fileGlobFromStrftimeLayout(strftimeLayout string) string {
   107  	var regexps = []*regexp.Regexp{
   108  		regexp.MustCompile(`%[%+A-Za-z]`),
   109  		regexp.MustCompile(`\*+`),
   110  	}
   111  	globPattern := strftimeLayout
   112  	for _, re := range regexps {
   113  		globPattern = re.ReplaceAllString(globPattern, "*")
   114  	}
   115  	return globPattern + `*`
   116  }
   117  
   118  func (f *RotateFile) Write(b []byte) (n int, err error) {
   119  	// Guard against concurrent writes
   120  	f.mu.Lock()
   121  	defer f.mu.Unlock()
   122  
   123  	out, err := f.getWriterLocked(false, false)
   124  	if err != nil {
   125  		return 0, fmt.Errorf("acquite rotated file :%w", err)
   126  	}
   127  	if out == nil {
   128  		return 0, nil
   129  	}
   130  
   131  	return out.Write(b)
   132  }
   133  
   134  // WriteString is like Write, but writes the contents of string s rather than
   135  // a slice of bytes.
   136  func (f *RotateFile) WriteString(s string) (n int, err error) {
   137  	return f.Write([]byte(s))
   138  }
   139  
   140  // WriteAt writes len(b) bytes to the File starting at byte offset off.
   141  // It returns the number of bytes written and an error, if any.
   142  // WriteAt returns a non-nil error when n != len(b).
   143  //
   144  // If file was opened with the O_APPEND flag, WriteAt returns an error.
   145  func (f *RotateFile) WriteAt(b []byte, off int64) (n int, err error) {
   146  	// Guard against concurrent writes
   147  	f.mu.Lock()
   148  	defer f.mu.Unlock()
   149  
   150  	return f.WriteAt(b, off)
   151  }
   152  
   153  // Close satisfies the io.Closer interface. You must
   154  // call this method if you performed any writes to
   155  // the object.
   156  func (f *RotateFile) Close() error {
   157  	f.mu.Lock()
   158  	defer f.mu.Unlock()
   159  
   160  	if f.usingFile == nil {
   161  		return nil
   162  	}
   163  	defer f.serializedClean()
   164  
   165  	defer func() { f.usingFile = nil }()
   166  	return f.usingFile.Close()
   167  }
   168  
   169  // Rotate forcefully rotates the file. If the generated file name
   170  // clash because file already exists, a numeric suffix of the form
   171  // ".1", ".2", ".3" and so forth are appended to the end of the log file
   172  //
   173  // This method can be used in conjunction with a signal handler so to
   174  // emulate servers that generate new log files when they receive a SIGHUP
   175  func (f *RotateFile) Rotate(forceRotate bool) error {
   176  	f.mu.Lock()
   177  	defer f.mu.Unlock()
   178  	if _, err := f.getWriterLocked(true, forceRotate); err != nil {
   179  		return err
   180  	}
   181  	return nil
   182  }
   183  
   184  func (f *RotateFile) filePathByRotateTime() string {
   185  	// create a new file name using the regular time layout
   186  	return f.FilePathPrefix + time_.TruncateByLocation(time.Now(), f.RotateInterval).Format(f.FilePathRotateLayout)
   187  }
   188  
   189  func (f *RotateFile) filePathByRotateSize() (name string, seq int) {
   190  	// instead of just using the regular time layout,
   191  	// we create a new file name using names such as "foo.1", "foo.2", "foo.3", etc
   192  	return nextSeqFileName(f.filePathByRotateTime(), f.usingSeq)
   193  }
   194  
   195  func (f *RotateFile) filePathByRotate(forceRotate bool) (name string, seq int, byTime, bySize bool) {
   196  	// name using the regular time layout, without seq
   197  	name = f.filePathByRotateTime()
   198  	// startup
   199  	if f.usingFilePath == "" {
   200  		if f.ForceNewFileOnStartup {
   201  			// instead of just using the regular time layout,
   202  			// we create a new file name using names such as "foo", "foo.1", "foo.2", "foo.3", etc
   203  			name, seq = nextSeqFileName(name, f.usingSeq)
   204  			return name, seq, false, true
   205  		}
   206  		name, seq = maxSeqFileName(name)
   207  		return name, seq, true, false
   208  	}
   209  
   210  	// rotate by time
   211  	// compare expect time with current using file
   212  	if name != trimSeqFromNextFileName(f.usingFilePath, f.usingSeq) {
   213  		if forceRotate {
   214  			// instead of just using the regular time layout,
   215  			// we create a new file name using names such as "foo", "foo.1", "foo.2", "foo.3", etc
   216  			name, seq = nextSeqFileName(name, 0)
   217  			return name, seq, true, false
   218  		}
   219  		name, seq = maxSeqFileName(name)
   220  		return name, seq, true, false
   221  	}
   222  
   223  	// determine if rotate by size
   224  
   225  	// using file not exist, recreate file as rotated by time
   226  	usingFileInfo, err := os.Stat(f.usingFilePath)
   227  	if os.IsNotExist(err) {
   228  		name = f.usingFilePath
   229  		seq = f.usingSeq
   230  		return name, seq, false, false
   231  	}
   232  
   233  	// rotate by size
   234  	// compare rotate size with current using file
   235  	if forceRotate || (err == nil && (f.RotateSize > 0 && usingFileInfo.Size() > f.RotateSize)) {
   236  		// instead of just using the regular time layout,
   237  		// we create a new file name using names such as "foo", "foo.1", "foo.2", "foo.3", etc
   238  		name, seq = nextSeqFileName(name, f.usingSeq)
   239  		return name, seq, false, true
   240  	}
   241  	name = f.usingFilePath
   242  	seq = f.usingSeq
   243  	return name, seq, false, false
   244  }
   245  
   246  func (f *RotateFile) makeUsingFileReadyLocked() error {
   247  	if f.usingFile != nil {
   248  		_, err := os.Stat(f.usingFile.Name())
   249  		if err != nil {
   250  			_ = f.usingFile.Close()
   251  			f.usingFile = nil
   252  		}
   253  	}
   254  
   255  	if f.usingFile == nil {
   256  		file, err := AppendAllIfNotExist(f.usingFilePath)
   257  		if err != nil {
   258  			return err
   259  		}
   260  
   261  		// link -> filename
   262  		if f.FileLinkPath != "" {
   263  			if err := ReSymlink(f.usingFilePath, f.FileLinkPath); err != nil {
   264  				return err
   265  			}
   266  		}
   267  		f.usingFile = file
   268  	}
   269  	return nil
   270  
   271  }
   272  func (f *RotateFile) getWriterLocked(bailOnRotateFail, forceRotate bool) (out io.Writer, err error) {
   273  	newName, newSeq, byTime, bySize := f.filePathByRotate(forceRotate)
   274  	if !byTime && !bySize {
   275  		err = f.makeUsingFileReadyLocked()
   276  		if err != nil {
   277  			if bailOnRotateFail {
   278  				// Failure to rotate is a problem, but it's really not a great
   279  				// idea to stop your application just because you couldn't rename
   280  				// your log.
   281  				//
   282  				// We only return this error when explicitly needed (as specified by bailOnRotateFail)
   283  				//
   284  				// However, we *NEED* to close `fh` here
   285  				if f.usingFile != nil {
   286  					_ = f.usingFile.Close()
   287  					f.usingFile = nil
   288  				}
   289  				return nil, err
   290  			}
   291  		}
   292  		return f.usingFile, nil
   293  	}
   294  	if f.PreRotateHandler != nil {
   295  		f.PreRotateHandler(f.usingFilePath)
   296  	}
   297  	newFile, err := f.rotateLocked(newName)
   298  	if err != nil {
   299  		if bailOnRotateFail {
   300  			// Failure to rotate is a problem, but it's really not a great
   301  			// idea to stop your application just because you couldn't rename
   302  			// your log.
   303  			//
   304  			// We only return this error when explicitly needed (as specified by bailOnRotateFail)
   305  			//
   306  			// However, we *NEED* to close `fh` here
   307  			if newFile != nil {
   308  				_ = newFile.Close()
   309  				newFile = nil
   310  			}
   311  			return nil, err
   312  		}
   313  	}
   314  	if newFile == nil {
   315  		// no file can be written, it's an error explicitly
   316  		if f.usingFile == nil {
   317  			return nil, err
   318  		}
   319  		return f.usingFile, nil
   320  	}
   321  
   322  	if f.usingFile != nil {
   323  		_ = f.usingFile.Close()
   324  		f.usingFile = nil
   325  	}
   326  	f.usingFile = newFile
   327  	f.usingFilePath = newName
   328  	f.usingSeq = newSeq
   329  	if f.PostRotateHandler != nil {
   330  		f.PostRotateHandler(f.usingFilePath)
   331  	}
   332  
   333  	return f.usingFile, nil
   334  }
   335  
   336  // file may not be nil if err is nil
   337  func (f *RotateFile) rotateLocked(newName string) (*os.File, error) {
   338  	var err error
   339  	// if we got here, then we need to create a file
   340  	switch f.RotateMode {
   341  	case RotateModeCopyRename:
   342  		// for which open the file, and write file not by RotateFile
   343  		// CopyRenameFileAll = RenameFileAll(src->dst) + OpenFile(src)
   344  		// usingFilePath->newName + recreate usingFilePath
   345  		err = CopyRenameAll(newName, f.usingFilePath)
   346  	case RotateModeCopyTruncate:
   347  		// for which open the file, and write file not by RotateFile
   348  		// CopyTruncateFile = CopyFile(src->dst) + Truncate(src)
   349  		// usingFilePath->newName + truncate usingFilePath
   350  		err = CopyTruncateAll(newName, f.usingFilePath)
   351  	case RotateModeNew:
   352  		// for which open the file, and write file by RotateFile
   353  		fallthrough
   354  	default:
   355  	}
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  	file, err := AppendAllIfNotExist(newName)
   360  	if err != nil {
   361  		return nil, err
   362  	}
   363  
   364  	// link -> filename
   365  	if f.FileLinkPath != "" {
   366  		if err := ReSymlink(newName, f.FileLinkPath); err != nil {
   367  			return nil, err
   368  		}
   369  	}
   370  	// unlink files on a separate goroutine
   371  	go f.serializedClean()
   372  
   373  	return file, nil
   374  }
   375  
   376  // unlink files
   377  // expect run on a separate goroutine
   378  func (f *RotateFile) serializedClean() error {
   379  	// running already, ignore duplicate clean
   380  	if !f.cleaning.CAS(false, true) {
   381  		return nil
   382  	}
   383  	defer f.cleaning.Store(false)
   384  
   385  	now := time.Now()
   386  
   387  	// find old files
   388  	var filesNotExpired []string
   389  	filesExpired, err := filepath_.GlobFunc(f.FilePathPrefix+f.RotateFileGlob, func(name string) bool {
   390  		fi, err := os.Stat(name)
   391  		if err != nil {
   392  			return false
   393  		}
   394  
   395  		fl, err := os.Lstat(name)
   396  		if err != nil {
   397  			return false
   398  		}
   399  		if f.MaxAge <= 0 {
   400  			filesNotExpired = append(filesNotExpired, name)
   401  			return false
   402  		}
   403  
   404  		if now.Sub(fi.ModTime()) < f.MaxAge {
   405  			filesNotExpired = append(filesNotExpired, name)
   406  			return false
   407  		}
   408  
   409  		if fl.Mode()&os.ModeSymlink == os.ModeSymlink {
   410  			return false
   411  		}
   412  		return true
   413  	})
   414  	if err != nil {
   415  		return err
   416  	}
   417  
   418  	var filesExceedMaxCount []string
   419  	if f.MaxCount > 0 && len(filesNotExpired) > 0 {
   420  		removeCount := len(filesNotExpired) - f.MaxCount
   421  		if removeCount < 0 {
   422  			removeCount = 0
   423  		}
   424  		sort.Sort(rotateFileSlice(filesNotExpired))
   425  		filesExceedMaxCount = filesNotExpired[:removeCount]
   426  	}
   427  	var errs []error
   428  	for _, path := range filesExpired {
   429  		err = os.Remove(path)
   430  		if err != nil {
   431  			errs = append(errs, err)
   432  		}
   433  	}
   434  	for _, path := range filesExceedMaxCount {
   435  		err = os.Remove(path)
   436  		if err != nil {
   437  			errs = append(errs, err)
   438  		}
   439  	}
   440  	return errors_.Multi(errs...)
   441  }
   442  
   443  // foo.txt, 0 -> foo.txt
   444  // foo.txt, 1 -> foo.txt.[1,2,...], which is not exist and seq is max
   445  func nextSeqFileName(name string, seq int) (string, int) {
   446  	// A new file has been requested. Instead of just using the
   447  	// regular strftime pattern, we create a new file name using
   448  	// generational names such as "foo.1", "foo.2", "foo.3", etc
   449  	nf, seqUsed, err := NextFile(name+".*", seq)
   450  	if err != nil {
   451  		return name, seq
   452  	}
   453  	defer nf.Close()
   454  	if seqUsed == 0 {
   455  		return name, seqUsed
   456  	}
   457  	return nf.Name(), seqUsed
   458  }
   459  
   460  // foo.txt -> foo.txt
   461  // foo.txt.1 -> foo.txt
   462  // foo.txt.1.1 -> foo.txt.1
   463  func trimSeqFromNextFileName(name string, seq int) string {
   464  	if seq == 0 {
   465  		return name
   466  	}
   467  	return strings.TrimSuffix(name, fmt.Sprintf(".%d", seq))
   468  }
   469  
   470  // foo.txt.* -> foo.txt.[1,2,...], which exists and seq is max
   471  func maxSeqFileName(name string) (string, int) {
   472  	prefix, seq, suffix := MaxSeq(name + ".*")
   473  	if seq == 0 {
   474  		return name, seq
   475  	}
   476  	return fmt.Sprintf("%s%d%s", prefix, seq, suffix), seq
   477  }
   478  
   479  // sort filename by mode time and ascii in increase order
   480  type rotateFileSlice []string
   481  
   482  func (s rotateFileSlice) Len() int {
   483  	return len(s)
   484  }
   485  func (s rotateFileSlice) Swap(i, j int) {
   486  	s[i], s[j] = s[j], s[i]
   487  }
   488  
   489  func (s rotateFileSlice) Less(i, j int) bool {
   490  	fi, err := os.Stat(s[i])
   491  	if err != nil {
   492  		return false
   493  	}
   494  	fj, err := os.Stat(s[j])
   495  	if err != nil {
   496  		return false
   497  	}
   498  	if fi.ModTime().Equal(fj.ModTime()) {
   499  		if len(s[i]) == len(s[j]) {
   500  			return s[i] < s[j]
   501  		}
   502  		return len(s[i]) > len(s[j]) // foo.1, foo.2, ..., foo
   503  	}
   504  	return fi.ModTime().Before(fj.ModTime())
   505  }