Golang紀錄檔新選擇:slog

2023-09-11 21:00:30

go1.21中,slog這一被Go語言團隊精心設計的結構化紀錄檔包正式落地,本文將帶領讀者上手slog,體會其與傳統log的差異。

WHY

在紀錄檔處理上,我們從前使用的log包缺乏結構化的輸出,導致資訊呈現出來的樣子並非最適合人類閱讀,而slog是一種結構化的紀錄檔,它可以用鍵值對的形式將我們需要的資訊呈現出來,使得處理與分析紀錄檔變得更為容易。

HOW

1. 快速入門

package main

import (
	"log/slog"
)

func main() {
	slog.Info("my first slog msg", "greeting", "hello, slog")
	slog.Error("my secod slog message", "greeting", "hello slog")
	slog.Warn("my third message", "greeting", "hello slog")
}

以上是三條最簡單的slog語句,其結果是這樣的:

2023/09/10 21:51:03 INFO my first slog msg greeting="hello, slog"
2023/09/10 21:51:03 ERROR my secod slog message greeting="hello slog"
2023/09/10 21:51:03 WARN my third message greeting="hello slog"

這三行程式碼中的第一個引數代表了log的message,我們可以看到,此時列印出來的紀錄檔資訊是文字資訊,那如何使得紀錄檔以非純文字(比如json)展現呢?

2. TextHandler和JSONHandler

當我們想要紀錄檔以key-value格式呈現時,我們可以用下面這種方式:

h := slog.NewTextHandler(os.Stderr, nil)
	l := slog.New(h)
	l.Info("greeting", "name", "xxx")

最終結果:

time=2023-09-10T21:58:34.144+08:00 level=INFO msg=greeting name=xxx

當我們想要紀錄檔以json格式呈現時,我們可以使用下面這種方式:

h1 := slog.NewJSONHandler(os.Stderr, nil)
	l1 := slog.New(h1)
	l1.Info("greeting", "name", "xxx")

最終結果:

{"time":"2023-09-10T22:00:04.687003+08:00","level":"INFO","msg":"greeting","name":"xxx"}

slog.NewJSONHandler函數和slog.NewTextHandler函數都會返回一個JsonHandler結構體或是TextHandler的參照,這個結構體會被slog.new函數接受,該函數返回一個Logger結構體的參照,這個logger結構體包含Handler介面,擁有一系列紀錄檔相關函數,是我們最終列印紀錄檔的地方

func NewJSONHandler(w io.Writer, opts *HandlerOptions) *JSONHandler {}
func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler {}

func New(h Handler) *Logger {}

type Logger struct {
	handler Handler // for structured logging
}

type Handler interface {}

如此,我們實現了基本的紀錄檔結構化輸出。

3.紀錄檔設定

我們通過對slog.HandlerOptions設定,可以實現例如 是否輸出紀錄檔來源 等設定;

s := &slog.HandlerOptions{
		AddSource: true,
	}
	slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, s)))
	slog.Info("Test", "greeting", "hello, world")

此處筆者將該s設定為default情況,這樣直接使用slog.info就可以用到之前的設定;

最終結果:

time=2023-09-10T22:11:04.432+08:00 level=INFO source="/Users/wurenyu/Library/Mobile Documents/com~apple~CloudDocs/Go_learn/basic/slog/t1.go:13" msg=Test greeting="hello, world"

可以看到,由於AddSource被設定為true,我們的輸出紀錄檔中多了source這一資訊;

又由於NewTextHandler,所以紀錄檔是以鍵值對的形式輸出的。

再來看這一段程式碼:

opts := slog.HandlerOptions{
AddSource: true,
Level:     slog.LevelError,
}

slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, &opts)))
slog.Info("open file for reading", "name", "foo.txt", "path", "/home/tonybai/demo/foo.txt")
slog.Error("open file error", "err", os.ErrNotExist, "status", 2)

在slog設定中將Level設定為了LevelError,如此,將只能使用slog.error這一級別;

最終輸出結果:

{"time":"2023-09-10T22:13:44.493714+08:00","level":"ERROR","source":{"function":"main.main","file":"/Users/wurenyu/Library/Mobile Documents/com~apple~CloudDocs/Go_learn/basic/slog/t1.go","line":16},"msg":"open file error","err":"file does not exist","status":2}

3. Group形式輸出紀錄檔

baseLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	groupedLogger := baseLogger.WithGroup("TTT")

	// Log with the grouped logger
	groupedLogger.Info("This log entry includes module information.", "test1", "answer1")
	groupedLogger.Warn("This log entry also includes module information.", "test2", "answer2")

上述程式碼首先生成一個叫做baseLogger的logger,然後在這個logger上呼叫方法WithGroup,並傳入引數「TTT」,後面兩行分別輸出info和warn級別的紀錄檔;

最終結果如下:

{"time":"2023-09-10T22:23:28.527786+08:00","level":"INFO","msg":"This log entry includes module information.","TTT":{"test1":"answer1"}}
{"time":"2023-09-10T22:23:28.528019+08:00","level":"WARN","msg":"This log entry also includes module information.","TTT":{"test2":"answer2"}}

可以看到,在groupLogger後面加上的鍵值對都被加在了TTT後面;

不過值得關注的是,slog是支援給logger自定義欄位的,給一個logger加上一個屬性之後,每次用這個logger輸出紀錄檔,都會輸出這個屬性對應的鍵值對,而這個資訊不會被包含在WithGroup函數傳入的引數後面。

風格

個人認為一般不需要在msg中直接傳入程式碼中的資料,msg中應該儘量直接使用constant常數,這樣更可控。

WHAT

以下是slog大致的架構:

全文終。