获取源码

go get gopkg.in/natefinch/lumberjack.v2

 

介绍

       lumberjack用于记录日志,但是它可以控制每个日志文件的大小,以及可以按规则删除历史的日志文件,甚至可以对历史的日志文件进行压缩.

       Logger会首先打开或创建logFile文件,如果logFile文件已存在并且该文件的大小没有超过设置的MaxSize,就会在打开该文件并进行追加日志。否则会创建新的日志文件。

      当前日志文件超过MaxSize MB,就会关闭当前文件,并将其重命名,并使用原始名称创建一个新的日志文件。因此,最新的日志输出都在原始名称的文件中。

 

保留的日志文件

存留的历史文件名称为:name-timestamp.ext  [name是给定的文件名,timestamp是日志轮换格式的时间(2006-01-02T15-04-05.000)]

 

清理旧的日志文件策略

 每当创建新的日志文件时,旧的日志文件都可能被删除。删除会根据MaxAge和MaxBackups的参数设置

 1. 时间戳早于MaxAge天的文件都会被删除,如果MaxAge为0,则不会根据MaxAge删除日志文件

 2. MaxBackups是要保留的最大旧日志文件数,用来控制该程序日志文件的最大大小。早于MaxBackups数之前的文件都会被删除,如果MaxBackups为0,则不会根据MaxBackups进行删除日志文件

 3. 如果MaxAge 和 MaxBackups都为0,则不会删除日志文件

 

源码分析

    核心结构体Logger

type Logger struct {
	// Filename is the file to write logs to.  Backup log files will be retained
	// in the same directory.  It uses <processname>-lumberjack.log in
	// os.TempDir() if empty.
	//写日志的文件名称
	Filename string `json:"filename" yaml:"filename"`

	// MaxSize is the maximum size in megabytes of the log file before it gets
	// rotated. It defaults to 100 megabytes.
	//每个日志文件长度的最大大小,默认100M。
	MaxSize int `json:"maxsize" yaml:"maxsize"`

	// MaxAge is the maximum number of days to retain old log files based on the
	// timestamp encoded in their filename.  Note that a day is defined as 24
	// hours and may not exactly correspond to calendar days due to daylight
	// savings, leap seconds, etc. The default is not to remove old log files
	// based on age.
	//日志保留的最大天数(只保留最近多少天的日志)
	MaxAge int `json:"maxage" yaml:"maxage"`

	// MaxBackups is the maximum number of old log files to retain.  The default
	// is to retain all old log files (though MaxAge may still cause them to get
	// deleted.)
	//只保留最近多少个日志文件,用于控制程序总日志的大小
	MaxBackups int `json:"maxbackups" yaml:"maxbackups"`

	// LocalTime determines if the time used for formatting the timestamps in
	// backup files is the computer's local time.  The default is to use UTC
	// time.
	//是否使用本地时间,默认使用UTC时间
	LocalTime bool `json:"localtime" yaml:"localtime"`

	// Compress determines if the rotated log files should be compressed
	// using gzip.
	// 是否压缩日志文件,压缩方法gzip
	Compress bool `json:"compress" yaml:"compress"`

	size int64   //记录当前日志文件的字节数
	file *os.File  //当前的日志文件
	mu   sync.Mutex

	millCh    chan bool
	startMill sync.Once
}

  

核心方法Write

func (l *Logger) Write(p []byte) (n int, err error) {
	l.mu.Lock()
	defer l.mu.Unlock()

	writeLen := int64(len(p))
	if writeLen > l.max() {
		return 0, fmt.Errorf(
			"write length %d exceeds maximum file size %d", writeLen, l.max(),
		)
	}

	if l.file == nil {
		if err = l.openExistingOrNew(len(p)); err != nil {
			return 0, err
		}
	}

	//如果写入将导致日志文件大于MaxSize,则调用rotate方法进行日志文件的切换
	if l.size+writeLen > l.max() {
		if err := l.rotate(); err != nil {
			return 0, err
		}
	}

	n, err = l.file.Write(p)  //将数据写入日志文件
	l.size += int64(n)

	return n, err
}

       Write方法实现io.Writer接口,用于向日志文件中写入信息,如果写入将导致日志文件大于MaxSize,则将当前文件关闭,将其重命名为包括当前时间的时间戳,并使用原始日志文件名创建新的日志文件。如果一次写入的长度大于MaxSize,则返回错误。

     从Write方法中我们看到每次写入日志前都会检测本次的写入是否会导致当前日志文件的大小大于MaxSize,如果大于则调用

rotate方法进行处理。

    rotate方法

func (l *Logger) rotate() error {
	//关闭当前日志文件
	if err := l.close(); err != nil {
		return err
	}
	//把当前日志文件进行重命名,并创建一个新的日志文件用于写入日志
	if err := l.openNew(); err != nil {
		return err
	}
	l.mill()
	return nil
}

       rotate方法用于日志切换,关闭现有的日志文件,并调用openNew方法把当前关闭的日志文件重命名,并创建一个新的日志文件进行写入日志。调用mill方法根据配置进行日志删除或压缩操作。

 

