首頁 > 軟體

Go語言程式設計實現支援六種級別的紀錄檔庫 

2022-05-16 13:00:10

前言

Golang標準紀錄檔庫提供的紀錄檔輸出方法有Print、Fatal、Panic等,沒有常見的Debug、Info、Error等紀錄檔級別,用起來不太順手。這篇文章就來手擼一個自己的紀錄檔庫,可以記錄不同級別的紀錄檔。

其實對於追求簡單來說,Golang標準紀錄檔庫的三個輸出方法也夠用了,理解起來也很容易:

  • Print用於記錄一個普通的程式紀錄檔,開發者想記點什麼都可以。
  • Fatal用於記錄一個導致程式崩潰的紀錄檔,並會退出程式。
  • Panic用於記錄一個異常紀錄檔,並觸發panic。

不過對於用慣了Debug、Info、Error的人來說,還是有點不習慣;對於想更細緻的區分紀錄檔級別的需求,標準紀錄檔庫還提供了一個通用的Output方法,開發者在要輸出的字串中加入級別也是可以的,但總是有點彆扭,不夠直接。

目前市面上也已經有很多優秀的三方紀錄檔庫,比如uber開源的zap,常見的還有zerolog、logrus等。不過我這裡還是想自己手擼一個,因為大多數開源產品都不會完全貼合自己的需求,有很多自己用不上的功能,這會增加系統的複雜性,有沒有隱藏的坑也很難說,當然自己入坑的可能性也很大;再者看了官方紀錄檔庫的實現之後,感覺可以簡單封裝下即可實現自己想要的功能,能夠hold住。

初始需求

我這裡的初始需求是:

  • 將紀錄檔寫入磁碟檔案,每個月一個資料夾,每個小時一個檔案。
  • 支援常見紀錄檔級別:Trace、Debug、Info、Warn、Error、Fatal,並且程式能夠設定紀錄檔級別。

我給這個紀錄檔庫取名為ylog,預期的使用方法如下:

ylog.SetLevel(LevelInfo)
ylog.Debug("I am a debug log.")
ylog.Info("I am a Info log.")

技術實現

型別定義

需要定義一個結構體,儲存紀錄檔級別、要寫入的檔案等資訊。

type FileLogger struct {
	lastHour int64
	file     *os.File
	Level    LogLevel
	mu       sync.Mutex
	iLogger  *log.Logger
	Path     string
}

來看一下這幾個引數:

lastHour 用來記錄建立紀錄檔檔案時的小時數,如果小時變了,就要建立新的紀錄檔檔案。

file 當前使用的紀錄檔檔案。

Level 當前使用的紀錄檔級別。

mu 因為可能在不同的go routine中寫紀錄檔,需要一個互斥體保證紀錄檔檔案不會重複建立。

iLogger 標準紀錄檔庫範例,因為這裡是封裝了標準紀錄檔庫。

Path 紀錄檔輸出的最上層目錄,比如程式根目錄下的logs目錄,這裡就儲存一個字串:logs。

紀錄檔級別

先把紀錄檔級別定義出來,這裡紀錄檔級別其實是int型別,從0到5,級別不斷升高。

如果設定為ToInfo,則Info級別及比Info級別高的紀錄檔都能輸出。

type LogLevel int
const (
	LevelTrace LogLevel = iota
	LevelDebug
	LevelInfo
	LevelWarn
	LevelError
	LevelFatal
)

上文提到可以在Output方法的引數中加入紀錄檔級別,這裡就通過封裝Output方法來實現不同級別的紀錄檔記錄方法。這裡貼出其中一個方法,封裝的方式都一樣,就不全都貼出來了:

func (l *FileLogger) CanInfo() bool {
	return l.Level <= LevelInfo 
}
func (l *FileLogger) Info(v ...any) {
	if l.CanInfo() {
		l.ensureFile()
		v = append([]any{"Info "}, v...)
		l.iLogger.Output(2, fmt.Sprintln(v...))
	}
}

