Golang日誌框架lumberjack包源碼分析

github地址:  https://github.com/natefinch/lumberjack

 

獲取源碼

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
}

 

其他的一些方法就不在累述,有興趣可自行閱讀

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章