全面分析Uber的高性能日志庫Zap
簡介
zap 是什么?
?ZAP[1] 是uber 開源的提供快速,結(jié)構(gòu)化,高性能的日志記錄包。
zap 高性能體現(xiàn)在哪里?
在介紹zap包的優(yōu)化部分之前,讓我們看下zap日志庫的工作流程圖

大多數(shù)日志庫提供的方式是基于反射的序列化和字符串格式化,這種方式代價高昂,而 Zap 采取不同的方法。
避免 interface{} 使用強類型設(shè)計
封裝強類型,無反射
使用零分配內(nèi)存的 JSON 編碼器,盡可能避免序列化開銷,它比其他結(jié)構(gòu)化日志包快 4 - 10 倍。
logger.Info("failed to fetch URL",
zap.String("url", "https://baidu.com"),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
使用 sync.Pool 以避免記錄消息時的內(nèi)存分配
詳情在下文 zapcore 模塊介紹。
Example
安裝
go get -u go.uber.org/zap
Zap 提供了兩種類型的 logger
SugaredLogger Logger
在性能良好但不是關(guān)鍵的情況下,使用 SugaredLogger,它比其他結(jié)構(gòu)化的日志包快 4-10 倍,并且支持結(jié)構(gòu)化和 printf 風(fēng)格的APIs。
例一 調(diào)用 NewProduction 創(chuàng)建logger對象
func TestSugar(t *testing.T) {
logger, _ := zap.NewProduction()
// 默認 logger 不緩沖。
// 但由于底層 api 允許緩沖,所以在進程退出之前調(diào)用 Sync 是一個好習(xí)慣。
defer logger.Sync()
sugar := logger.Sugar()
sugar.Infof("Failed to fetch URL: %s", "https://baidu.com")
}
對性能和類型安全要求嚴格的情況下,可以使用 Logger ,它甚至比前者SugaredLogger更快,內(nèi)存分配次數(shù)也更少,但它僅支持強類型的結(jié)構(gòu)化日志記錄。
例二 調(diào)用 NewDevelopment 創(chuàng)建logger對象
func TestLogger(t *testing.T) {
logger, _ := zap.NewDevelopment()
defer logger.Sync()
logger.Info("failed to fetch URL",
// 強類型字段
zap.String("url", "https://baidu.com"),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
}
不需要為整個應(yīng)用程序決定選擇使用 Logger 還是 SugaredLogger ,兩者之間都可以輕松轉(zhuǎn)換。
例三 Logger 與 SugaredLogger 相互轉(zhuǎn)換
// 創(chuàng)建 logger
logger := zap.NewExample()
defer logger.Sync()
// 轉(zhuǎn)換 SugaredLogger
sugar := logger.Sugar()
// 轉(zhuǎn)換 logger
plain := sugar.Desugar()
例四 自定義格式
自定義一個日志消息格式,帶著問題看下列代碼。
debug 級別的日志打印到控制臺了嗎? 最后的 error 會打印到控制臺嗎 ?
package main
import (
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func NewCustomEncoderConfig() zapcore.EncoderConfig {
return zapcore.EncoderConfig{
TimeKey: "ts",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalColorLevelEncoder,
EncodeTime: zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05"),
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
}
func main() {
atom := zap.NewAtomicLevelAt(zap.DebugLevel)
core := zapcore.NewCore(
zapcore.NewConsoleEncoder(NewCustomEncoderConfig()),
zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout)),
atom,
)
logger := zap.New(core, zap.AddCaller(), zap.Development())
defer logger.Sync()
// 配置 zap 包的全局變量
zap.ReplaceGlobals(logger)
// 運行時安全地更改 logger 日記級別
atom.SetLevel(zap.InfoLevel)
sugar := logger.Sugar()
// 問題 1: debug 級別的日志打印到控制臺了嗎?
sugar.Debug("debug")
sugar.Info("info")
sugar.Warn("warn")
sugar.DPanic("dPanic")
// 問題 2: 最后的 error 會打印到控制臺嗎?
sugar.Error("error")
}
結(jié)果見下圖

問題 1:
沒有打印。AtomicLevel 是原子性可更改的動態(tài)日志級別,通過調(diào)用 atom.SetLevel 更改日志級別為 infoLevel 。
問題 2:
沒有打印。zap.Development() 啟用了開發(fā)模式,在開發(fā)模式下 DPanic 函數(shù)會引發(fā) panic,所以最后的 error 不會打印到控制臺。
源碼分析
此次源碼分析基于 Zap 1.16

上圖僅表示 zap 可調(diào)用兩種 logger,沒有表達 Logger 與 SugaredLogger 的關(guān)系,繼續(xù)往下看,你會更理解。
Logger
logger 提供快速,分級,結(jié)構(gòu)化的日志記錄。所有的方法都是安全的,內(nèi)存分配很重要,因此它的 API 有意偏向于性能和類型安全。
[email protected] - logger.go
type Logger struct {
// 實現(xiàn)編碼和輸出的接口
core zapcore.Core
// 記錄器開發(fā)模式,DPanic 等級將記錄 panic
development bool
// 開啟記錄調(diào)用者的行號和函數(shù)名
addCaller bool
// 致命日志采取的操作,默認寫入日志后 os.Exit()
onFatal zapcore.CheckWriteAction
name string
// 設(shè)置記錄器生成的錯誤目的地
errorOutput zapcore.WriteSyncer
// 記錄 >= 該日志等級的堆棧追蹤
addStack zapcore.LevelEnabler
// 避免記錄器認為封裝函數(shù)為調(diào)用方
callerSkip int
// 默認為系統(tǒng)時間
clock Clock
}
在 Example 中分別使用了 NewProduction 和 NewDevelopment ,接下來以這兩個函數(shù)開始分析。下圖表示 A 函數(shù)調(diào)用了 B 函數(shù),其中箭頭表示函數(shù)調(diào)用關(guān)系。圖中函數(shù)都會分析到。

NewProduction
從下面代碼中可以看出,此函數(shù)是對 NewProductionConfig().Build(...) 封裝的快捷方式。
[email protected] - logger.go
func NewProduction(options ...Option) (*Logger, error) {
return NewProductionConfig().Build(options...)
}
NewProductionConfig
在 InfoLevel 及更高級別上啟用了日志記錄。它使用 JSON 編碼器,寫入 stderr,啟用采樣。
[email protected] - config.go
func NewProductionConfig() Config {
return Config{
// info 日志級別
Level: NewAtomicLevelAt(InfoLevel),
// 非開發(fā)模式
Development: false,
// 采樣設(shè)置
Sampling: &SamplingConfig{
Initial: 100, // 相同日志級別下相同內(nèi)容每秒日志輸出數(shù)量
Thereafter: 100, // 超過該數(shù)量,才會再次輸出
},
// JSON 編碼器
Encoding: "json",
// 后面介紹
EncoderConfig: NewProductionEncoderConfig(),
// 輸出到 stderr
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
}
Config 結(jié)構(gòu)體
通過 Config 可以設(shè)置通用的配置項。
[email protected] - config.go
type Config struct {
// 日志級別
Level AtomicLevel `json:"level" yaml:"level"`
// 開發(fā)模式
Development bool `json:"development" yaml:"development"`
// 停止使用調(diào)用方的函數(shù)和行號
DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
// 完全停止使用堆棧跟蹤,默認為 `>=WarnLevel` 使用堆棧跟蹤
DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
// 采樣設(shè)置策略
Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
// 記錄器的編碼,有效值為 'json' 和 'console' 以及通過 `RegisterEncoder` 注冊的有效編碼
Encoding string `json:"encoding" yaml:"encoding"`
// 編碼器選項
EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
// 日志的輸出路徑
OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
// zap 內(nèi)部錯誤的輸出路徑
ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
// 添加到根記錄器的字段的集合
InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}
NewDevelopment
從下面代碼中可以看出,此函數(shù)是對 NewDevelopmentConfig().Build(...) 封裝的快捷方式
[email protected] - logger.go
func NewDevelopment(options ...Option) (*Logger, error) {
return NewDevelopmentConfig().Build(options...)
}
NewDevelopmentConfig
此函數(shù)在 DebugLevel 及更高版本上啟用日志記錄,它使用 console 編碼器,寫入 stderr,禁用采樣。
[email protected] - config.go
func NewDevelopmentConfig() Config {
return Config{
// debug 等級
Level: NewAtomicLevelAt(DebugLevel),
// 開發(fā)模式
Development: true,
// console 編碼器
Encoding: "console",
EncoderConfig: NewDevelopmentEncoderConfig(),
// 輸出到 stderr
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
}
NewProductionEncoderConfig 和 NewDevelopmentEncoderConfig 都是返回編碼器配置。
[email protected] - config.go
type EncoderConfig struct {
// 設(shè)置 編碼為 JSON 時的 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"`
// 配置行分隔符
LineEnding string `json:"lineEnding" yaml:"lineEnding"`
// 配置常見復(fù)雜類型的基本表示形式。
EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"`
EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"`
EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"`
// 日志名稱,此參數(shù)可選
EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
// 配置 console 編碼器使用的字段分隔符,默認 tab
ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}
NewProductionEncoderConfig
[email protected] - config.go
func NewProductionEncoderConfig() zapcore.EncoderConfig {
return zapcore.EncoderConfig{
TimeKey: "ts",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
MessageKey: "msg",
StacktraceKey: "stacktrace",
// 默認換行符 \n
LineEnding: zapcore.DefaultLineEnding,
// 日志等級序列為小寫字符串,如:InfoLevel被序列化為 "info"
EncodeLevel: zapcore.LowercaseLevelEncoder,
// 時間序列化成浮點秒數(shù)
EncodeTime: zapcore.EpochTimeEncoder,
// 時間序列化,Duration為經(jīng)過的浮點秒數(shù)
EncodeDuration: zapcore.SecondsDurationEncoder,
// 以 包名/文件名:行數(shù) 格式序列化
EncodeCaller: zapcore.ShortCallerEncoder,
}
}
該配置會輸出如下結(jié)果,此結(jié)果出處參見 Example 中的例一
{"level":"info","ts":1620367988.461055,"caller":"test/use_test.go:24","msg":"Failed to fetch URL: https://baidu.com"}
NewDevelopmentEncoderConfig
[email protected] - config.go
func NewDevelopmentEncoderConfig() zapcore.EncoderConfig {
return zapcore.EncoderConfig{
// keys 值可以是任意非空的值
TimeKey: "T",
LevelKey: "L",
NameKey: "N",
CallerKey: "C",
FunctionKey: zapcore.OmitKey,
MessageKey: "M",
StacktraceKey: "S",
// 默認換行符 \n
LineEnding: zapcore.DefaultLineEnding,
// 日志等級序列為大寫字符串,如:InfoLevel被序列化為 "INFO"
EncodeLevel: zapcore.CapitalLevelEncoder,
// 時間格式化為 ISO8601 格式
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
// // 以 包名/文件名:行數(shù) 格式序列化
EncodeCaller: zapcore.ShortCallerEncoder,
}
}
該配置會輸出如下結(jié)果,此結(jié)果出處參見 Example 中的例二
2021-05-07T14:14:12.434+0800 INFO test/use_test.go:31 failed to fetch URL {"url": "https://baidu.com", "attempt": 3, "backoff": "1s"}
NewProductionConfig 和 NewDevelopmentConfig 返回 config 調(diào)用 Build 函數(shù)返回 logger,接下來我們看看這個函數(shù)。
[email protected] - config.go
func (cfg Config) Build(opts ...Option) (*Logger, error) {
enc, err := cfg.buildEncoder()
if err != nil {
return nil, err
}
sink, errSink, err := cfg.openSinks()
if err != nil {
return nil, err
}
if cfg.Level == (AtomicLevel{}) {
return nil, fmt.Errorf("missing Level")
}
log := New(
zapcore.NewCore(enc, sink, cfg.Level),
cfg.buildOptions(errSink)...,
)
if len(opts) > 0 {
log = log.WithOptions(opts...)
}
return log, nil
}
從上面的代碼中,通過解析 config 的參數(shù),調(diào)用 New 方法來創(chuàng)建 Logger。在 Example 中例四,就是調(diào)用 New 方法來自定義 Logger。
SugaredLogger
Logger 作為 SugaredLogger 的屬性,這個封裝優(yōu)點在于不是很在乎性能的情況下,可以快速調(diào)用Logger。所以名字為加了糖的 Logger。
[email protected] - logger.go
type SugaredLogger struct {
base *Logger
}
zap.ReplaceGlobals(logger) // 重新配置全局變量
zap.S().Info("SugaredLogger") // S 返回全局 SugaredLogger
zap.L().Info("logger") // L 返回全局 logger
與Logger不同,SugaredLogger不強制日志結(jié)構(gòu)化。所以對于每個日志級別,都提供了三種方法。

[email protected] - sugar.go
以 info 級別為例,相關(guān)的三種方法。
// Info 使用 fmt.Sprint 構(gòu)造和記錄消息。
func (s *SugaredLogger) Info(args ...interface{}) {
s.log(InfoLevel, "", args, nil)
}
// Infof 使用 fmt.Sprintf 記錄模板消息。
func (s *SugaredLogger) Infof(template string, args ...interface{}) {
s.log(InfoLevel, template, args, nil)
}
// Infow 記錄帶有其他上下文的消息
func (s *SugaredLogger) Infow(msg string, keysAndValues ...interface{}) {
s.log(InfoLevel, msg, nil, keysAndValues)
}
在 sugar.Infof("...") 打上斷點,從這開始追蹤源碼。

在調(diào)試代碼之前,先給大家看一下SugaredLogger 的 Infof 函數(shù)的調(diào)用的大致工作流,其中不涉及采樣等。

Info , Infof, Infow 三個函數(shù)都調(diào)用了 log 函數(shù),log 函數(shù)代碼如下
[email protected] - sugar.go
func (s *SugaredLogger) log(lvl zapcore.Level, template string, fmtArgs []interface{}, context []interface{}) {
// 判斷是否啟用的日志級別
if lvl < DPanicLevel && !s.base.Core().Enabled(lvl) {
return
}
// 將參數(shù)合并到語句中
msg := getMessage(template, fmtArgs)
// Check 可以幫助避免分配一個分片來保存字段。
if ce := s.base.Check(lvl, msg); ce != nil {
ce.Write(s.sweetenFields(context)...)
}
}
函數(shù)的第一個參數(shù) InfoLevel 是日志級別,其源碼如下
[email protected] - zapcore/level.go
const (
// Debug 應(yīng)是大量的,且通常在生產(chǎn)狀態(tài)禁用.
DebugLevel = zapcore.DebugLevel
// Info 是默認的記錄優(yōu)先級.
InfoLevel = zapcore.InfoLevel
// Warn 比 info 更重要.
WarnLevel = zapcore.WarnLevel
// Error 是高優(yōu)先級的,如果程序順利不應(yīng)該產(chǎn)生任何 err 級別日志.
ErrorLevel = zapcore.ErrorLevel
// DPanic 特別重大的錯誤,在開發(fā)模式下引起 panic.
DPanicLevel = zapcore.DPanicLevel
// Panic 記錄消息后調(diào)用 panic.
PanicLevel = zapcore.PanicLevel
// Fatal 記錄消息后調(diào)用 os.Exit(1).
FatalLevel = zapcore.FatalLevel
)
getMessage 函數(shù)處理 template 和 fmtArgs 參數(shù),主要為不同的參數(shù)選擇最合適的方式拼接消息
[email protected] - sugar.go
func getMessage(template string, fmtArgs []interface{}) string {
// 沒有參數(shù)直接返回 template
if len(fmtArgs) == 0 {
return template
}
// 此處調(diào)用 Sprintf 會使用反射
if template != "" {
return fmt.Sprintf(template, fmtArgs...)
}
// 消息為空并且有一個參數(shù),返回該參數(shù)
if len(fmtArgs) == 1 {
if str, ok := fmtArgs[0].(string); ok {
return str
}
}
// 返回所有 fmtArgs
return fmt.Sprint(fmtArgs...)
}
關(guān)于 s.base.Check ,這就需要介紹zapcore ,下面分析相關(guān)模塊。
zapcore
zapcore包 定義并實現(xiàn)了構(gòu)建 zap 的低級接口。通過提供這些接口的替代實現(xiàn),外部包可以擴展 zap 的功能。
[email protected] - zapcore/core.go
// Core 是一個最小的、快速的記錄器接口。
type Core interface {
// 接口,決定一個日志等級是否啟用
LevelEnabler
// 向 core 添加核心上下文
With([]Field) Core
// 檢查是否應(yīng)記錄提供的條目
// 在調(diào)用 write 之前必須先調(diào)用 Check
Check(Entry, *CheckedEntry) *CheckedEntry
// 寫入日志
Write(Entry, []Field) error
// 同步刷新緩存日志(如果有)
Sync() error
}
Check 函數(shù)有兩個入?yún)ⅰ5谝粋€參數(shù)表示一條完整的日志消息,第二個參數(shù)為 nil 時會從 sync.Pool 創(chuàng)建的池中取出*CheckedEntry對象復(fù)用,避免重新分配內(nèi)存。該函數(shù)內(nèi)部調(diào)用 AddCore 實現(xiàn)獲取 *CheckedEntry對象,最后調(diào)用 Write 寫入日志消息。
相關(guān)代碼全部貼在下面,更多介紹請看代碼中的注釋。
[email protected] - zapcore/entry.go
// 一個 entry 表示一個完整的日志消息
type Entry struct {
Level Level
Time time.Time
LoggerName string
Message string
Caller EntryCaller
Stack string
}
// 使用 sync.Pool 復(fù)用臨時對象
var (
_cePool = sync.Pool{New: func() interface{} {
return &CheckedEntry{
cores: make([]Core, 4),
}
}}
)
// 從池中取出 CheckedEntry 并初始化值
func getCheckedEntry() *CheckedEntry {
ce := _cePool.Get().(*CheckedEntry)
ce.reset()
return ce
}
// CheckedEntry 是 enter 和 cores 集合。
type CheckedEntry struct {
Entry
ErrorOutput WriteSyncer
dirty bool // 用于檢測是否重復(fù)使用對象
should CheckWriteAction // 結(jié)束程序的動作
cores []Core
}
// 重置對象
func (ce *CheckedEntry) reset() {
ce.Entry = Entry{}
ce.ErrorOutput = nil
ce.dirty = false
ce.should = WriteThenNoop
for i := range ce.cores {
// 不要保留對 core 的引用!!
ce.cores[i] = nil
}
ce.cores = ce.cores[:0]
}
// 將 entry 寫入存儲的 cores
// 最后將 CheckedEntry 添加到池中
func (ce *CheckedEntry) Write(fields ...Field) {
if ce == nil {
return
}
if ce.dirty {
if ce.ErrorOutput != nil {
// 檢查 CheckedEntry 的不安全重復(fù)使用
fmt.Fprintf(ce.ErrorOutput, "%v Unsafe CheckedEntry re-use near Entry %+v.\n", ce.Time, ce.Entry)
ce.ErrorOutput.Sync()
}
return
}
ce.dirty = true
var err error
// 寫入日志消息
for i := range ce.cores {
err = multierr.Append(err, ce.cores[i].Write(ce.Entry, fields))
}
// 處理內(nèi)部發(fā)生的錯誤
if ce.ErrorOutput != nil {
if err != nil {
fmt.Fprintf(ce.ErrorOutput, "%v write error: %v\n", ce.Time, err)
ce.ErrorOutput.Sync()
}
}
should, msg := ce.should, ce.Message
// 將 CheckedEntry 添加到池中,下次復(fù)用
putCheckedEntry(ce)
// 判斷是否需要 panic 或其它方式終止程序..
switch should {
case WriteThenPanic:
panic(msg)
case WriteThenFatal:
exit.Exit()
case WriteThenGoexit:
runtime.Goexit()
}
}
func (ce *CheckedEntry) AddCore(ent Entry, core Core) *CheckedEntry {
if ce == nil {
// 從池中取 CheckedEntry,減少內(nèi)存分配
ce = getCheckedEntry()
ce.Entry = ent
}
ce.cores = append(ce.cores, core)
return ce
}
Doc
https://pkg.go.dev/go.uber.org/zap
QA
設(shè)計問題
為什么要在Logger性能上花費這么多精力呢?
當(dāng)然,大多數(shù)應(yīng)用程序不會注意到Logger慢的影響:因為它們每次操作會需要幾十或幾百毫秒,所以額外的幾毫秒很無關(guān)緊要。
另一方面,為什么不使用結(jié)構(gòu)化日志快速開發(fā)呢?與其他日志包相比SugaredLogger的使用并不難,Logger使結(jié)構(gòu)化記錄在對性能要求嚴格的環(huán)境中成為可能。在 Go 微服務(wù)的架構(gòu)體系中,使每個應(yīng)用程序甚至稍微更有效地加速執(zhí)行。
為什么沒有Logger和SugaredLogger接口?
不像熟悉的io.Writer和http.Handler、Logger和SugaredLogger接口將包括很多方法。正如 Rob Pike 諺語指出[2]的,"The bigger the interface, the weaker the abstraction"(接口越大,抽象越弱)。接口也是嚴格的,任何更改都需要發(fā)布一個新的主版本,因為它打破了所有第三方實現(xiàn)。
Logger和SugaredLogger成為具體類型并不會犧牲太多抽象,而且它允許我們在不引入破壞性更改的情況下添加方法。您的應(yīng)用程序應(yīng)該定義并依賴只包含您使用的方法的接口。
為什么我的一些日志會丟失?
在啟用抽樣時,通過zap有意地刪除日志。生產(chǎn)配置(如NewProductionConfig()返回的那樣)支持抽樣,這將導(dǎo)致在一秒鐘內(nèi)對重復(fù)日志進行抽樣。有關(guān)為什么啟用抽樣的更多詳細信息,請參見"為什么使用示例應(yīng)用日志"中啟用采樣.
為什么要使用示例應(yīng)用程序日志?
應(yīng)用程序經(jīng)常會遇到錯誤,無論是因為錯誤還是因為用戶使用錯誤。記錄錯誤日志通常是一個好主意,但它很容易使這種糟糕的情況變得更糟:不僅您的應(yīng)用程序應(yīng)對大量錯誤,它還花費額外的CPU周期和I/O記錄這些錯誤日志。由于寫入通常是序列化的,因此在最需要時,logger會限制吞吐量。
采樣通過刪除重復(fù)的日志條目來解決這個問題。在正常情況下,您的應(yīng)用程序會輸出每個記錄。但是,當(dāng)類似的記錄每秒輸出數(shù)百或數(shù)千次時,zap 開始丟棄重復(fù)以保存吞吐量。
為什么結(jié)構(gòu)化的日志 API 除了接受字段之外還可以接收消息?
主觀上,我們發(fā)現(xiàn)在結(jié)構(gòu)化上下文中附帶一個簡短的描述是有幫助的。這在開發(fā)過程中并不關(guān)鍵,但它使調(diào)試和操作不熟悉的系統(tǒng)更加容易。
更具體地說,zap 的采樣算法使用消息來識別重復(fù)的條目。根據(jù)我們的經(jīng)驗,這是一個介于隨機抽樣(通常在調(diào)試時刪除您需要的確切條目)和哈希完整條目(代價高)之間的一個中間方法。
為什么要包括全局 loggers?
由于許多其他日志包都包含全局變量logger,許多應(yīng)用程序沒有設(shè)計成接收logger作為顯式參數(shù)。更改函數(shù)簽名通常是一種破壞性的更改,因此zap包含全局logger以簡化遷移。
盡可能避免使用它們。
為什么包括專用的Panic和Fatal日志級別?
一般來說,應(yīng)用程序代碼應(yīng)優(yōu)雅地處理錯誤,而不是使用panic或os.Exit。但是,每個規(guī)則都有例外,當(dāng)錯誤確實無法恢復(fù)時,崩潰是很常見的。為了避免丟失任何信息(尤其是崩潰的原因),記錄器必須在進程退出之前沖洗任何緩沖條目。
Zap 通過提供在退出前自動沖洗的Panic和Fatal記錄方法來使這一操作變得簡單。當(dāng)然,這并不保證日志永遠不會丟失,但它消除了常見的錯誤。
有關(guān)詳細信息,請參閱 Uber-go/zap#207 中的討論。
什么是DPanic?
DPanic代表"panic in development."。在development中,它會打印Panic級別的日志:反之,它將發(fā)生在Error級別的日志,DPanic更加容易捕獲可能但實際上不應(yīng)該發(fā)生的錯誤,而不是在生產(chǎn)環(huán)境中Panic。
如果你曾經(jīng)寫過這樣的代碼,就可以使用DPanic:
if err != nil {
panic(fmt.Sprintf("shouldn't ever get here: %v", err))
}
安裝問題
錯誤expects import "go.uber.org/zap"是什么意思?
要么zap安裝錯誤,要么您引用了代碼中的錯誤包名。
Zap 的源代碼托管在 GitHub 上,但 import path[3]是 go.uber.org/zap,讓我們項目維護者,可以更方便地自由移動源代碼。所以在安裝和使用包時需要注意這一點。
如果你遵循兩個簡單的規(guī)則,就會正常工作:安裝zapgo get -u go.uber.org/zap并始終導(dǎo)入它在你的代碼import "go.uber.org/zap",代碼不應(yīng)包含任何對github.com/uber-go/zap的引用.
用法問題
Zap是否支持日志切割?
Zap 不支持切割日志文件,因為我們更喜歡將此交給外部程序,如logrotate.
但是,日志切割包很容易集成,如 `gopkg.in/natefinch/lumberjack.v2`[4] 作為zapcore.WriteSyncer.
// lumberjack.Logger is already safe for concurrent use, so we don't need to
// lock it.
w := zapcore.AddSync(&lumberjack.Logger{
Filename: "/var/log/myapp/foo.log",
MaxSize: 500, // megabytes
MaxBackups: 3,
MaxAge: 28, // days
})
core := zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
w,
zap.InfoLevel,
)
logger := zap.New(core)
插件
我們很希望zap 本身能滿足的每一個logging需求,但我們只熟悉少數(shù)日志攝入(log ingestion)系統(tǒng)、參數(shù)解析(flag-parsing)包等。所以我們更愿意發(fā)展 zap 插件生態(tài)系統(tǒng)。
下面擴展包,可以作為參考使用:
| 包 | 集成 |
|---|---|
github.com/tchap/zapext | Sentry, syslog |
github.com/fgrosse/zaptest | Ginkgo |
github.com/blendle/zapdriver | Stackdriver |
github.com/moul/zapgorm | Gorm |
性能比較
說明 : 以下資料來源于 zap 官方,Zap 提供的基準測試清楚地表明,zerolog[5]是與 Zap 競爭最激烈的。zerolo還提供結(jié)果非常相似的基準測試[6]:
記錄一個10個kv字段的消息:
| Package | Time | Time % to zap | Objects Allocated |
|---|---|---|---|
| ? zap | 862 ns/op | +0% | 5 allocs/op |
| ? zap (sugared) | 1250 ns/op | +45% | 11 allocs/op |
| zerolog | 4021 ns/op | +366% | 76 allocs/op |
| go-kit | 4542 ns/op | +427% | 105 allocs/op |
| apex/log | 26785 ns/op | +3007% | 115 allocs/op |
| logrus | 29501 ns/op | +3322% | 125 allocs/op |
| log15 | 29906 ns/op | +3369% | 122 allocs/op |
使用一個已經(jīng)有10個kv字段的logger記錄一條消息:
| Package | Time | Time % to zap | Objects Allocated |
|---|---|---|---|
| ? zap | 126 ns/op | +0% | 0 allocs/op |
| ? zap (sugared) | 187 ns/op | +48% | 2 allocs/op |
| zerolog | 88 ns/op | -30% | 0 allocs/op |
| go-kit | 5087 ns/op | +3937% | 103 allocs/op |
| log15 | 18548 ns/op | +14621% | 73 allocs/op |
| apex/log | 26012 ns/op | +20544% | 104 allocs/op |
| logrus | 27236 ns/op | +21516% | 113 allocs/op |
記錄一個字符串,沒有字段或printf風(fēng)格的模板:
| Package | Time | Time % to zap | Objects Allocated |
|---|---|---|---|
| ? zap | 118 ns/op | +0% | 0 allocs/op |
| ? zap (sugared) | 191 ns/op | +62% | 2 allocs/op |
| zerolog | 93 ns/op | -21% | 0 allocs/op |
| go-kit | 280 ns/op | +137% | 11 allocs/op |
| standard library | 499 ns/op | +323% | 2 allocs/op |
| apex/log | 1990 ns/op | +1586% | 10 allocs/op |
| logrus | 3129 ns/op | +2552% | 24 allocs/op |
| log15 | 3887 ns/op | +3194% | 23 allocs/op |
相似的庫
logrus[7] 功能強大
zerolog[8] 性能相當(dāng)好的日志庫
參考資料
?ZAP: https://github.com/uber-go/zap
[2]Rob Pike 諺語指出: https://go-proverbs.github.io/
[3]import path: https://golang.org/cmd/go/#hdr-Remote_import_paths
[4]gopkg.in/natefinch/lumberjack.v2: https://godoc.org/gopkg.in/natefinch/lumberjack.v2
[5]zerolog: https://github.com/rs/zerolog
[6]基準測試: https://github.com/rs/zerolog#benchmarks
[7]logrus: https://github.com/sirupsen/logrus
[8]zerolog: https://github.com/rs/zerolog
推薦閱讀