輸出紀錄檔前做了三件事:

  • 判斷紀錄檔級別,如果設定的紀錄檔級別小於等於當前輸出級別,則可以輸出。
  • 確保紀錄檔檔案已經建立好,後邊會講如何確保。
  • 將紀錄檔級別前插到紀錄檔字串中。

然後呼叫標準庫的Output函數輸出紀錄檔,這裡第一個引數是為了獲取到當前正在寫紀錄檔的程式檔名,傳入的是在程式呼叫棧中進行查詢的深度值,這裡用2就正好。

寫到檔案

標準庫的log是支援輸出到多種目標的,只要實現了io.Write介面:

type Writer interface {
	Write(p []byte) (n int, err error)
}

因為檔案物件也實現了這個介面,所以這裡可以建立os.File的範例,並把它設定到內嵌的標準紀錄檔庫範例,也就是設定到前邊建立的FileLogger中的iLogger中。這個操作在ensureFile方法中,看一下這個檔案的實現:

func (l *FileLogger) ensureFile() (err error) {
	currentTime := time.Now()
	if l.file == nil {
		l.mu.Lock()
		defer l.mu.Unlock()
		if l.file == nil {
			l.file, err = createFile(&l.Path, &currentTime)
			l.iLogger.SetOutput(l.file)
			l.iLogger.SetFlags(log.Lshortfile | log.Ldate | log.Ltime | log.Lmicroseconds)
			l.lastHour = getTimeHour(&currentTime)
		}
		return
	}
	currentHour := getTimeHour(&currentTime)
	if l.lastHour != currentHour {
		l.mu.Lock()
		defer l.mu.Unlock()
		if l.lastHour != currentHour {
			_ = l.file.Close()
			l.file, err = createFile(&l.Path, &currentTime)
			l.iLogger.SetOutput(l.file)
			l.iLogger.SetFlags(log.Llongfile | log.Ldate | log.Ltime)
			l.lastHour = getTimeHour(&currentTime)
		}
	}
	return
}

這裡稍微有點複雜,基本邏輯是:如果檔案範例不存在,則建立;如果需要建立新的檔案,則先關閉舊的檔案再建立新的檔案。

更改檔案範例時需要加鎖,否則可能多次操作,出現預期之外的情況。

設定輸出到檔案後,標準log庫的Output方法就會將紀錄檔輸出到這個檔案了。

預設實現

經過上邊一系列操作,這個FileLogger就可以使用了:

var logger = NewFileLogger(LevelInfo, "logs")
logger.Info("This is a info.")

不過和最初設想的用法有點差別:ylog.Info("xxxx")

這需要在ylog包中再定義一個名為Info的公開函數,可以在這個公開函數中呼叫一個預設建立的FileLogger範例,程式碼是這樣的:

var stdPath = "logs"
var std = NewFileLogger(LevelInfo, stdPath)
func Trace(v ...any) {
	if std.CanTrace() {
		std.ensureFile()
		v = append([]any{"Trace"}, v...)
		std.iLogger.Output(2, fmt.Sprintln(v...))
	}
}

注意這裡沒有呼叫std的Trace方法,這是因為Output中的第一個引數,如果巢狀呼叫std.Trace,則多了一層,這個引數就得設定為3,但是自己建立範例呼叫Trace時這個引數需要為2,這就產生衝突了。

經過以上這些操作,就可以實現預期的紀錄檔操作了:

ylog.SetLevel(LevelInfo)
ylog.Debug("I am a debug log.")
ylog.Info("I am a Info log.")

完整的程式程式碼:https://github.com/bosima/ylog/tree/v1.0.1

下篇文章將繼續改造這個紀錄檔庫,支援輸出Json格式的紀錄檔,以及輸出紀錄檔到Kafka,更多關於Golan紀錄檔庫的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com