Zap
Zap 是一個用 Go 構建的,快速的 ,結構化,級別化的日志組件。
官方倉庫:uber-go/zap: Blazing fast, structured, leveled logging in Go. (github.com)
官方文檔:zap package - go.uber.org/zap - Go Packages
安裝
go get -u go.uber.org/zap快速開始
官方給出了兩個快速開始的示例,兩個都是產品級別的日志,第一個是一個支持printf風格但是性能相對較低的Sugar。
logger, _ := zap.NewProduction()
defer logger.Sync() // 在程序結束時將緩存同步到文件中
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
"url", url,
"attempt", 3,
"backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)第二個是性能比較好,但是僅支持強類型輸出的日志·logger
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("failed to fetch URL",
// Structured context as strongly typed Field values.
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)TIP
Zap 的使用非常簡單,麻煩的點在於配置出一個適合自己項目的日志,官方例子很少,要多讀源代碼注釋。
配置
一般來說日志的配置都是寫在配置文件裡的,Zap 的配置也支持通過配置文件反序列化,但是僅支持基礎的配置,即便是高級配置官方給出的例子也是十分簡潔,並不足以投入使用,所以要詳細講一下細節的配置。
首先看一下總體的配置結構體,需要先搞明白裡面的每一個字段的含義
type Config struct {
// 最小日志級別
Level AtomicLevel `json:"level" yaml:"level"`
// 開發模式,主要影響堆棧跟蹤
Development bool `json:"development" yaml:"development"`
// 調用者追蹤
DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
// 堆棧跟蹤
DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
// 采樣,在限制日志對性能佔用的情況下僅記錄部分比較有代表性的日志,等於日志選擇性記錄
Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
// 編碼,分為json和console兩種模式
Encoding string `json:"encoding" yaml:"encoding"`
// 編碼配置,主要是一些輸出格式化的配置
EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
// 日志文件輸出路徑
OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
// 錯誤文件輸出路徑
ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
// 給日志添加一些默認輸出的內容
InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}如下是關於編碼配置的細節
type EncoderConfig struct {
// 鍵值,如果key為空,那麼對於的屬性將不會輸出
MessageKey string `json:"messageKey" yaml:"messageKey"`
LevelKey string `json:"levelKey" yaml:"levelKey"`
TimeKey string `json:"timeKey" yaml:"timeKey"`
NameKey string `json:"nameKey" yaml:"nameKey"`
CallerKey string `json:"callerKey" yaml:"callerKey"`
FunctionKey string `json:"functionKey" yaml:"functionKey"`
StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
SkipLineEnding bool `json:"skipLineEnding" yaml:"skipLineEnding"`
LineEnding string `json:"lineEnding" yaml:"lineEnding"`
// 一些自定義的編碼器
EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"`
EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"`
EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"`
// 日志器名稱編碼器
EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
// 反射編碼器,主要是對於interface{}類型,如果沒有默認jsonencoder
NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
// 控制台輸出間隔字符串
ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}Option是關於一些配置的開關及應用,有很多實現。
type Option interface {
apply(*Logger)
}
// Option的實現
type optionFunc func(*Logger)
func (f optionFunc) apply(log *Logger) {
f(log)
}
// 應用
func Development() Option {
return optionFunc(func(log *Logger) {
log.development = true
})
}這是最常用的日志核心,其內部的字段基本上就代表了我們配置的步驟,也可以參考官方在反序列化配置時的步驟,大致都是一樣的。
type ioCore struct {
// 日志級別
LevelEnabler
// 日志編碼
enc Encoder
// 日志書寫
out WriteSyncer
}zap.Encoder 負責日志的格式化,編碼
zap.WriteSyncer 負責日志的輸出,主要是輸出到文件和控制台
zap.LevelEnabler 最小日志級別,該級別以下的日志不會再通過syncer輸出。
日志編碼
日志編碼主要涉及到對於日志的一些細節的格式化,首先看一下直接使用最原始的日志的輸出。
func TestQuickStart(t *testing.T) {
rawJSON := []byte(`{
"level": "debug",
"encoding": "json",
"outputPaths": ["stdout"],
"errorOutputPaths": ["stderr"],
"initialFields": {"foo": "bar"},
"encoderConfig": {
"messageKey": "message",
"levelKey": "level",
"levelEncoder": "lowercase"
}
}`)
var cfg zap.Config
if err := json.Unmarshal(rawJSON, &cfg); err != nil {
panic(err)
}
logger := zap.Must(cfg.Build())
defer logger.Sync()
logger.Info("logger construction succeeded")
}{"level":"info","message":"logger construction succeeded","foo":"bar"}會發現這行日志有幾個問題:
- 沒有時間
- 沒有調用者的情況,不知道這行日志是哪裡輸出的,不然到時候發生錯誤的話都沒法排查
- 沒有堆棧情況
接下來就一步一步的來解決問題,主要是對zapcore.EncoderConfig來進行改造,首先我們要自己書寫配置文件,不采用官方的直接反序列化。首先自己創建一個配置文件config.yml
# Zap日志配置
zap:
prefix: ZapLogTest
timeFormat: 2006/01/02 - 15:04:05.00000
level: debug
caller: true
stackTrace: false
encode: console
# 日志輸出到哪裡 file | console | both
writer: both
logFile:
maxSize: 20
backups: 5
compress: true
output:
- "./log/output.log"映射到的結構體
// ZapConfig
// @Date: 2023-01-09 16:37:05
// @Description: zap日志配置結構體
type ZapConfig struct {
Prefix string `yaml:"prefix" mapstructure:""prefix`
TimeFormat string `yaml:"timeFormat" mapstructure:"timeFormat"`
Level string `yaml:"level" mapstructure:"level"`
Caller bool `yaml:"caller" mapstructure:"caller"`
StackTrace bool `yaml:"stackTrace" mapstructure:"stackTrace"`
Writer string `yaml:"writer" mapstructure:"writer"`
Encode string `yaml:"encode" mapstructure:"encode"`
LogFile *LogFileConfig `yaml:"logFile" mapstructure:"logFile"`
}
// LogFileConfig
// @Date: 2023-01-09 16:38:45
// @Description: 日志文件配置結構體
type LogFileConfig struct {
MaxSize int `yaml:"maxSize" mapstructure:"maxSize"`
BackUps int `yaml:"backups" mapstructure:"backups"`
Compress bool `yaml:"compress" mapstructure:"compress"`
Output []string `yaml:"output" mapstructure:"output"`
Errput []string `yaml:"errput" mapstructure:"errput"`
}TIP
讀取配置使用Viper,具體代碼省略。
type TimeEncoder func(time.Time, PrimitiveArrayEncoder)TimerEncoder本質上其實是一個函數,我們可以采用官方提供的其他時間編碼器,也可以自行編寫。
func CustomTimeFormatEncoder(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(global.Config.ZapConfig.Prefix + "\t" + t.Format(global.Config.ZapConfig.TimeFormat))
}整體部分如下
func zapEncoder(config *ZapConfig) zapcore.Encoder {
// 新建一個配置
encoderConfig := zapcore.EncoderConfig{
TimeKey: "Time",
LevelKey: "Level",
NameKey: "Logger",
CallerKey: "Caller",
MessageKey: "Message",
StacktraceKey: "StackTrace",
LineEnding: zapcore.DefaultLineEnding,
FunctionKey: zapcore.OmitKey,
}
// 自定義時間格式
encoderConfig.EncodeTime = CustomTimeFormatEncoder
// 日志級別大寫
encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
// 秒級時間間隔
encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
// 簡短的調用者輸出
encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
// 完整的序列化logger名稱
encoderConfig.EncodeName = zapcore.FullNameEncoder
// 最終的日志編碼 json或者console
switch config.Encode {
case "json":
{
return zapcore.NewJSONEncoder(encoderConfig)
}
case "console":
{
return zapcore.NewConsoleEncoder(encoderConfig)
}
}
// 默認console
return zapcore.NewConsoleEncoder(encoderConfig)
}日式輸出
日志輸出分為控制台輸出和文件輸出,我們可以根據配置文件來進行動態配置,並且如果想要進行日志文件切割的話還需要使用另一個第三方的依賴。
go get -u github.com/natefinch/lumberjack最後代碼如下
func zapWriteSyncer(cfg *ZapConfig) zapcore.WriteSyncer {
syncers := make([]zapcore.WriteSyncer, 0, 2)
// 如果開啟了日志控制台輸出,就加入控制台書寫器
if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteConsole {
syncers = append(syncers, zapcore.AddSync(os.Stdout))
}
// 如果開啟了日志文件存儲,就根據文件路徑切片加入書寫器
if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteFile {
// 添加日志輸出器
for _, path := range cfg.LogFile.Output {
logger := &lumberjack.Logger{
Filename: path, //文件路徑
MaxSize: cfg.LogFile.MaxSize, //分割文件的大小
MaxBackups: cfg.LogFile.BackUps, //備份次數
Compress: cfg.LogFile.Compress, // 是否壓縮
LocalTime: true, //使用本地時間
}
syncers = append(syncers, zapcore.Lock(zapcore.AddSync(logger)))
}
}
return zap.CombineWriteSyncers(syncers...)
}日志級別
官方有關於日志級別的枚舉項,直接使用即可。
func zapLevelEnabler(cfg *ZapConfig) zapcore.LevelEnabler {
switch cfg.Level {
case config.DebugLevel:
return zap.DebugLevel
case config.InfoLevel:
return zap.InfoLevel
case config.ErrorLevel:
return zap.ErrorLevel
case config.PanicLevel:
return zap.PanicLevel
case config.FatalLevel:
return zap.FatalLevel
}
// 默認Debug級別
return zap.DebugLevel
}最後構建
func InitZap(config *ZapConfig) *zap.Logger {
// 構建編碼器
encoder := zapEncoder(config)
// 構建日志級別
levelEnabler := zapLevelEnabler(config)
// 最後獲得Core和Options
subCore, options := tee(config, encoder, levelEnabler)
// 創建Logger
return zap.New(subCore, options...)
}
// 將所有合並
func tee(cfg *ZapConfig, encoder zapcore.Encoder, levelEnabler zapcore.LevelEnabler) (core zapcore.Core, options []zap.Option) {
sink := zapWriteSyncer(cfg)
return zapcore.NewCore(encoder, sink, levelEnabler), buildOptions(cfg, levelEnabler)
}
// 構建Option
func buildOptions(cfg *ZapConfig, levelEnabler zapcore.LevelEnabler) (options []zap.Option) {
if cfg.Caller {
options = append(options, zap.AddCaller())
}
if cfg.StackTrace {
options = append(options, zap.AddStacktrace(levelEnabler))
}
return
}最後效果
ZapLogTest 2023/01/09 - 19:44:00.91076 INFO demo/zap.go:49 日志初始化完成