
簡介
在很早之前的文章中,我們介紹過 Go 标準日志庫
log
和結構化的日志庫
logrus
。在熱點函數中記錄日志對日志庫的執行性能有較高的要求,不能影響正常邏輯的執行時間。
uber
開源的日志庫
zap
,對性能和記憶體配置設定做了極緻的優化。
快速使用
先安裝:
$ go get go.uber.org/zap
後使用:
package main
import (
"time"
"go.uber.org/zap"
)
func main() {
logger := zap.NewExample()
defer logger.Sync()
url := "http://example.org/api"
logger.Info("failed to fetch URL",
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
"url", url,
"attempt", 3,
"backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)
}
zap
庫的使用與其他的日志庫非常相似。先建立一個
logger
,然後調用各個級别的方法記錄日志(
Debug/Info/Error/Warn
)。
zap
提供了幾個快速建立
logger
的方法,
zap.NewExample()
、
zap.NewDevelopment()
、
zap.NewProduction()
,還有高度定制化的建立方法
zap.New()
。建立前 3 個
logger
時,
zap
會使用一些預定義的設定,它們的使用場景也有所不同。
Example
适合用在測試代碼中,
Development
在開發環境中使用,
Production
用在生成環境。
zap
底層 API 可以設定緩存,是以一般使用
defer logger.Sync()
将緩存同步到檔案中。
由于
fmt.Printf
之類的方法大量使用
interface{}
和反射,會有不少性能損失,并且增加了記憶體配置設定的頻次。
zap
為了提高性能、減少記憶體配置設定次數,沒有使用反射,而且預設的
Logger
隻支援強類型的、結構化的日志。必須使用
zap
提供的方法記錄字段。
zap
為 Go 語言中所有的基本類型和其他常見類型都提供了方法。這些方法的名稱也比較好記憶,
zap.Type
(
Type
為
bool/int/uint/float64/complex64/time.Time/time.Duration/error
等)就表示該類型的字段,
zap.Typep
以
p
結尾表示該類型指針的字段,
zap.Types
以
s
結尾表示該類型切片的字段。如:
-
:zap.Bool(key string, val bool) Field
字段bool
-
:zap.Boolp(key string, val *bool) Field
指針字段;bool
-
:zap.Bools(key string, val []bool) Field
切片字段。bool
當然也有一些特殊類型的字段:
-
:任意類型的字段;zap.Any(key string, value interface{}) Field
-
:二進制串的字段。zap.Binary(key string, val []byte) Field
當然,每個字段都用方法包一層用起來比較繁瑣。
zap
也提供了便捷的方法
SugarLogger
,可以使用
printf
格式符的方式。調用
logger.Sugar()
即可建立
SugaredLogger
。
SugaredLogger
的使用比
Logger
簡單,隻是性能比
Logger
低 50% 左右,可以用在非熱點函數中。調用
SugarLogger
以
f
結尾的方法與
fmt.Printf
沒什麼差別,如例子中的
Infof
。同時
SugarLogger
還支援以
w
結尾的方法,這種方式不需要先建立字段對象,直接将字段名和值依次放在參數中即可,如例子中的
Infow
。
預設情況下,
Example
輸出的日志為 JSON 格式:
{"level":"info","msg":"failed to fetch URL","url":"http://example.org/api","attempt":3,"backoff":"1s"}
{"level":"info","msg":"failed to fetch URL","url":"http://example.org/api","attempt":3,"backoff":"1s"}
{"level":"info","msg":"Failed to fetch URL: http://example.org/api"}
記錄層級關系
前面我們記錄的日志都是一層結構,沒有嵌套的層級。我們可以使用
zap.Namespace(key string) Field
建構一個
命名空間,後續的
Field
都記錄在此命名空間中:
func main() {
logger := zap.NewExample()
defer logger.Sync()
logger.Info("tracked some metrics",
zap.Namespace("metrics"),
zap.Int("counter", 1),
)
logger2 := logger.With(
zap.Namespace("metrics"),
zap.Int("counter", 1),
)
logger2.Info("tracked some metrics")
}
輸出:
{"level":"info","msg":"tracked some metrics","metrics":{"counter":1}}
{"level":"info","msg":"tracked some metrices","metrics":{"counter":1}}
上面我們示範了兩種
Namespace
的用法,一種是直接作為字段傳入
Debug/Info
等方法,一種是調用
With()
建立一個新的
Logger
,新的
Logger
記錄日志時總是帶上預設的字段。
With()
方法實際上是建立了一個新的
Logger
:
// src/go.uber.org/zap/logger.go
func (log *Logger) With(fields ...Field) *Logger {
if len(fields) == 0 {
return log
}
l := log.clone()
l.core = l.core.With(fields)
return l
}
定制 Logger
Logger
調用
NexExample()/NewDevelopment()/NewProduction()
這 3 個方法,
zap
使用預設的配置。我們也可以手動調整,配置結構如下:
// src/go.uber.org/zap/config.go
type Config struct {
Level AtomicLevel `json:"level" yaml:"level"`
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"`
}
-
:日志級别;Level
-
:輸出的日志格式,預設為 JSON;Encoding
-
:可以配置多個輸出路徑,路徑可以是檔案路徑和OutputPaths
(标準輸出);stdout
-
:錯誤輸出路徑,也可以是多個;ErrorOutputPaths
-
:每條日志中都會輸出這些值。InitialFields
其中
EncoderConfig
為編碼配置:
// src/go.uber.org/zap/zapcore/encoder.go
type EncoderConfig struct {
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"`
StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
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"`
}
-
:日志中資訊的鍵名,預設為MessageKey
;msg
-
:日志中級别的鍵名,預設為LevelKey
;level
-
:日志中級别的格式,預設為小寫,如EncodeLevel
。debug/info
調用
zap.Config
的
Build()
方法即可使用該配置對象建立一個
Logger
:
func main() {
rawJSON := []byte(`{
"level":"debug",
"encoding":"json",
"outputPaths": ["stdout", "server.log"],
"errorOutputPaths": ["stderr"],
"initialFields":{"name":"dj"},
"encoderConfig": {
"messageKey": "message",
"levelKey": "level",
"levelEncoder": "lowercase"
}
}`)
var cfg zap.Config
if err := json.Unmarshal(rawJSON, &cfg); err != nil {
panic(err)
}
logger, err := cfg.Build()
if err != nil {
panic(err)
}
defer logger.Sync()
logger.Info("server start work successfully!")
}
上面建立一個輸出到标準輸出
stdout
和檔案
server.log
的
Logger
。觀察輸出:
{"level":"info","message":"server start work successfully!","name":"dj"}
使用
NewDevelopment()
建立的
Logger
使用的是如下的配置:
// src/go.uber.org/zap/config.go
func NewDevelopmentConfig() Config {
return Config{
Level: NewAtomicLevelAt(DebugLevel),
Development: true,
Encoding: "console",
EncoderConfig: NewDevelopmentEncoderConfig(),
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
}
func NewDevelopmentEncoderConfig() zapcore.EncoderConfig {
return zapcore.EncoderConfig{
// Keys can be anything except the empty string.
TimeKey: "T",
LevelKey: "L",
NameKey: "N",
CallerKey: "C",
MessageKey: "M",
StacktraceKey: "S",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
}
NewProduction()
的配置可自行檢視。
選項
NewExample()/NewDevelopment()/NewProduction()
這 3 個函數可以傳入若幹類型為
zap.Option
的選項,進而定制
Logger
的行為。又一次見到了
選項模式!!
zap
提供了豐富的選項供我們選擇。
輸出檔案名和行号
調用
zap.AddCaller()
傳回的選項設定輸出檔案名和行号。但是有一個前提,必須設定配置對象
Config
中的
CallerKey
字段。也是以
NewExample()
不能輸出這個資訊(它的
Config
沒有設定
CallerKey
)。
func main() {
logger, _ := zap.NewProduction(zap.AddCaller())
defer logger.Sync()
logger.Info("hello world")
}
輸出:
{"level":"info","ts":1587740198.9508286,"caller":"caller/main.go:9","msg":"hello world"}
Info()
方法在
main.go
的第 9 行被調用。
AddCaller()
與
zap.WithCaller(true)
等價。
有時我們稍微封裝了一下記錄日志的方法,但是我們希望輸出的檔案名和行号是調用封裝函數的位置。這時可以使用
zap.AddCallerSkip(skip int)
向上跳 1 層:
func Output(msg string, fields ...zap.Field) {
zap.L().Info(msg, fields...)
}
func main() {
logger, _ := zap.NewProduction(zap.AddCaller(), zap.AddCallerSkip(1))
defer logger.Sync()
zap.ReplaceGlobals(logger)
Output("hello world")
}
輸出:
{"level":"info","ts":1587740501.5592482,"caller":"skip/main.go:15","msg":"hello world"}
輸出在
main
函數中調用
Output()
的位置。如果不指定
zap.AddCallerSkip(1)
,将輸出
"caller":"skip/main.go:6"
,這是在
Output()
函數中調用
zap.Info()
的位置。因為這個
Output()
函數可能在很多地方被調用,是以這個位置參考意義并不大。試試看!
輸出調用堆棧
有時候在某個函數進行中遇到了異常情況,因為這個函數可能在很多地方被調用。如果我們能輸出此次調用的堆棧,那麼分析起來就會很友善。我們可以使用
zap.AddStackTrace(lvl zapcore.LevelEnabler)
達成這個目的。該函數指定
lvl
和之上的級别都需要輸出調用堆棧:
func f1() {
f2("hello world")
}
func f2(msg string, fields ...zap.Field) {
zap.L().Warn(msg, fields...)
}
func main() {
logger, _ := zap.NewProduction(zap.AddStacktrace(zapcore.WarnLevel))
defer logger.Sync()
zap.ReplaceGlobals(logger)
f1()
}
将
zapcore.WarnLevel
傳入
AddStacktrace()
,之後
Warn()/Error()
等級别的日志會輸出堆棧,
Debug()/Info()
這些級别不會。運作結果:
{"level":"warn","ts":1587740883.4965692,"caller":"stacktrace/main.go:13","msg":"hello world","stacktrace":"main.f2ntd:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:13nmain.f1ntd:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:9nmain.mainntd:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:22nruntime.mainntC:/Go/src/runtime/proc.go:203"}
把
stacktrace
單獨拉出來:
main.f2
d:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:13
main.f1
d:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:9
main.main
d:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:22
runtime.main
C:/Go/src/runtime/proc.go:203
很清楚地看到調用路徑。
全局 Logger
Logger
為了友善使用,
zap
提供了兩個全局的
Logger
,一個是
*zap.Logger
,可調用
zap.L()
獲得;另一個是
*zap.SugaredLogger
,可調用
zap.S()
獲得。需要注意的是,全局的
Logger
預設并不會記錄日志!它是一個無實際效果的
Logger
。看源碼:
// go.uber.org/zap/global.go
var (
_globalMu sync.RWMutex
_globalL = NewNop()
_globalS = _globalL.Sugar()
)
我們可以使用
ReplaceGlobals(logger *Logger) func()
将
logger
設定為全局的
Logger
,該函數傳回一個無參函數,用于恢複全局
Logger
設定:
func main() {
zap.L().Info("global Logger before")
zap.S().Info("global SugaredLogger before")
logger := zap.NewExample()
defer logger.Sync()
zap.ReplaceGlobals(logger)
zap.L().Info("global Logger after")
zap.S().Info("global SugaredLogger after")
}
輸出:
{"level":"info","msg":"global Logger after"}
{"level":"info","msg":"global SugaredLogger after"}
可以看到在調用
ReplaceGlobals
之前記錄的日志并沒有輸出。
預設日志字段
如果每條日志都要記錄一些共用的字段,那麼使用
zap.Fields(fs ...Field)
建立的選項。例如在伺服器日志中記錄可能都需要記錄
serverId
和
serverName
:
func main() {
logger := zap.NewExample(zap.Fields(
zap.Int("serverId", 90),
zap.String("serverName", "awesome web"),
))
logger.Info("hello world")
}
輸出:
{"level":"info","msg":"hello world","serverId":90,"serverName":"awesome web"}
與标準日志庫搭配使用
如果項目一開始使用的是标準日志庫
log
,後面想轉為
zap
。這時不必修改每一個檔案。我們可以調用
zap.NewStdLog(l *Logger) *log.Logger
傳回一個标準的
log.Logger
,内部實際上寫入的還是我們之前建立的
zap.Logger
:
func main() {
logger := zap.NewExample()
defer logger.Sync()
std := zap.NewStdLog(logger)
std.Print("standard logger wrapper")
}
輸出:
{"level":"info","msg":"standard logger wrapper"}
很友善不是嗎?我們還可以使用
NewStdLogAt(l *logger, level zapcore.Level) (*log.Logger, error)
讓标準接口以
level
級别寫入内部的
*zap.Logger
。
如果我們隻是想在一段代碼内使用标準日志庫
log
,其它地方還是使用
zap.Logger
。可以調用
RedirectStdLog(l *Logger) func()
。它會傳回一個無參函數恢複設定:
func main() {
logger := zap.NewExample()
defer logger.Sync()
undo := zap.RedirectStdLog(logger)
log.Print("redirected standard library")
undo()
log.Print("restored standard library")
}
看前後輸出變化:
{"level":"info","msg":"redirected standard library"}
2020/04/24 22:13:58 restored standard library
當然
RedirectStdLog
也有一個對應的
RedirectStdLogAt
以特定的級别調用内部的
*zap.Logger
方法。
總結
zap
用在日志性能和記憶體配置設定比較關鍵的地方。本文僅介紹了
zap
庫的基本使用,子包
zapcore
中有更底層的接口,可以定制豐富多樣的
Logger
。
大家如果發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上送出 issue
參考
- zap GitHub:https://github.com/jordan-wright/zap
- Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
我
我的部落格:https://darjun.github.io
歡迎關注我的微信公衆号【GoUpUp】,共同學習,一起進步~