openNew方法

func (l *Logger) openNew() error {
	//如果目录不存在,则进行创建
	err := os.MkdirAll(l.dir(), 0744)
	if err != nil {
		return fmt.Errorf("can't make directories for new logfile: %s", err)
	}

	name := l.filename()
	mode := os.FileMode(0644)
	info, err := os_Stat(name)  //获取当前文件的信息
	if err == nil {
		// Copy the mode off the old logfile.
		mode = info.Mode()
		// move the existing file
		newname := backupName(name, l.LocalTime)  //获取要转换的日志名称
		if err := os.Rename(name, newname); err != nil { //将当前文件重命名
			return fmt.Errorf("can't rename log file: %s", err)
		}

		// this is a no-op anywhere but linux
		if err := chown(name, info); err != nil {  //改变linux系统下文件的权限
			return err
		}
	}

	// we use truncate here because this should only get called when we've moved
	// the file ourselves. if someone else creates the file in the meantime,
	// just wipe out the contents.
	//创建新的日志文件
	f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
	if err != nil {
		return fmt.Errorf("can't open new logfile: %s", err)
	}
	l.file = f
	l.size = 0
	return nil
}

openNew方法比较简单,主要是把当前关闭的日志文件重命名,并创建一个新的日志文件进行写入日志。

 

mill相关的函数

func (l *Logger) mill() {
	l.startMill.Do(func() {
		l.millCh = make(chan bool, 1)
		go l.millRun()
	})
	select {
	case l.millCh <- true:
	default:
	}
}

func (l *Logger) millRun() {
	for _ = range l.millCh {
		// what am I going to do, log this?
		_ = l.millRunOnce()
	}
}

func (l *Logger) millRunOnce() error {
	if l.MaxBackups == 0 && l.MaxAge == 0 && !l.Compress {
		return nil
	}

	//获取老的日志文件
	files, err := l.oldLogFiles()
	if err != nil {
		return err
	}

	var compress, remove []logInfo

	//MaxBackups大于0 并且 当前的文件数大于MaxBackups,说明有需要删除的日志文件
	if l.MaxBackups > 0 && l.MaxBackups < len(files) {
		preserved := make(map[string]bool)
		var remaining []logInfo
		for _, f := range files {  //遍历每一个文件
			// Only count the uncompressed log file or the
			// compressed log file, not both.
			fn := f.Name()  //获取文件名称
			//如果文件名以.gz结尾,则从文件名称删除.gz
			if strings.HasSuffix(fn, compressSuffix) {
				fn = fn[:len(fn)-len(compressSuffix)]
			}
			preserved[fn] = true

			if len(preserved) > l.MaxBackups {
				remove = append(remove, f)  //需要删除的文件列表
			} else {
				remaining = append(remaining, f)  //保留的文件列表
			}
		}
		files = remaining
	}
	if l.MaxAge > 0 {
		diff := time.Duration(int64(24*time.Hour) * int64(l.MaxAge))
		cutoff := currentTime().Add(-1 * diff)  //需要删除的时间节点

		var remaining []logInfo
		for _, f := range files { //遍历保留的日志文件
			if f.timestamp.Before(cutoff) {  //需要删除的日志文件(超过了保留时间)
				remove = append(remove, f)  //需要删除的文件列表
			} else {
				remaining = append(remaining, f)
			}
		}
		files = remaining
	}

	if l.Compress { //获取需要压缩的文件列表
		for _, f := range files {
			if !strings.HasSuffix(f.Name(), compressSuffix) {
				compress = append(compress, f)
			}
		}
	}

	for _, f := range remove {  //需要删除的文件列表
		errRemove := os.Remove(filepath.Join(l.dir(), f.Name()))
		if err == nil && errRemove != nil {
			err = errRemove
		}
	}
	for _, f := range compress {  //压缩每一个需要删除的日志文件
		fn := filepath.Join(l.dir(), f.Name())
		errCompress := compressLogFile(fn, fn+compressSuffix)
		if err == nil && errCompress != nil {
			err = errCompress
		}
	}

	return err
}

mill方法会开启一个goroutine进行处理,处理的核心方法是millRunOnce, millRunOnce方法会根据配置判断是否需要删除的历史日志文件,如果有则删除。如果配置的压缩,则会对未压缩的历史文件进行压缩。

 

Rotate方法
func (l *Logger) Rotate() error {
	l.mu.Lock()
	defer l.mu.Unlock()
	return l.rotate()
}

Rotate方法是对外提供手动切换日志文件的功能,同步调用rotate方法

 

Close方法

func (l *Logger) Close() error {
	l.mu.Lock()
	defer l.mu.Unlock()
	return l.close()
}

// close closes the file if it is open.
//关闭日志文件,将file置为nil
func (l *Logger) close() error {
	if l.file == nil {
		return nil
	}
	err := l.file.Close()
	l.file = nil
	return err
}

 

其他的一些方法就不在累述,有兴趣可自行阅读