github.com/lingyao2333/mo-zero@v1.4.1/core/logx/rotatelogger.go (about)

     1  package logx
     2  
     3  import (
     4  	"compress/gzip"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"sort"
    13  	"strings"
    14  	"sync"
    15  	"time"
    16  
    17  	"github.com/lingyao2333/mo-zero/core/fs"
    18  	"github.com/lingyao2333/mo-zero/core/lang"
    19  )
    20  
    21  const (
    22  	dateFormat      = "2006-01-02"
    23  	fileTimeFormat  = time.RFC3339
    24  	hoursPerDay     = 24
    25  	bufferSize      = 100
    26  	defaultDirMode  = 0o755
    27  	defaultFileMode = 0o600
    28  	gzipExt         = ".gz"
    29  	megaBytes       = 1 << 20
    30  )
    31  
    32  // ErrLogFileClosed is an error that indicates the log file is already closed.
    33  var ErrLogFileClosed = errors.New("error: log file closed")
    34  
    35  type (
    36  	// A RotateRule interface is used to define the log rotating rules.
    37  	RotateRule interface {
    38  		BackupFileName() string
    39  		MarkRotated()
    40  		OutdatedFiles() []string
    41  		ShallRotate(size int64) bool
    42  	}
    43  
    44  	// A RotateLogger is a Logger that can rotate log files with given rules.
    45  	RotateLogger struct {
    46  		filename string
    47  		backup   string
    48  		fp       *os.File
    49  		channel  chan []byte
    50  		done     chan lang.PlaceholderType
    51  		rule     RotateRule
    52  		compress bool
    53  		// can't use threading.RoutineGroup because of cycle import
    54  		waitGroup   sync.WaitGroup
    55  		closeOnce   sync.Once
    56  		currentSize int64
    57  	}
    58  
    59  	// A DailyRotateRule is a rule to daily rotate the log files.
    60  	DailyRotateRule struct {
    61  		rotatedTime string
    62  		filename    string
    63  		delimiter   string
    64  		days        int
    65  		gzip        bool
    66  	}
    67  
    68  	// SizeLimitRotateRule a rotation rule that make the log file rotated base on size
    69  	SizeLimitRotateRule struct {
    70  		DailyRotateRule
    71  		maxSize    int64
    72  		maxBackups int
    73  	}
    74  )
    75  
    76  // DefaultRotateRule is a default log rotating rule, currently DailyRotateRule.
    77  func DefaultRotateRule(filename, delimiter string, days int, gzip bool) RotateRule {
    78  	return &DailyRotateRule{
    79  		rotatedTime: getNowDate(),
    80  		filename:    filename,
    81  		delimiter:   delimiter,
    82  		days:        days,
    83  		gzip:        gzip,
    84  	}
    85  }
    86  
    87  // BackupFileName returns the backup filename on rotating.
    88  func (r *DailyRotateRule) BackupFileName() string {
    89  	return fmt.Sprintf("%s%s%s", r.filename, r.delimiter, getNowDate())
    90  }
    91  
    92  // MarkRotated marks the rotated time of r to be the current time.
    93  func (r *DailyRotateRule) MarkRotated() {
    94  	r.rotatedTime = getNowDate()
    95  }
    96  
    97  // OutdatedFiles returns the files that exceeded the keeping days.
    98  func (r *DailyRotateRule) OutdatedFiles() []string {
    99  	if r.days <= 0 {
   100  		return nil
   101  	}
   102  
   103  	var pattern string
   104  	if r.gzip {
   105  		pattern = fmt.Sprintf("%s%s*%s", r.filename, r.delimiter, gzipExt)
   106  	} else {
   107  		pattern = fmt.Sprintf("%s%s*", r.filename, r.delimiter)
   108  	}
   109  
   110  	files, err := filepath.Glob(pattern)
   111  	if err != nil {
   112  		Errorf("failed to delete outdated log files, error: %s", err)
   113  		return nil
   114  	}
   115  
   116  	var buf strings.Builder
   117  	boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(dateFormat)
   118  	buf.WriteString(r.filename)
   119  	buf.WriteString(r.delimiter)
   120  	buf.WriteString(boundary)
   121  	if r.gzip {
   122  		buf.WriteString(gzipExt)
   123  	}
   124  	boundaryFile := buf.String()
   125  
   126  	var outdates []string
   127  	for _, file := range files {
   128  		if file < boundaryFile {
   129  			outdates = append(outdates, file)
   130  		}
   131  	}
   132  
   133  	return outdates
   134  }
   135  
   136  // ShallRotate checks if the file should be rotated.
   137  func (r *DailyRotateRule) ShallRotate(_ int64) bool {
   138  	return len(r.rotatedTime) > 0 && getNowDate() != r.rotatedTime
   139  }
   140  
   141  // NewSizeLimitRotateRule returns the rotation rule with size limit
   142  func NewSizeLimitRotateRule(filename, delimiter string, days, maxSize, maxBackups int, gzip bool) RotateRule {
   143  	return &SizeLimitRotateRule{
   144  		DailyRotateRule: DailyRotateRule{
   145  			rotatedTime: getNowDateInRFC3339Format(),
   146  			filename:    filename,
   147  			delimiter:   delimiter,
   148  			days:        days,
   149  			gzip:        gzip,
   150  		},
   151  		maxSize:    int64(maxSize) * megaBytes,
   152  		maxBackups: maxBackups,
   153  	}
   154  }
   155  
   156  func (r *SizeLimitRotateRule) BackupFileName() string {
   157  	dir := filepath.Dir(r.filename)
   158  	prefix, ext := r.parseFilename()
   159  	timestamp := getNowDateInRFC3339Format()
   160  	return filepath.Join(dir, fmt.Sprintf("%s%s%s%s", prefix, r.delimiter, timestamp, ext))
   161  }
   162  
   163  func (r *SizeLimitRotateRule) MarkRotated() {
   164  	r.rotatedTime = getNowDateInRFC3339Format()
   165  }
   166  
   167  func (r *SizeLimitRotateRule) OutdatedFiles() []string {
   168  	dir := filepath.Dir(r.filename)
   169  	prefix, ext := r.parseFilename()
   170  
   171  	var pattern string
   172  	if r.gzip {
   173  		pattern = fmt.Sprintf("%s%s%s%s*%s%s", dir, string(filepath.Separator),
   174  			prefix, r.delimiter, ext, gzipExt)
   175  	} else {
   176  		pattern = fmt.Sprintf("%s%s%s%s*%s", dir, string(filepath.Separator),
   177  			prefix, r.delimiter, ext)
   178  	}
   179  
   180  	files, err := filepath.Glob(pattern)
   181  	if err != nil {
   182  		Errorf("failed to delete outdated log files, error: %s", err)
   183  		return nil
   184  	}
   185  
   186  	sort.Strings(files)
   187  
   188  	outdated := make(map[string]lang.PlaceholderType)
   189  
   190  	// test if too many backups
   191  	if r.maxBackups > 0 && len(files) > r.maxBackups {
   192  		for _, f := range files[:len(files)-r.maxBackups] {
   193  			outdated[f] = lang.Placeholder
   194  		}
   195  		files = files[len(files)-r.maxBackups:]
   196  	}
   197  
   198  	// test if any too old backups
   199  	if r.days > 0 {
   200  		boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(fileTimeFormat)
   201  		boundaryFile := filepath.Join(dir, fmt.Sprintf("%s%s%s%s", prefix, r.delimiter, boundary, ext))
   202  		if r.gzip {
   203  			boundaryFile += gzipExt
   204  		}
   205  		for _, f := range files {
   206  			if f >= boundaryFile {
   207  				break
   208  			}
   209  			outdated[f] = lang.Placeholder
   210  		}
   211  	}
   212  
   213  	var result []string
   214  	for k := range outdated {
   215  		result = append(result, k)
   216  	}
   217  	return result
   218  }
   219  
   220  func (r *SizeLimitRotateRule) ShallRotate(size int64) bool {
   221  	return r.maxSize > 0 && r.maxSize < size
   222  }
   223  
   224  func (r *SizeLimitRotateRule) parseFilename() (prefix, ext string) {
   225  	logName := filepath.Base(r.filename)
   226  	ext = filepath.Ext(r.filename)
   227  	prefix = logName[:len(logName)-len(ext)]
   228  	return
   229  }
   230  
   231  // NewLogger returns a RotateLogger with given filename and rule, etc.
   232  func NewLogger(filename string, rule RotateRule, compress bool) (*RotateLogger, error) {
   233  	l := &RotateLogger{
   234  		filename: filename,
   235  		channel:  make(chan []byte, bufferSize),
   236  		done:     make(chan lang.PlaceholderType),
   237  		rule:     rule,
   238  		compress: compress,
   239  	}
   240  	if err := l.init(); err != nil {
   241  		return nil, err
   242  	}
   243  
   244  	l.startWorker()
   245  	return l, nil
   246  }
   247  
   248  // Close closes l.
   249  func (l *RotateLogger) Close() error {
   250  	var err error
   251  
   252  	l.closeOnce.Do(func() {
   253  		close(l.done)
   254  		l.waitGroup.Wait()
   255  
   256  		if err = l.fp.Sync(); err != nil {
   257  			return
   258  		}
   259  
   260  		err = l.fp.Close()
   261  	})
   262  
   263  	return err
   264  }
   265  
   266  func (l *RotateLogger) Write(data []byte) (int, error) {
   267  	select {
   268  	case l.channel <- data:
   269  		return len(data), nil
   270  	case <-l.done:
   271  		log.Println(string(data))
   272  		return 0, ErrLogFileClosed
   273  	}
   274  }
   275  
   276  func (l *RotateLogger) getBackupFilename() string {
   277  	if len(l.backup) == 0 {
   278  		return l.rule.BackupFileName()
   279  	}
   280  
   281  	return l.backup
   282  }
   283  
   284  func (l *RotateLogger) init() error {
   285  	l.backup = l.rule.BackupFileName()
   286  
   287  	if _, err := os.Stat(l.filename); err != nil {
   288  		basePath := path.Dir(l.filename)
   289  		if _, err = os.Stat(basePath); err != nil {
   290  			if err = os.MkdirAll(basePath, defaultDirMode); err != nil {
   291  				return err
   292  			}
   293  		}
   294  
   295  		if l.fp, err = os.Create(l.filename); err != nil {
   296  			return err
   297  		}
   298  	} else if l.fp, err = os.OpenFile(l.filename, os.O_APPEND|os.O_WRONLY, defaultFileMode); err != nil {
   299  		return err
   300  	}
   301  
   302  	fs.CloseOnExec(l.fp)
   303  
   304  	return nil
   305  }
   306  
   307  func (l *RotateLogger) maybeCompressFile(file string) {
   308  	if !l.compress {
   309  		return
   310  	}
   311  
   312  	defer func() {
   313  		if r := recover(); r != nil {
   314  			ErrorStack(r)
   315  		}
   316  	}()
   317  
   318  	if _, err := os.Stat(file); err != nil {
   319  		// file not exists or other error, ignore compression
   320  		return
   321  	}
   322  
   323  	compressLogFile(file)
   324  }
   325  
   326  func (l *RotateLogger) maybeDeleteOutdatedFiles() {
   327  	files := l.rule.OutdatedFiles()
   328  	for _, file := range files {
   329  		if err := os.Remove(file); err != nil {
   330  			Errorf("failed to remove outdated file: %s", file)
   331  		}
   332  	}
   333  }
   334  
   335  func (l *RotateLogger) postRotate(file string) {
   336  	go func() {
   337  		// we cannot use threading.GoSafe here, because of import cycle.
   338  		l.maybeCompressFile(file)
   339  		l.maybeDeleteOutdatedFiles()
   340  	}()
   341  }
   342  
   343  func (l *RotateLogger) rotate() error {
   344  	if l.fp != nil {
   345  		err := l.fp.Close()
   346  		l.fp = nil
   347  		if err != nil {
   348  			return err
   349  		}
   350  	}
   351  
   352  	_, err := os.Stat(l.filename)
   353  	if err == nil && len(l.backup) > 0 {
   354  		backupFilename := l.getBackupFilename()
   355  		err = os.Rename(l.filename, backupFilename)
   356  		if err != nil {
   357  			return err
   358  		}
   359  
   360  		l.postRotate(backupFilename)
   361  	}
   362  
   363  	l.backup = l.rule.BackupFileName()
   364  	if l.fp, err = os.Create(l.filename); err == nil {
   365  		fs.CloseOnExec(l.fp)
   366  	}
   367  
   368  	return err
   369  }
   370  
   371  func (l *RotateLogger) startWorker() {
   372  	l.waitGroup.Add(1)
   373  
   374  	go func() {
   375  		defer l.waitGroup.Done()
   376  
   377  		for {
   378  			select {
   379  			case event := <-l.channel:
   380  				l.write(event)
   381  			case <-l.done:
   382  				return
   383  			}
   384  		}
   385  	}()
   386  }
   387  
   388  func (l *RotateLogger) write(v []byte) {
   389  	if l.rule.ShallRotate(l.currentSize + int64(len(v))) {
   390  		if err := l.rotate(); err != nil {
   391  			log.Println(err)
   392  		} else {
   393  			l.rule.MarkRotated()
   394  			l.currentSize = 0
   395  		}
   396  	}
   397  	if l.fp != nil {
   398  		l.fp.Write(v)
   399  		l.currentSize += int64(len(v))
   400  	}
   401  }
   402  
   403  func compressLogFile(file string) {
   404  	start := time.Now()
   405  	Infof("compressing log file: %s", file)
   406  	if err := gzipFile(file); err != nil {
   407  		Errorf("compress error: %s", err)
   408  	} else {
   409  		Infof("compressed log file: %s, took %s", file, time.Since(start))
   410  	}
   411  }
   412  
   413  func getNowDate() string {
   414  	return time.Now().Format(dateFormat)
   415  }
   416  
   417  func getNowDateInRFC3339Format() string {
   418  	return time.Now().Format(fileTimeFormat)
   419  }
   420  
   421  func gzipFile(file string) error {
   422  	in, err := os.Open(file)
   423  	if err != nil {
   424  		return err
   425  	}
   426  	defer in.Close()
   427  
   428  	out, err := os.Create(fmt.Sprintf("%s%s", file, gzipExt))
   429  	if err != nil {
   430  		return err
   431  	}
   432  	defer out.Close()
   433  
   434  	w := gzip.NewWriter(out)
   435  	if _, err = io.Copy(w, in); err != nil {
   436  		return err
   437  	} else if err = w.Close(); err != nil {
   438  		return err
   439  	}
   440  
   441  	return os.Remove(file)
   442  }