一文帶你由淺入深地解讀 Zap 的高性能
簡介
zap 是什麼?
⚡ZAP[1] 是 uber 開源的提供快速,結構化,高性能的日誌記錄包。
zap 高性能體現在哪裏?
在介紹 zap 包的優化部分之前,讓我們看下 zap 日誌庫的工作流程圖
大多數日誌庫提供的方式是基於反射的序列化和字符串格式化,這種方式代價高昂,而 Zap 採取不同的方法。
-
避免 interface{} 使用強類型設計
-
封裝強類型,無反射
-
使用零分配內存的 JSON 編碼器,儘可能避免序列化開銷,它比其他結構化日誌包快 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 以避免記錄消息時的內存分配
詳情在下文 zapcore 模塊介紹。
Example
安裝
go get -u go.uber.org/zap
Zap 提供了兩種類型的 logger
-
SugaredLogger
-
Logger
在性能良好但不是關鍵的情況下,使用 SugaredLogger,它比其他結構化的日誌包快 4-10 倍,並且支持結構化和 printf
風格的 APIs。
例一 調用 NewProduction 創建 logger 對象
func TestSugar(t *testing.T) {
logger, _ := zap.NewProduction()
// 默認 logger 不緩衝。
// 但由於底層 api 允許緩衝,所以在進程退出之前調用 Sync 是一個好習慣。
defer logger.Sync()
sugar := logger.Sugar()
sugar.Infof("Failed to fetch URL: %s", "https://baidu.com")
}
對性能和類型安全要求嚴格的情況下,可以使用 Logger ,它甚至比前者 SugaredLogger 更快,內存分配次數也更少,但它僅支持強類型的結構化日誌記錄。
例二 調用 NewDevelopment 創建 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),
)
}
不需要爲整個應用程序決定選擇使用 Logger
還是 SugaredLogger
,兩者之間都可以輕鬆轉換。
例三 Logger
與 SugaredLogger
相互轉換
// 創建 logger
logger := zap.NewExample()
defer logger.Sync()
// 轉換 SugaredLogger
sugar := logger.Sugar()
// 轉換 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")
}
結果見下圖
image-20210525201656456
問題 1:
沒有打印。AtomicLevel 是原子性可更改的動態日誌級別,通過調用 atom.SetLevel
更改日誌級別爲 infoLevel
。
問題 2:
沒有打印。zap.Development()
啓用了開發模式,在開發模式下 DPanic
函數會引發 panic,所以最後的 error
不會打印到控制檯。
源碼分析
此次源碼分析基於 Zap 1.16
zap 概覽
上圖僅表示 zap 可調用兩種 logger,沒有表達 Logger
與 SugaredLogger
的關係,繼續往下看,你會更理解。
Logger
logger 提供快速,分級,結構化的日誌記錄。所有的方法都是安全的,內存分配很重要,因此它的 API 有意偏向於性能和類型安全。
zap@v1.16.0 - logger.go
type Logger struct {
// 實現編碼和輸出的接口
core zapcore.Core
// 記錄器開發模式,DPanic 等級將記錄 panic
development bool
// 開啓記錄調用者的行號和函數名
addCaller bool
// 致命日誌採取的操作,默認寫入日誌後 os.Exit()
onFatal zapcore.CheckWriteAction
name string
// 設置記錄器生成的錯誤目的地
errorOutput zapcore.WriteSyncer
// 記錄 >= 該日誌等級的堆棧追蹤
addStack zapcore.LevelEnabler
// 避免記錄器認爲封裝函數爲調用方
callerSkip int
// 默認爲系統時間
clock Clock
}
在 Example 中分別使用了 NewProduction
和 NewDevelopment
,接下來以這兩個函數開始分析。下圖表示 A 函數調用了 B 函數,其中箭頭表示函數調用關係。圖中函數都會分析到。
NewProduction
從下面代碼中可以看出,此函數是對 NewProductionConfig().Build(...)
封裝的快捷方式。
zap@v1.16.0 - logger.go
func NewProduction(options ...Option) (*Logger, error) {
return NewProductionConfig().Build(options...)
}
NewProductionConfig
在 InfoLevel 及更高級別上啓用了日誌記錄。它使用 JSON 編碼器,寫入 stderr,啓用採樣。
zap@v1.16.0 - config.go
func NewProductionConfig() Config {
return Config{
// info 日誌級別
Level: NewAtomicLevelAt(InfoLevel),
// 非開發模式
Development: false,
// 採樣設置
Sampling: &SamplingConfig{
Initial: 100, // 相同日誌級別下相同內容每秒日誌輸出數量
Thereafter: 100, // 超過該數量,纔會再次輸出
},
// JSON 編碼器
Encoding: "json",
// 後面介紹
EncoderConfig: NewProductionEncoderConfig(),
// 輸出到 stderr
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
}
Config 結構體
通過 Config 可以設置通用的配置項。
zap@v1.16.0 - config.go
type Config struct {
// 日誌級別
Level AtomicLevel `json:"level" yaml:"level"`
// 開發模式
Development bool `json:"development" yaml:"development"`
// 停止使用調用方的函數和行號
DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
// 完全停止使用堆棧跟蹤,默認爲 `>=WarnLevel` 使用堆棧跟蹤
DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
// 採樣設置策略
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 內部錯誤的輸出路徑
ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
// 添加到根記錄器的字段的集合
InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}
NewDevelopment
從下面代碼中可以看出,此函數是對 NewDevelopmentConfig().Build(...)
封裝的快捷方式
zap@v1.16.0 - logger.go
func NewDevelopment(options ...Option) (*Logger, error) {
return NewDevelopmentConfig().Build(options...)
}
NewDevelopmentConfig
此函數在 DebugLevel 及更高版本上啓用日誌記錄,它使用 console 編碼器,寫入 stderr,禁用採樣。
zap@v1.16.0 - config.go
func NewDevelopmentConfig() Config {
return Config{
// debug 等級
Level: NewAtomicLevelAt(DebugLevel),
// 開發模式
Development: true,
// console 編碼器
Encoding: "console",
EncoderConfig: NewDevelopmentEncoderConfig(),
// 輸出到 stderr
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
}
NewProductionEncoderConfig
和 NewDevelopmentEncoderConfig
都是返回編碼器配置。
zap@v1.16.0 - config.go
type EncoderConfig struct {
// 設置 編碼爲 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"`
// 配置常見覆雜類型的基本表示形式。
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"`
// 配置 console 編碼器使用的字段分隔符,默認 tab
ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}
NewProductionEncoderConfig
zap@v1.16.0 - 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,
// 時間序列化成浮點秒數
EncodeTime: zapcore.EpochTimeEncoder,
// 時間序列化,Duration爲經過的浮點秒數
EncodeDuration: zapcore.SecondsDurationEncoder,
// 以 包名/文件名:行數 格式序列化
EncodeCaller: zapcore.ShortCallerEncoder,
}
}
該配置會輸出如下結果,此結果出處參見 Example 中的例一
{"level":"info","ts":1620367988.461055,"caller":"test/use_test.go:24","msg":"Failed to fetch URL: https://baidu.com"}
NewDevelopmentEncoderConfig
zap@v1.16.0 - 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,
// // 以 包名/文件名:行數 格式序列化
EncodeCaller: zapcore.ShortCallerEncoder,
}
}
該配置會輸出如下結果,此結果出處參見 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 調用 Build
函數返回 logger,接下來我們看看這個函數。
zap@v1.16.0 - 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 的參數,調用 New
方法來創建 Logger。在 Example 中例四,就是調用 New
方法來自定義 Logger。
SugaredLogger
Logger
作爲 SugaredLogger
的屬性,這個封裝優點在於不是很在乎性能的情況下,可以快速調用Logger
。所以名字爲加了糖的 Logger。
zap@v1.16.0 - logger.go
type SugaredLogger struct {
base *Logger
}
zap.ReplaceGlobals(logger) // 重新配置全局變量
zap.S().Info("SugaredLogger") // S 返回全局 SugaredLogger
zap.L().Info("logger") // L 返回全局 logger
與Logger
不同,SugaredLogger
不強制日誌結構化。所以對於每個日誌級別,都提供了三種方法。
level
zap@v1.16.0 - sugar.go
以 info 級別爲例,相關的三種方法。
// Info 使用 fmt.Sprint 構造和記錄消息。
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("...")
打上斷點,從這開始追蹤源碼。
image-20210519111252185
在調試代碼之前,先給大家看一下SugaredLogger
的 Infof
函數的調用的大致工作流,其中不涉及採樣等。
infof 工作流程
Info
, Infof
, Infow
三個函數都調用了 log
函數,log
函數代碼如下
zap@v1.16.0 - sugar.go
func (s *SugaredLogger) log(lvl zapcore.Level, template string, fmtArgs []interface{}, context []interface{}) {
// 判斷是否啓用的日誌級別
if lvl < DPanicLevel && !s.base.Core().Enabled(lvl) {
return
}
// 將參數合併到語句中
msg := getMessage(template, fmtArgs)
// Check 可以幫助避免分配一個分片來保存字段。
if ce := s.base.Check(lvl, msg); ce != nil {
ce.Write(s.sweetenFields(context)...)
}
}
函數的第一個參數 InfoLevel
是日誌級別,其源碼如下
zap@v1.16.0 - zapcore/level.go
const (
// Debug 應是大量的,且通常在生產狀態禁用.
DebugLevel = zapcore.DebugLevel
// Info 是默認的記錄優先級.
InfoLevel = zapcore.InfoLevel
// Warn 比 info 更重要.
WarnLevel = zapcore.WarnLevel
// Error 是高優先級的,如果程序順利不應該產生任何 err 級別日誌.
ErrorLevel = zapcore.ErrorLevel
// DPanic 特別重大的錯誤,在開發模式下引起 panic.
DPanicLevel = zapcore.DPanicLevel
// Panic 記錄消息後調用 panic.
PanicLevel = zapcore.PanicLevel
// Fatal 記錄消息後調用 os.Exit(1).
FatalLevel = zapcore.FatalLevel
)
getMessage
函數處理 template
和 fmtArgs
參數,主要爲不同的參數選擇最合適的方式拼接消息
zap@v1.16.0 - sugar.go
func getMessage(template string, fmtArgs []interface{}) string {
// 沒有參數直接返回 template
if len(fmtArgs) == 0 {
return template
}
// 此處調用 Sprintf 會使用反射
if template != "" {
return fmt.Sprintf(template, fmtArgs...)
}
// 消息爲空並且有一個參數,返回該參數
if len(fmtArgs) == 1 {
if str, ok := fmtArgs[0].(string); ok {
return str
}
}
// 返回所有 fmtArgs
return fmt.Sprint(fmtArgs...)
}
關於 s.base.Check
,這就需要介紹zapcore
,下面分析相關模塊。
zapcore
zapcore
包 定義並實現了構建 zap 的低級接口。通過提供這些接口的替代實現,外部包可以擴展 zap 的功能。
zap@v1.16.0 - zapcore/core.go
// Core 是一個最小的、快速的記錄器接口。
type Core interface {
// 接口,決定一個日誌等級是否啓用
LevelEnabler
// 向 core 添加核心上下文
With([]Field) Core
// 檢查是否應記錄提供的條目
// 在調用 write 之前必須先調用 Check
Check(Entry, *CheckedEntry) *CheckedEntry
// 寫入日誌
Write(Entry, []Field) error
// 同步刷新緩存日誌(如果有)
Sync() error
}
Check
函數有兩個入參。第一個參數表示一條完整的日誌消息,第二個參數爲 nil 時會從 sync.Pool
創建的池中取出*CheckedEntry
對象複用,避免重新分配內存。該函數內部調用 AddCore
實現獲取 *CheckedEntry
對象,最後調用 Write
寫入日誌消息。
相關代碼全部貼在下面,更多介紹請看代碼中的註釋。
zap@v1.16.0 - zapcore/entry.go
// 一個 entry 表示一個完整的日誌消息
type Entry struct {
Level Level
Time time.Time
LoggerName string
Message string
Caller EntryCaller
Stack string
}
// 使用 sync.Pool 複用臨時對象
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 // 用於檢測是否重複使用對象
should CheckWriteAction // 結束程序的動作
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 的不安全重複使用
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))
}
// 處理內部發生的錯誤
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 添加到池中,下次複用
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,減少內存分配
ce = getCheckedEntry()
ce.Entry = ent
}
ce.cores = append(ce.cores, core)
return ce
}
Doc
https://pkg.go.dev/go.uber.org/zap
QA
設計問題
爲什麼要在 Logger 性能上花費這麼多精力呢?
當然,大多數應用程序不會注意到 Logger 慢的影響:因爲它們每次操作會需要幾十或幾百毫秒,所以額外的幾毫秒很無關緊要。
另一方面,爲什麼不使用結構化日誌快速開發呢?與其他日誌包相比SugaredLogger
的使用並不難,Logger
使結構化記錄在對性能要求嚴格的環境中成爲可能。在 Go 微服務的架構體系中,使每個應用程序甚至稍微更有效地加速執行。
爲什麼沒有Logger
和SugaredLogger
接口?
不像熟悉的io.Writer
和http.Handler
、Logger
和SugaredLogger
接口將包括很多方法。正如 Rob Pike 諺語指出 [2] 的,"The bigger the interface, the weaker the abstraction"(接口越大,抽象越弱)。接口也是嚴格的,任何更改都需要發佈一個新的主版本,因爲它打破了所有第三方實現。
Logger
和SugaredLogger
成爲具體類型並不會犧牲太多抽象,而且它允許我們在不引入破壞性更改的情況下添加方法。您的應用程序應該定義並依賴只包含您使用的方法的接口。
爲什麼我的一些日誌會丟失?
在啓用抽樣時,通過 zap 有意地刪除日誌。生產配置 (如 NewProductionConfig() 返回的那樣)支持抽樣,這將導致在一秒鐘內對重複日誌進行抽樣。有關爲什麼啓用抽樣的更多詳細信息,請參見 "爲什麼使用示例應用日誌" 中啓用採樣.
爲什麼要使用示例應用程序日誌?
應用程序經常會遇到錯誤,無論是因爲錯誤還是因爲用戶使用錯誤。記錄錯誤日誌通常是一個好主意,但它很容易使這種糟糕的情況變得更糟:不僅您的應用程序應對大量錯誤,它還花費額外的 CPU 週期和 I/O 記錄這些錯誤日誌。由於寫入通常是序列化的,因此在最需要時,logger
會限制吞吐量。
採樣通過刪除重複的日誌條目來解決這個問題。在正常情況下,您的應用程序會輸出每個記錄。但是,當類似的記錄每秒輸出數百或數千次時,zap 開始丟棄重複以保存吞吐量。
爲什麼結構化的日誌 API 除了接受字段之外還可以接收消息?
主觀上,我們發現在結構化上下文中附帶一個簡短的描述是有幫助的。這在開發過程中並不關鍵,但它使調試和操作不熟悉的系統更加容易。
更具體地說,zap 的採樣算法使用消息來識別重複的條目。根據我們的經驗,這是一個介於隨機抽樣(通常在調試時刪除您需要的確切條目)和哈希完整條目(代價高)之間的一箇中間方法。
爲什麼要包括全局 loggers?
由於許多其他日誌包都包含全局變量 logger,許多應用程序沒有設計成接收 logger 作爲顯式參數。更改函數簽名通常是一種破壞性的更改,因此 zap 包含全局 logger 以簡化遷移。
儘可能避免使用它們。
爲什麼包括專用的 Panic 和 Fatal 日誌級別?
一般來說,應用程序代碼應優雅地處理錯誤,而不是使用panic
或os.Exit
。但是,每個規則都有例外,當錯誤確實無法恢復時,崩潰是很常見的。爲了避免丟失任何信息(尤其是崩潰的原因),記錄器必須在進程退出之前沖洗任何緩衝條目。
Zap 通過提供在退出前自動沖洗的Panic
和Fatal
記錄方法來使這一操作變得簡單。當然,這並不保證日誌永遠不會丟失,但它消除了常見的錯誤。
有關詳細信息,請參閱 Uber-go/zap#207 中的討論。
什麼是DPanic
?
DPanic
代表 "panic in development."。在 development 中,它會打印 Panic 級別的日誌:反之,它將發生在 Error 級別的日誌,DPanic
更加容易捕獲可能但實際上不應該發生的錯誤,而不是在生產環境中 Panic。
如果你曾經寫過這樣的代碼,就可以使用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
,讓我們項目維護者,可以更方便地自由移動源代碼。所以在安裝和使用包時需要注意這一點。
如果你遵循兩個簡單的規則,就會正常工作:安裝 zapgo get -u go.uber.org/zap
並始終導入它在你的代碼import "go.uber.org/zap"
,代碼不應包含任何對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 需求,但我們只熟悉少數日誌攝入 (log ingestion) 系統、參數解析 (flag-parsing) 包等。所以我們更願意發展 zap 插件生態系統。
下面擴展包,可以作爲參考使用:
性能比較
說明 : 以下資料來源於 zap 官方,Zap 提供的基準測試清楚地表明,zerolog[5] 是與 Zap 競爭最激烈的。zerolo 還提供結果非常相似的基準測試 [6]:
記錄一個 10 個 kv 字段的消息:
使用一個已經有 10 個 kv 字段的 logger 記錄一條消息:
記錄一個字符串,沒有字段或printf
風格的模板:
相似的庫
logrus[7] 功能強大
zerolog[8] 性能相當好的日誌庫
參考資料
[1] ⚡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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Qc5tGELKPZOA9LEBOEkuow