github.com/wfusion/gofusion@v1.1.14/common/infra/rotatelog/rotatelog.go (about)

     1  // Package rotatelog provides a rolling logger.
     2  //
     3  // Note that this is v2.0 of lumberjack, and should be imported using gopkg.in
     4  //
     5  // Lumberjack is intended to be one part of a logging infrastructure.
     6  // It is not an all-in-one solution, but instead is a pluggable
     7  // component at the bottom of the logging stack that simply controls the files
     8  // to which logs are written.
     9  //
    10  // Lumberjack plays well with any logging package that can write to an
    11  // io.Writer, including the standard library's log package.
    12  //
    13  // Lumberjack assumes that only one process is writing to the output files.
    14  // Using the same lumberjack configuration from multiple processes on the same
    15  // machine will result in improper behavior.
    16  package rotatelog
    17  
    18  import (
    19  	"compress/gzip"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"os"
    25  	"path/filepath"
    26  	"sort"
    27  	"strings"
    28  	"sync"
    29  	"time"
    30  
    31  	"github.com/dustin/go-humanize"
    32  	"go.uber.org/atomic"
    33  
    34  	"github.com/wfusion/gofusion/common/utils"
    35  )
    36  
    37  const (
    38  	backupTimeFormat = "2006-01-02T15-04-05.000"
    39  	compressSuffix   = ".gz"
    40  	defaultMaxSize   = 100
    41  )
    42  
    43  // ensure we always implement io.WriteCloser
    44  var _ io.WriteCloser = (*Logger)(nil)
    45  
    46  // Logger is an io.WriteCloser that writes to the specified filename.
    47  //
    48  // Logger opens or creates the logfile on first Write.  If the file exists and
    49  // is less than MaxSize megabytes, lumberjack will open and append to that file.
    50  // If the file exists and its size is >= MaxSize megabytes, the file is renamed
    51  // by putting the current time in a timestamp in the name immediately before the
    52  // file's extension (or the end of the filename if there's no extension). A new
    53  // log file is then created using original filename.
    54  //
    55  // Whenever a write would cause the current log file exceed MaxSize megabytes,
    56  // the current file is closed, renamed, and a new log file created with the
    57  // original name. Thus, the filename you give Logger is always the "current" log
    58  // file.
    59  //
    60  // Backups use the log file name given to Logger, in the form
    61  // `name-timestamp.ext` where name is the filename without the extension,
    62  // timestamp is the time at which the log was rotated formatted with the
    63  // time.Time format of `2006-01-02T15-04-05.000` and the extension is the
    64  // original extension.  For example, if your Logger.Filename is
    65  // `/var/log/foo/server.log`, a backup created at 6:30pm on Nov 11 2016 would
    66  // use the filename `/var/log/foo/server-2016-11-04T18-30-00.000.log`
    67  //
    68  // Cleaning Up Old Log Files
    69  //
    70  // Whenever a new logfile gets created, old log files may be deleted.  The most
    71  // recent files according to the encoded timestamp will be retained, up to a
    72  // number equal to MaxBackups (or all of them if MaxBackups is 0).  Any files
    73  // with an encoded timestamp older than MaxAge days are deleted, regardless of
    74  // MaxBackups.  Note that the time encoded in the timestamp is the rotation
    75  // time, which may differ from the last time that file was written to.
    76  //
    77  // If MaxBackups and MaxAge are both 0, no old log files will be deleted.
    78  type Logger struct {
    79  	// Filename is the file to write logs to.  Backup log files will be retained
    80  	// in the same directory.  It uses <processname>-rotate.log in
    81  	// os.TempDir() if empty.
    82  	Filename string `json:"filename" yaml:"filename" toml:"filename"`
    83  
    84  	// MaxSize is the maximum size in megabytes of the log file before it gets
    85  	// rotated. It defaults to 100 megabytes.
    86  	MaxSize int64 `json:"maxsize" yaml:"maxsize" toml:"maxsize"`
    87  
    88  	// MaxAge is the maximum number of days to retain old log files based on the
    89  	// timestamp encoded in their filename.  Note that may not exactly correspond
    90  	// to calendar days due to daylight savings, leap seconds, etc.
    91  	// The default is not to remove old log files based on age.
    92  	MaxAge time.Duration `json:"maxage" yaml:"maxage" toml:"maxage"`
    93  
    94  	// MaxBackups is the maximum number of old log files to retain.  The default
    95  	// is to retain all old log files (though MaxAge may still cause them to get
    96  	// deleted.)
    97  	MaxBackups int `json:"maxbackups" yaml:"maxbackups" toml:"maxbackups"`
    98  
    99  	// LocalTime determines if the time used for formatting the timestamps in
   100  	// backup files is the computer's local time.  The default is to use UTC
   101  	// time.
   102  	LocalTime bool `json:"localtime" yaml:"localtime" toml:"localtime"`
   103  
   104  	// Compress determines if the rotated log files should be compressed
   105  	// using gzip. The default is not to perform compression.
   106  	Compress bool `json:"compress" yaml:"compress" toml:"compress"`
   107  
   108  	size atomic.Int64
   109  	file *os.File
   110  	mu   sync.Mutex
   111  
   112  	millCh    chan bool
   113  	startMill sync.Once
   114  }
   115  
   116  var (
   117  	// currentTime exists so it can be mocked out by tests.
   118  	currentTime = time.Now
   119  
   120  	// os_Stat exists so it can be mocked out by tests.
   121  	osStat = os.Stat
   122  )
   123  
   124  // Write implements io.Writer.  If a write would cause the log file to be larger
   125  // than MaxSize, the file is closed, renamed to include a timestamp of the
   126  // current time, and a new log file is created using the original log file name.
   127  // If the length of the write is greater than MaxSize, an error is returned.
   128  func (l *Logger) Write(p []byte) (n int, err error) {
   129  	l.mu.Lock()
   130  	defer l.mu.Unlock()
   131  
   132  	writeLen := int64(len(p))
   133  	if writeLen > l.max() {
   134  		return 0, fmt.Errorf(
   135  			"write length %d exceeds maximum file size %d", writeLen, l.max(),
   136  		)
   137  	}
   138  
   139  	if l.file == nil {
   140  		if err = l.openExistingOrNew(len(p)); err != nil {
   141  			return 0, err
   142  		}
   143  	}
   144  
   145  	if l.size.Load()+writeLen > l.max() {
   146  		if err := l.rotate(); err != nil {
   147  			return 0, err
   148  		}
   149  	}
   150  
   151  	n, err = l.file.Write(p)
   152  	l.size.Add(int64(n))
   153  
   154  	return n, err
   155  }
   156  
   157  // Close implements io.Closer, and closes the current logfile.
   158  func (l *Logger) Close() error {
   159  	l.mu.Lock()
   160  	defer l.mu.Unlock()
   161  	return l.close()
   162  }
   163  
   164  // close closes the file if it is open.
   165  func (l *Logger) close() error {
   166  	if l.file == nil {
   167  		return nil
   168  	}
   169  	err := l.file.Close()
   170  	l.file = nil
   171  	return err
   172  }
   173  
   174  // Rotate causes Logger to close the existing log file and immediately create a
   175  // new one.  This is a helper function for applications that want to initiate
   176  // rotations outside of the normal rotation rules, such as in response to
   177  // SIGHUP.  After rotating, this initiates compression and removal of old log
   178  // files according to the configuration.
   179  func (l *Logger) Rotate() error {
   180  	l.mu.Lock()
   181  	defer l.mu.Unlock()
   182  	return l.rotate()
   183  }
   184  
   185  // rotate closes the current file, moves it aside with a timestamp in the name,
   186  // (if it exists), opens a new file with the original filename, and then runs
   187  // post-rotation processing and removal.
   188  func (l *Logger) rotate() error {
   189  	if err := l.close(); err != nil {
   190  		return err
   191  	}
   192  	if err := l.openNew(); err != nil {
   193  		return err
   194  	}
   195  	l.mill()
   196  	return nil
   197  }
   198  
   199  // openNew opens a new log file for writing, moving any old log file out of the
   200  // way.  This methods assumes the file has already been closed.
   201  func (l *Logger) openNew() error {
   202  	err := os.MkdirAll(l.dir(), 0755)
   203  	if err != nil {
   204  		return fmt.Errorf("can't make directories for new logfile: %s", err)
   205  	}
   206  
   207  	name := l.filename()
   208  	mode := os.FileMode(0600)
   209  	info, err := osStat(name)
   210  	if err == nil {
   211  		// Copy the mode off the old logfile.
   212  		mode = info.Mode()
   213  		// move the existing file
   214  		newname := backupName(name, l.LocalTime)
   215  		if err := os.Rename(name, newname); err != nil {
   216  			return fmt.Errorf("can't rename log file: %s", err)
   217  		}
   218  
   219  		// this is a no-op anywhere but linux
   220  		if err := chown(name, info); err != nil {
   221  			return err
   222  		}
   223  	}
   224  
   225  	// we use truncate here because this should only get called when we've moved
   226  	// the file ourselves. if someone else creates the file in the meantime,
   227  	// just wipe out the contents.
   228  	f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
   229  	if err != nil {
   230  		return fmt.Errorf("can't open new logfile: %s", err)
   231  	}
   232  	l.file = f
   233  	l.size.Store(0)
   234  	return nil
   235  }
   236  
   237  // backupName creates a new filename from the given name, inserting a timestamp
   238  // between the filename and the extension, using the local time if requested
   239  // (otherwise UTC).
   240  func backupName(name string, local bool) string {
   241  	dir := filepath.Dir(name)
   242  	filename := filepath.Base(name)
   243  	ext := filepath.Ext(filename)
   244  	prefix := filename[:len(filename)-len(ext)]
   245  	t := currentTime()
   246  	if !local {
   247  		t = t.UTC()
   248  	}
   249  
   250  	timestamp := t.Format(backupTimeFormat)
   251  	return filepath.Join(dir, fmt.Sprintf("%s-%s%s", prefix, timestamp, ext))
   252  }
   253  
   254  // openExistingOrNew opens the logfile if it exists and if the current write
   255  // would not put it over MaxSize.  If there is no such file or the write would
   256  // put it over the MaxSize, a new file is created.
   257  func (l *Logger) openExistingOrNew(writeLen int) error {
   258  	l.mill()
   259  
   260  	filename := l.filename()
   261  	info, err := osStat(filename)
   262  	if os.IsNotExist(err) {
   263  		return l.openNew()
   264  	}
   265  	if err != nil {
   266  		return fmt.Errorf("error getting log file info: %s", err)
   267  	}
   268  
   269  	if info.Size()+int64(writeLen) >= l.max() {
   270  		return l.rotate()
   271  	}
   272  
   273  	file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0644)
   274  	if err != nil {
   275  		// if we fail to open the old log file for some reason, just ignore
   276  		// it and open a new log file.
   277  		return l.openNew()
   278  	}
   279  	l.file = file
   280  	l.size.Store(0)
   281  	return nil
   282  }
   283  
   284  // filename generates the name of the logfile from the current time.
   285  func (l *Logger) filename() string {
   286  	if l.Filename != "" {
   287  		return l.Filename
   288  	}
   289  	name := filepath.Base(os.Args[0]) + "-rotate.log"
   290  	return filepath.Join(os.TempDir(), name)
   291  }
   292  
   293  // millRunOnce performs compression and removal of stale log files.
   294  // Log files are compressed if enabled via configuration and old log
   295  // files are removed, keeping at most l.MaxBackups files, as long as
   296  // none of them are older than MaxAge.
   297  func (l *Logger) millRunOnce() error {
   298  	if l.MaxBackups == 0 && l.MaxAge == 0 && !l.Compress {
   299  		return nil
   300  	}
   301  
   302  	files, err := l.oldLogFiles()
   303  	if err != nil {
   304  		return err
   305  	}
   306  
   307  	var compress, remove []logInfo
   308  
   309  	if l.MaxBackups > 0 && l.MaxBackups < len(files) {
   310  		preserved := make(map[string]bool)
   311  		var remaining []logInfo
   312  		for _, f := range files {
   313  			// Only count the uncompressed log file or the
   314  			// compressed log file, not both.
   315  			fn := f.Name()
   316  			fn = strings.TrimSuffix(fn, compressSuffix)
   317  			preserved[fn] = true
   318  
   319  			if len(preserved) > l.MaxBackups {
   320  				remove = append(remove, f)
   321  			} else {
   322  				remaining = append(remaining, f)
   323  			}
   324  		}
   325  		files = remaining
   326  	}
   327  	if l.MaxAge > 0 {
   328  		cutoff := currentTime().Add(-1 * l.MaxAge)
   329  
   330  		var remaining []logInfo
   331  		for _, f := range files {
   332  			if f.timestamp.Before(cutoff) {
   333  				remove = append(remove, f)
   334  			} else {
   335  				remaining = append(remaining, f)
   336  			}
   337  		}
   338  		files = remaining
   339  	}
   340  
   341  	if l.Compress {
   342  		for _, f := range files {
   343  			if !strings.HasSuffix(f.Name(), compressSuffix) {
   344  				compress = append(compress, f)
   345  			}
   346  		}
   347  	}
   348  
   349  	for _, f := range remove {
   350  		errRemove := os.Remove(filepath.Join(l.dir(), f.Name()))
   351  		if err == nil && errRemove != nil {
   352  			err = errRemove
   353  		}
   354  	}
   355  	for _, f := range compress {
   356  		fn := filepath.Join(l.dir(), f.Name())
   357  		errCompress := compressLogFile(fn, fn+compressSuffix)
   358  		if err == nil && errCompress != nil {
   359  			err = errCompress
   360  		}
   361  	}
   362  
   363  	return err
   364  }
   365  
   366  // millRun runs in a goroutine to manage post-rotation compression and removal
   367  // of old log files.
   368  func (l *Logger) millRun() {
   369  	for range l.millCh {
   370  		// what am I going to do, log this?
   371  		_ = l.millRunOnce()
   372  	}
   373  }
   374  
   375  // mill performs post-rotation compression and removal of stale log files,
   376  // starting the mill goroutine if necessary.
   377  func (l *Logger) mill() {
   378  	l.startMill.Do(func() {
   379  		l.millCh = make(chan bool, 1)
   380  		go utils.Catch(func() { l.millRun() })
   381  	})
   382  	select {
   383  	case l.millCh <- true:
   384  	default:
   385  	}
   386  }
   387  
   388  // oldLogFiles returns the list of backup log files stored in the same
   389  // directory as the current log file, sorted by ModTime
   390  func (l *Logger) oldLogFiles() ([]logInfo, error) {
   391  	files, err := ioutil.ReadDir(l.dir())
   392  	if err != nil {
   393  		return nil, fmt.Errorf("can't read log file directory: %s", err)
   394  	}
   395  	logFiles := make([]logInfo, 0, l.MaxBackups+1)
   396  
   397  	prefix, ext := l.prefixAndExt()
   398  
   399  	for _, f := range files {
   400  		if f.IsDir() {
   401  			continue
   402  		}
   403  		if t, err := l.timeFromName(f.Name(), prefix, ext); err == nil {
   404  			logFiles = append(logFiles, logInfo{t, f})
   405  			continue
   406  		}
   407  		if t, err := l.timeFromName(f.Name(), prefix, ext+compressSuffix); err == nil {
   408  			logFiles = append(logFiles, logInfo{t, f})
   409  			continue
   410  		}
   411  		// error parsing means that the suffix at the end was not generated
   412  		// by lumberjack, and therefore it's not a backup file.
   413  	}
   414  
   415  	sort.Sort(byFormatTime(logFiles))
   416  
   417  	return logFiles, nil
   418  }
   419  
   420  // timeFromName extracts the formatted time from the filename by stripping off
   421  // the filename's prefix and extension. This prevents someone's filename from
   422  // confusing time.parse.
   423  func (l *Logger) timeFromName(filename, prefix, ext string) (time.Time, error) {
   424  	if !strings.HasPrefix(filename, prefix) {
   425  		return time.Time{}, errors.New("mismatched prefix")
   426  	}
   427  	if !strings.HasSuffix(filename, ext) {
   428  		return time.Time{}, errors.New("mismatched extension")
   429  	}
   430  	ts := filename[len(prefix) : len(filename)-len(ext)]
   431  	return time.Parse(backupTimeFormat, ts)
   432  }
   433  
   434  // max returns the maximum size in bytes of log files before rolling.
   435  func (l *Logger) max() int64 {
   436  	if l.MaxSize == 0 {
   437  		return int64(defaultMaxSize * humanize.MiByte)
   438  	}
   439  	return l.MaxSize
   440  }
   441  
   442  // dir returns the directory for the current filename.
   443  func (l *Logger) dir() string {
   444  	return filepath.Dir(l.filename())
   445  }
   446  
   447  // prefixAndExt returns the filename part and extension part from the Logger's
   448  // filename.
   449  func (l *Logger) prefixAndExt() (prefix, ext string) {
   450  	filename := filepath.Base(l.filename())
   451  	ext = filepath.Ext(filename)
   452  	prefix = filename[:len(filename)-len(ext)] + "-"
   453  	return prefix, ext
   454  }
   455  
   456  // compressLogFile compresses the given log file, removing the
   457  // uncompressed log file if successful.
   458  func compressLogFile(src, dst string) (err error) {
   459  	f, err := os.Open(src)
   460  	if err != nil {
   461  		return fmt.Errorf("failed to open log file: %v", err)
   462  	}
   463  	defer f.Close()
   464  
   465  	fi, err := osStat(src)
   466  	if err != nil {
   467  		return fmt.Errorf("failed to stat log file: %v", err)
   468  	}
   469  
   470  	if err := chown(dst, fi); err != nil {
   471  		return fmt.Errorf("failed to chown compressed log file: %v", err)
   472  	}
   473  
   474  	// If this file already exists, we presume it was created by
   475  	// a previous attempt to compress the log file.
   476  	gzf, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fi.Mode())
   477  	if err != nil {
   478  		return fmt.Errorf("failed to open compressed log file: %v", err)
   479  	}
   480  	defer gzf.Close()
   481  
   482  	gz := gzip.NewWriter(gzf)
   483  
   484  	defer func() {
   485  		if err != nil {
   486  			_ = os.Remove(dst)
   487  			err = fmt.Errorf("failed to compress log file: %v", err)
   488  		}
   489  	}()
   490  
   491  	if _, err := io.Copy(gz, f); err != nil {
   492  		return err
   493  	}
   494  	if err := gz.Close(); err != nil {
   495  		return err
   496  	}
   497  	if err := gzf.Close(); err != nil {
   498  		return err
   499  	}
   500  
   501  	if err := f.Close(); err != nil {
   502  		return err
   503  	}
   504  	if err := os.Remove(src); err != nil {
   505  		return err
   506  	}
   507  
   508  	return nil
   509  }
   510  
   511  // logInfo is a convenience struct to return the filename and its embedded
   512  // timestamp.
   513  type logInfo struct {
   514  	timestamp time.Time
   515  	os.FileInfo
   516  }
   517  
   518  // byFormatTime sorts by newest time formatted in the name.
   519  type byFormatTime []logInfo
   520  
   521  func (b byFormatTime) Less(i, j int) bool {
   522  	return b[i].timestamp.After(b[j].timestamp)
   523  }
   524  
   525  func (b byFormatTime) Swap(i, j int) {
   526  	b[i], b[j] = b[j], b[i]
   527  }
   528  
   529  func (b byFormatTime) Len() int {
   530  	return len(b)
   531  }