github.com/shuguocloud/go-zero@v1.3.0/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  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/shuguocloud/go-zero/core/fs"
    17  	"github.com/shuguocloud/go-zero/core/lang"
    18  	"github.com/shuguocloud/go-zero/core/timex"
    19  )
    20  
    21  const (
    22  	dateFormat      = "2006-01-02"
    23  	hoursPerDay     = 24
    24  	bufferSize      = 100
    25  	defaultDirMode  = 0o755
    26  	defaultFileMode = 0o600
    27  )
    28  
    29  // ErrLogFileClosed is an error that indicates the log file is already closed.
    30  var ErrLogFileClosed = errors.New("error: log file closed")
    31  
    32  type (
    33  	// A RotateRule interface is used to define the log rotating rules.
    34  	RotateRule interface {
    35  		BackupFileName() string
    36  		MarkRotated()
    37  		OutdatedFiles() []string
    38  		ShallRotate() bool
    39  	}
    40  
    41  	// A RotateLogger is a Logger that can rotate log files with given rules.
    42  	RotateLogger struct {
    43  		filename string
    44  		backup   string
    45  		fp       *os.File
    46  		channel  chan []byte
    47  		done     chan lang.PlaceholderType
    48  		rule     RotateRule
    49  		compress bool
    50  		// can't use threading.RoutineGroup because of cycle import
    51  		waitGroup sync.WaitGroup
    52  		closeOnce sync.Once
    53  	}
    54  
    55  	// A DailyRotateRule is a rule to daily rotate the log files.
    56  	DailyRotateRule struct {
    57  		rotatedTime string
    58  		filename    string
    59  		delimiter   string
    60  		days        int
    61  		gzip        bool
    62  	}
    63  )
    64  
    65  // DefaultRotateRule is a default log rotating rule, currently DailyRotateRule.
    66  func DefaultRotateRule(filename, delimiter string, days int, gzip bool) RotateRule {
    67  	return &DailyRotateRule{
    68  		rotatedTime: getNowDate(),
    69  		filename:    filename,
    70  		delimiter:   delimiter,
    71  		days:        days,
    72  		gzip:        gzip,
    73  	}
    74  }
    75  
    76  // BackupFileName returns the backup filename on rotating.
    77  func (r *DailyRotateRule) BackupFileName() string {
    78  	return fmt.Sprintf("%s%s%s", r.filename, r.delimiter, getNowDate())
    79  }
    80  
    81  // MarkRotated marks the rotated time of r to be the current time.
    82  func (r *DailyRotateRule) MarkRotated() {
    83  	r.rotatedTime = getNowDate()
    84  }
    85  
    86  // OutdatedFiles returns the files that exceeded the keeping days.
    87  func (r *DailyRotateRule) OutdatedFiles() []string {
    88  	if r.days <= 0 {
    89  		return nil
    90  	}
    91  
    92  	var pattern string
    93  	if r.gzip {
    94  		pattern = fmt.Sprintf("%s%s*.gz", r.filename, r.delimiter)
    95  	} else {
    96  		pattern = fmt.Sprintf("%s%s*", r.filename, r.delimiter)
    97  	}
    98  
    99  	files, err := filepath.Glob(pattern)
   100  	if err != nil {
   101  		Errorf("failed to delete outdated log files, error: %s", err)
   102  		return nil
   103  	}
   104  
   105  	var buf strings.Builder
   106  	boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(dateFormat)
   107  	fmt.Fprintf(&buf, "%s%s%s", r.filename, r.delimiter, boundary)
   108  	if r.gzip {
   109  		buf.WriteString(".gz")
   110  	}
   111  	boundaryFile := buf.String()
   112  
   113  	var outdates []string
   114  	for _, file := range files {
   115  		if file < boundaryFile {
   116  			outdates = append(outdates, file)
   117  		}
   118  	}
   119  
   120  	return outdates
   121  }
   122  
   123  // ShallRotate checks if the file should be rotated.
   124  func (r *DailyRotateRule) ShallRotate() bool {
   125  	return len(r.rotatedTime) > 0 && getNowDate() != r.rotatedTime
   126  }
   127  
   128  // NewLogger returns a RotateLogger with given filename and rule, etc.
   129  func NewLogger(filename string, rule RotateRule, compress bool) (*RotateLogger, error) {
   130  	l := &RotateLogger{
   131  		filename: filename,
   132  		channel:  make(chan []byte, bufferSize),
   133  		done:     make(chan lang.PlaceholderType),
   134  		rule:     rule,
   135  		compress: compress,
   136  	}
   137  	if err := l.init(); err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	l.startWorker()
   142  	return l, nil
   143  }
   144  
   145  // Close closes l.
   146  func (l *RotateLogger) Close() error {
   147  	var err error
   148  
   149  	l.closeOnce.Do(func() {
   150  		close(l.done)
   151  		l.waitGroup.Wait()
   152  
   153  		if err = l.fp.Sync(); err != nil {
   154  			return
   155  		}
   156  
   157  		err = l.fp.Close()
   158  	})
   159  
   160  	return err
   161  }
   162  
   163  func (l *RotateLogger) Write(data []byte) (int, error) {
   164  	select {
   165  	case l.channel <- data:
   166  		return len(data), nil
   167  	case <-l.done:
   168  		log.Println(string(data))
   169  		return 0, ErrLogFileClosed
   170  	}
   171  }
   172  
   173  func (l *RotateLogger) getBackupFilename() string {
   174  	if len(l.backup) == 0 {
   175  		return l.rule.BackupFileName()
   176  	}
   177  
   178  	return l.backup
   179  }
   180  
   181  func (l *RotateLogger) init() error {
   182  	l.backup = l.rule.BackupFileName()
   183  
   184  	if _, err := os.Stat(l.filename); err != nil {
   185  		basePath := path.Dir(l.filename)
   186  		if _, err = os.Stat(basePath); err != nil {
   187  			if err = os.MkdirAll(basePath, defaultDirMode); err != nil {
   188  				return err
   189  			}
   190  		}
   191  
   192  		if l.fp, err = os.Create(l.filename); err != nil {
   193  			return err
   194  		}
   195  	} else if l.fp, err = os.OpenFile(l.filename, os.O_APPEND|os.O_WRONLY, defaultFileMode); err != nil {
   196  		return err
   197  	}
   198  
   199  	fs.CloseOnExec(l.fp)
   200  
   201  	return nil
   202  }
   203  
   204  func (l *RotateLogger) maybeCompressFile(file string) {
   205  	if !l.compress {
   206  		return
   207  	}
   208  
   209  	defer func() {
   210  		if r := recover(); r != nil {
   211  			ErrorStack(r)
   212  		}
   213  	}()
   214  	compressLogFile(file)
   215  }
   216  
   217  func (l *RotateLogger) maybeDeleteOutdatedFiles() {
   218  	files := l.rule.OutdatedFiles()
   219  	for _, file := range files {
   220  		if err := os.Remove(file); err != nil {
   221  			Errorf("failed to remove outdated file: %s", file)
   222  		}
   223  	}
   224  }
   225  
   226  func (l *RotateLogger) postRotate(file string) {
   227  	go func() {
   228  		// we cannot use threading.GoSafe here, because of import cycle.
   229  		l.maybeCompressFile(file)
   230  		l.maybeDeleteOutdatedFiles()
   231  	}()
   232  }
   233  
   234  func (l *RotateLogger) rotate() error {
   235  	if l.fp != nil {
   236  		err := l.fp.Close()
   237  		l.fp = nil
   238  		if err != nil {
   239  			return err
   240  		}
   241  	}
   242  
   243  	_, err := os.Stat(l.filename)
   244  	if err == nil && len(l.backup) > 0 {
   245  		backupFilename := l.getBackupFilename()
   246  		err = os.Rename(l.filename, backupFilename)
   247  		if err != nil {
   248  			return err
   249  		}
   250  
   251  		l.postRotate(backupFilename)
   252  	}
   253  
   254  	l.backup = l.rule.BackupFileName()
   255  	if l.fp, err = os.Create(l.filename); err == nil {
   256  		fs.CloseOnExec(l.fp)
   257  	}
   258  
   259  	return err
   260  }
   261  
   262  func (l *RotateLogger) startWorker() {
   263  	l.waitGroup.Add(1)
   264  
   265  	go func() {
   266  		defer l.waitGroup.Done()
   267  
   268  		for {
   269  			select {
   270  			case event := <-l.channel:
   271  				l.write(event)
   272  			case <-l.done:
   273  				return
   274  			}
   275  		}
   276  	}()
   277  }
   278  
   279  func (l *RotateLogger) write(v []byte) {
   280  	if l.rule.ShallRotate() {
   281  		if err := l.rotate(); err != nil {
   282  			log.Println(err)
   283  		} else {
   284  			l.rule.MarkRotated()
   285  		}
   286  	}
   287  	if l.fp != nil {
   288  		l.fp.Write(v)
   289  	}
   290  }
   291  
   292  func compressLogFile(file string) {
   293  	start := timex.Now()
   294  	Infof("compressing log file: %s", file)
   295  	if err := gzipFile(file); err != nil {
   296  		Errorf("compress error: %s", err)
   297  	} else {
   298  		Infof("compressed log file: %s, took %s", file, timex.Since(start))
   299  	}
   300  }
   301  
   302  func getNowDate() string {
   303  	return time.Now().Format(dateFormat)
   304  }
   305  
   306  func gzipFile(file string) error {
   307  	in, err := os.Open(file)
   308  	if err != nil {
   309  		return err
   310  	}
   311  	defer in.Close()
   312  
   313  	out, err := os.Create(fmt.Sprintf("%s.gz", file))
   314  	if err != nil {
   315  		return err
   316  	}
   317  	defer out.Close()
   318  
   319  	w := gzip.NewWriter(out)
   320  	if _, err = io.Copy(w, in); err != nil {
   321  		return err
   322  	} else if err = w.Close(); err != nil {
   323  		return err
   324  	}
   325  
   326  	return os.Remove(file)
   327  }