Golang:手擼一個支援六種級別的日誌庫
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, ¤tTime)
l.iLogger.SetOutput(l.file)
l.iLogger.SetFlags(log.Lshortfile | log.Ldate | log.Ltime | log.Lmicroseconds)
l.lastHour = getTimeHour(¤tTime)
}
return
}
currentHour := getTimeHour(¤tTime)
if l.lastHour != currentHour {
l.mu.Lock()
defer l.mu.Unlock()
if l.lastHour != currentHour {
_ = l.file.Close()
l.file, err = createFile(&l.Path, ¤tTime)
l.iLogger.SetOutput(l.file)
l.iLogger.SetFlags(log.Llongfile | log.Ldate | log.Ltime)
l.lastHour = getTimeHour(¤tTime)
}
}
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.")
完整的程式已經上傳到Github,歡迎訪問://github.com/bosima/ylog/tree/v1.0.1
下篇文章將繼續改造這個日誌庫,支援輸出Json格式的日誌,以及輸出日誌到Kafka。
收穫更多架構知識,請關注微信公眾號 螢火架構。原創內容,轉載請註明出處。