Go 日誌庫 zerolog 大解剖

2021-12-25 Golang技術分享
我是一隻可愛的土撥鼠,專注於分享 Go 職場、招聘和求職,解 Gopher 之憂!歡迎關注我。

歡迎大家加入Go招聘交流群,來這裡找志同道合的小夥伴!跟土撥鼠們一起交流學習。

什麼是 zerolog ?

使用 zerolog

安裝

Contextual Logger

多級 Logger

注意事項

了解源碼

看一下 Logger 結構體

debug 了解輸出日誌流程

從 zerolog 學習避免內存分配

學習日誌級別

學習如何實現 Hook

學習如何得到調用者函數名

從日誌採樣中學習 atomic

Doc

比較

參考資料

文章可能相對較長,請耐心看完。定有收穫。

什麼是 zerolog ?

zerolog 包提供了一個專門用於 JSON 輸出的簡單快速的 Logger。

zerolog 的 API 旨在為開發者提供出色的體驗和令人驚嘆的性能[1]。其獨特的鏈式 API 允許通過避免內存分配和反射來寫入 JSON ( 或 CBOR ) 日誌。

uber 的 zap[2] 庫開創了這種方法,zerolog 通過更簡單的應用編程接口和更好的性能,將這一概念提升到了更高的層次。

使用 zerolog安裝
go get -u github.com/rs/zerolog/log

Contextual Logger
func TestContextualLogger(t *testing.T) {
 log := zerolog.New(os.Stdout)
 log.Info().Str("content", "Hello world").Int("count", 3).Msg("TestContextualLogger")


  // 添加上下文 (文件名/行號/字符串)
 log = log.With().Caller().Str("foo", "bar").Logger()
 log.Info().Msg("Hello wrold")
}

輸出

// {"level":"info","content":"Hello world","count":3,"message":"TestContextualLogger"}
// {"level":"info","caller":"log_example_test.go:29","message":"Hello wrold"}

與 zap 相同的是,都定義了強類型欄位,你可以在這裡[3]找到支持欄位的完整列表。

與 zap 不同的是,zerolog 採用鏈式調用。

多級 Logger

zerolog 提供了從 Trace 到 Panic 七個級別

 // 設置日誌級別
 zerolog.SetGlobalLevel(zerolog.WarnLevel)
 log.Trace().Msg("Trace")
 log.Debug().Msg("Debug")
 log.Info().Msg("Info")
 log.Warn().Msg("Warn")
 log.Error().Msg("Error")
 log.Log().Msg("沒有級別")

輸出

 {"level":"warn","message":"Warn"}
 {"level":"error","message":"Error"}
 {"message":"沒有級別"}

注意事項

1.zerolog 不會對重複的欄位刪除

logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
logger.Info().
       Timestamp().
       Msg("dup")

輸出

{"level":"info","time":1494567715,"time":1494567715,"message":"dup"}

2.鏈式調用必須調用 Msg 或 MsgfSend 才能輸出日誌,Send 相當於調用 Msg("")

3.一旦調用 Msg ,Event 將會被處理 ( 放回池中或丟掉 ),不允許二次調用。

了解源碼

本次 zerolog 的源碼分析基於 zerolog 1.22.0 版本,源碼分析較長,希望大家耐心看完。希望大家能有所收穫。

看一下 Logger 結構體

Logger 的參數 w 類型是 LevelWriter 接口,用於向目標輸出事件。zerolog.New 函數用來創建 Logger,看下方源碼。

// ============ log.go ===

type Logger struct {
 w       LevelWriter // 輸出對象
 level   Level       // 日誌級別
 sampler Sampler     // 採樣器
 context []byte     // 存儲上下文
 hooks   []Hook
 stack   bool
}

func New(w io.Writer) Logger {
 if w == nil {
  // ioutil.Discard 所有成功執行的 Write 操作都不會產生任何實際的效果
  w = ioutil.Discard
 }
 lw, ok := w.(LevelWriter)
 // 傳入的不是 LevelWriter 類型,封裝成此類型
 if !ok {
  lw = levelWriterAdapter{w}
 }
 // 默認輸出日誌級別 TraceLevel
 return Logger{w: lw, level: TraceLevel}
}

debug 了解輸出日誌流程image-20210615150059405

如上圖所示,在第三行打上斷點。

下圖表示該行代碼執行流程。

開始 debug

// ============ log.go ===

// Info 開始記錄一條 info 級別的消息
// 你必須在返回的 *Event 上調用 Msg 才能發送事件
func (l *Logger) Info() *Event {
 return l.newEvent(InfoLevel, nil)
}

func (l *Logger) newEvent(level Level, done func(string)) *Event {
  // 判斷是否應該記錄的級別
 enabled := l.should(level)
 if !enabled {
  return nil
 }
  // 創建記錄日誌的對象
 e := newEvent(l.w, level)
  // 設置 done 函數
 e.done = done
  // 設置 hook 函數
 e.ch = l.hooks
  // 記錄日誌級別
 if level != NoLevel && LevelFieldName != "" {
  e.Str(LevelFieldName, LevelFieldMarshalFunc(level))
 }
  // 記錄上下文
 if l.context != nil && len(l.context) > 1 {
  e.buf = enc.AppendObjectData(e.buf, l.context)
 }
  // 堆棧跟蹤
 if l.stack {
  e.Stack()
 }
 return e
}

should 函數用於判斷是否需要記錄本次消息。

// ============ log.go ===

// should 如果應該被記錄,則返回 true
func (l *Logger) should(lvl Level) bool {
 if lvl < l.level || lvl < GlobalLevel() {
  return false
 }
  // 採樣後面講
 if l.sampler != nil && !samplingDisabled() {
  return l.sampler.Sample(lvl)
 }
 return true
}

newEvent 函數使用 sync.Pool 獲取 Event 對象,並將 Event 參數初始化:日誌級別level和寫入對象LevelWriter

// ============ event.go ===

// 表示一個日誌事件
type Event struct {
 buf       []byte     // 消息
 w         LevelWriter   // 待寫入的目標接口
 level     Level     // 日誌級別
 done      func(msg string) // msg 函數結束事件
 stack     bool       // 錯誤堆棧跟蹤
 ch        []Hook    // hook 函數組
 skipFrame int
}

func newEvent(w LevelWriter, level Level) *Event {
  e := eventPool.Get().(*Event)
 e.buf = e.buf[:0]
 e.ch = nil
  // 在開始添加左大括號 '{'
 e.buf = enc.AppendBeginMarker(e.buf)
 e.w = w
 e.level = level
 e.stack = false
 e.skipFrame = 0
 return e
}

Str 函數是負責將鍵值對添加到 buf,字符串類型添加到 JSON 格式,涉及到特殊字符編碼問題,如果是特殊字符,調用 appendStringComplex 函數解決。

// ============ event.go ===
func (e *Event) Str(key, val string) *Event {
 if e == nil {
  return e
 }
 e.buf = enc.AppendString(enc.AppendKey(e.buf, key), val)
 return e
}

// ============ internal/json/base.go ===
type Encoder struct{}

// 添加一個新 key
func (e Encoder) AppendKey(dst []byte, key string) []byte {
 // 非第一個參數,加個逗號
  if dst[len(dst)-1] != '{' {
  dst = append(dst, ',')
 }
 return append(e.AppendString(dst, key), ':')
}


// === internal/json/string.go ===
func (Encoder) AppendString(dst []byte, s string) []byte {
 // 雙引號起
 dst = append(dst, '"')
 // 遍歷字符
 for i := 0; i < len(s); i++ {
  // 檢查字符是否需要編碼
  if !noEscapeTable[s[i]] {
   dst = appendStringComplex(dst, s, i)
   return append(dst, '"')
  }
 }
 // 不需要編碼的字符串,添加到 dst
 dst = append(dst, s...)
 // 雙引號收
 return append(dst, '"')
}

Int 函數將鍵值(int 類型)對添加到 buf,內部調用 strconv.AppendInt 函數實現。

// ============ event.go ===

func (e *Event) Int(key string, i int) *Event {
 if e == nil {
  return e
 }
 e.buf = enc.AppendInt(enc.AppendKey(e.buf, key), i)
 return e
}

// === internal/json/types.go ===
func (Encoder) AppendInt(dst []byte, val int) []byte {
 // 添加整數
  return strconv.AppendInt(dst, int64(val), 10)
}

Msg 函數

// === event.go ===

// Msg 是對 msg 的封裝調用,當指針接收器為 nil 返回
func (e *Event) Msg(msg string) {
 if e == nil {
  return
 }
 e.msg(msg)
}

// msg
func (e *Event) msg(msg string) {
  // 運行 hook
 for _, hook := range e.ch {
  hook.Run(e, e.level, msg)
 }
  // 記錄消息
 if msg != "" {
  e.buf = enc.AppendString(enc.AppendKey(e.buf, MessageFieldName), msg)
 }
  // 判斷不為 nil,則使用 defer 調用 done 函數
 if e.done != nil {
  defer e.done(msg)
 }
  // 寫入日誌
 if err := e.write(); err != nil {
  if ErrorHandler != nil {
   ErrorHandler(err)
  } else {
   fmt.Fprintf(os.Stderr, "zerolog: could not write event: %v\n", err)
  }
 }
}

// 寫入日誌
func (e *Event) write() (err error) {
 if e == nil {
  return nil
 }
 if e.level != Disabled {
    // 大括號收尾
  e.buf = enc.AppendEndMarker(e.buf)
    // 換行
  e.buf = enc.AppendLineBreak(e.buf)
    // 向目標寫入日誌
  if e.w != nil {
      // 這裡傳遞的日誌級別,函數內並沒有使用
   _, err = e.w.WriteLevel(e.level, e.buf)
  }
 }
  // 將對象放回池中
 putEvent(e)
 return
}

// === writer.go ===
func (lw levelWriterAdapter) WriteLevel(l Level, p []byte) (n int, err error) {
 return lw.Write(p)
}

以上 debug 讓我們對日誌記錄流程有了大概的認識,接下來擴充一下相關知識。

從 zerolog 學習避免內存分配

每一條日誌都會產生一個 *Event對象 ,當多個 Goroutine 操作日誌,導致創建的對象數目劇增,進而導致 GC 壓力增大。形成 "並發大 - 佔用內存大 - GC 緩慢 - 處理並發能力降低 - 並發更大" 這樣的惡性循環。在這個時候,需要有一個對象池,程序不再自己單獨創建對象,而是從對象池中獲取。

使用 sync.Pool 可以將暫時不用的對象緩存起來,下次需要的時候從池中取,不用再次經過內存分配。

下面代碼中 putEvent 函數,當對象中記錄消息的 buf 不超過 64KB 時,放回池中。這裡有個連結,通過這個 issue 23199[4]了解到使用動態增長的 buffer 會導致大量內存被固定,在活鎖的情況下永遠不會釋放。

var eventPool = &sync.Pool{
 New: func() interface{} {
  return &Event{
   buf: make([]byte, 0, 500),
  }
 },
}

func putEvent(e *Event) {
 // 選擇佔用較小內存的 buf,將對象放回池中
 // See https://golang.org/issue/23199
 const maxSize = 1 << 16 // 64KiB
 if cap(e.buf) > maxSize {
  return
 }
 eventPool.Put(e)
}

學習日誌級別

下面代碼中,包含了日誌級別類型的定義,日誌級別對應的字符串值,獲取字符串值的方法以及解析字符串為日誌級別類型的方法。

// ============= log.go ===
// 日誌級別類型
type Level int8

// 定義所有日誌級別
const (
 DebugLevel Level = iota
 InfoLevel
 WarnLevel
 ErrorLevel
 FatalLevel
 PanicLevel
 NoLevel
 Disabled

 TraceLevel Level = -1
)

// 返回當前級別的 value
func (l Level) String() string {
 switch l {
 case TraceLevel:
  return LevelTraceValue
 case DebugLevel:
  return LevelDebugValue
 case InfoLevel:
  return LevelInfoValue
 case WarnLevel:
  return LevelWarnValue
 case ErrorLevel:
  return LevelErrorValue
 case FatalLevel:
  return LevelFatalValue
 case PanicLevel:
  return LevelPanicValue
 case Disabled:
  return "disabled"
 case NoLevel:
  return ""
 }
 return ""
}

// ParseLevel 將級別字符串解析成 zerolog level value
// 當字符串不匹配任何已知級別,返回錯誤
func ParseLevel(levelStr string) (Level, error) {
 switch levelStr {
 case LevelFieldMarshalFunc(TraceLevel):
  return TraceLevel, nil
 case LevelFieldMarshalFunc(DebugLevel):
  return DebugLevel, nil
 case LevelFieldMarshalFunc(InfoLevel):
  return InfoLevel, nil
 case LevelFieldMarshalFunc(WarnLevel):
  return WarnLevel, nil
 case LevelFieldMarshalFunc(ErrorLevel):
  return ErrorLevel, nil
 case LevelFieldMarshalFunc(FatalLevel):
  return FatalLevel, nil
 case LevelFieldMarshalFunc(PanicLevel):
  return PanicLevel, nil
 case LevelFieldMarshalFunc(Disabled):
  return Disabled, nil
 case LevelFieldMarshalFunc(NoLevel):
  return NoLevel, nil
 }
 return NoLevel, fmt.Errorf("Unknown Level String: '%s', defaulting to NoLevel", levelStr)
}


// ============= globals.go ===
var (
 // .
 // 級別欄位的 key 名稱
 LevelFieldName = "level"
 // 各個級別的 value
 LevelTraceValue = "trace"
 LevelDebugValue = "debug"
 LevelInfoValue = "info"
 LevelWarnValue = "warn"
 LevelErrorValue = "error"
 LevelFatalValue = "fatal"
 LevelPanicValue = "panic"
 // 返回形參級別的 value
  LevelFieldMarshalFunc = func(l Level) string {
  return l.String()
 }
  // .
)

全局日誌級別參數

這裡使用 atomic 來保證原子操作,要麼都執行,要麼都不執行,外界不會看到只執行到一半的狀態,原子操作由底層硬體支持,通常比鎖更有效率。

atomic.StoreInt32 用於存儲 int32 類型的值。

atomic.LoadInt32 用於讀取 int32 類型的值。

在源碼中,做級別判斷時,多處調用 GlobalLevel 以保證並發安全。

// ============= globals.go ===

var (
 gLevel          = new(int32)
 // .
)

// SetGlobalLevel 設置全局日誌級別
// 要全局禁用日誌,入參為 Disabled
func SetGlobalLevel(l Level) {
 atomic.StoreInt32(gLevel, int32(l))
}

// 返回當前全局日誌級別
func GlobalLevel() Level {
 return Level(atomic.LoadInt32(gLevel))
}

學習如何實現 Hook

首先定義 Hook 接口,內部有一個 Run 函數,入參包含 *Event,日誌級別**level 和消息 ( **Msg** 函數的參數 )。

然後定義了 LevelHook 結構體,用於為每個級別設置 Hook 。

// ============= hook.go ===

// hook 接口
type Hook interface {
 Run(e *Event, level Level, message string)
}

// HookFunc 函數適配器
type HookFunc func(e *Event, level Level, message string)

// Run 實現 Hook 接口.
func (h HookFunc) Run(e *Event, level Level, message string) {
 h(e, level, message)
}


// 為每個級別應用不同的 hook
type LevelHook struct {
 NoLevelHook, TraceHook, DebugHook, InfoHook, WarnHook, ErrorHook, FatalHook, PanicHook Hook
}

// Run 實現 Hook 接口
func (h LevelHook) Run(e *Event, level Level, message string) {
 switch level {
 case TraceLevel:
  if h.TraceHook != nil {
   h.TraceHook.Run(e, level, message)
  }
 case DebugLevel:
  if h.DebugHook != nil {
   h.DebugHook.Run(e, level, message)
  }
 case InfoLevel:
  if h.InfoHook != nil {
   h.InfoHook.Run(e, level, message)
  }
 case WarnLevel:
  if h.WarnHook != nil {
   h.WarnHook.Run(e, level, message)
  }
 case ErrorLevel:
  if h.ErrorHook != nil {
   h.ErrorHook.Run(e, level, message)
  }
 case FatalLevel:
  if h.FatalHook != nil {
   h.FatalHook.Run(e, level, message)
  }
 case PanicLevel:
  if h.PanicHook != nil {
   h.PanicHook.Run(e, level, message)
  }
 case NoLevel:
  if h.NoLevelHook != nil {
   h.NoLevelHook.Run(e, level, message)
  }
 }
}

// NewLevelHook 創建一個 LevelHook
func NewLevelHook() LevelHook {
 return LevelHook{}
}

在源碼中是如何使用的?

定義 PrintMsgHook 結構體並實現 Hook 接口,作為參數傳遞給 log.Hook 函數,Logger 內部的 hooks 參數用來保存對象。

// 使用案例

type PrintMsgHook struct{}

// 實現 Hook 接口,用來向控制臺輸出 msg
func (p PrintMsgHook) Run(e *zerolog.Event, l zerolog.Level, msg string) {
 fmt.Println(msg)
}

func TestContextualLogger(t *testing.T) {
 log := zerolog.New(os.Stdout)
 log = log.Hook(PrintMsgHook{})
 log.Info().Msg("TestContextualLogger")
}

添加 hook 源碼如下

// ============ log.go ===

// Hook 返回一個帶有 hook 的 Logger
func (l Logger) Hook(h Hook) Logger {
 l.hooks = append(l.hooks, h)
 return l
}

輸出日誌必須調用 msg 函數,hook 將在此函數的開頭執行。

// ============ event.go ===

// msg 函數用來運行 hook
func (e *Event) msg(msg string) {
 for _, hook := range e.ch {
  hook.Run(e, e.level, msg)
 }
 // ..
  // 寫入日誌,此函數上面已經介紹過,此處省略
  // ..
}

學習如何得到調用者函數名

在看 zerolog 源碼之前,需要知道一些關於 runtime.Caller 函數的前置知識,

runtime.Caller 可以獲取相關調用 goroutine 堆棧上的函數調用的文件和行號信息。

參數skip 是堆棧幀的數量,當 skip=0 時,輸出當前函數信息; 當 skip=1 時,輸出調用棧上一幀,即調用函數者的信息。

返回值為 程序計數器,文件位置,行號,是否能恢復信息

// ============ go@1.16.5 runtime/extern.go ===

func Caller(skip int) (pc uintptr, file string, line int, ok bool) {
 rpc := make([]uintptr, 1)
 n := callers(skip+1, rpc[:])
 if n < 1 {
  return
 }
 frame, _ := CallersFrames(rpc).Next()
 return frame.PC, frame.File, frame.Line, frame.PC != 0
}

再看 zerolog 源碼,定義 callerHook 結構體並實現了 Hook 接口,實現函數中調用了參數 *Event 提供的 caller函數。

其中入參為預定義參數 CallerSkipFrameCount 和 contextCallerSkipFrameCount ,值都為 2。

zerolog caller 調用示意圖
// ============ context.go ===
type callerHook struct {
 callerSkipFrameCount int
}

func newCallerHook(skipFrameCount int) callerHook {
 return callerHook{callerSkipFrameCount: skipFrameCount}
}

func (ch callerHook) Run(e *Event, level Level, msg string) {
 switch ch.callerSkipFrameCount {
  // useGlobalSkipFrameCount 是 int32 類型最小值
 case useGlobalSkipFrameCount:
    // CallerSkipFrameCount 預定義全局變量,值為 2
    // contextCallerSkipFrameCount 預定義變量,值為 2
  e.caller(CallerSkipFrameCount + contextCallerSkipFrameCount)
 default:
  e.caller(ch.callerSkipFrameCount + contextCallerSkipFrameCount)
 }
}

// useGlobalSkipFrameCount 值:-2147483648
const useGlobalSkipFrameCount = math.MinInt32

// 創建默認 callerHook
var ch = newCallerHook(useGlobalSkipFrameCount)

// Caller 為 Logger 添加 hook ,該 hook 用於記錄函數調用者的 file:line
func (c Context) Caller() Context {
 c.l = c.l.Hook(ch)
 return c
}

// ============ event.go ===

func (e *Event) caller(skip int) *Event {
 if e == nil {
  return e
 }
 _, file, line, ok := runtime.Caller(skip + e.skipFrame)
 if !ok {
  return e
 }
 // CallerFieldName是默認的 key 名
  // CallerMarshalFunc 函數用於拼接  file:line
 e.buf = enc.AppendString(enc.AppendKey(e.buf, CallerFieldName), CallerMarshalFunc(file, line))
 return e
}

從日誌採樣中學習 atomic

這個使用案例中,TestSample 每秒允許 記錄 5 條消息,超過則每 20 條僅記錄一條

func TestSample(t *testing.T) {
 sampled := log.Sample(&zerolog.BurstSampler{
  Burst:       5,
  Period:      1 * time.Second,
  NextSampler: &zerolog.BasicSampler{N: 20},
 })
 for i := 0; i <= 50; i++ {
  sampled.Info().Msgf("logged messages : %2d ", i)
 }
}

輸出結果本來應該輸出 50 條日誌,使用了採樣,在一秒內輸出最大 5 條日誌,當大於 5 條後,每 20 條日誌輸出一次。

image-20210618114636900

採樣的流程示意圖如下

zerolog 採樣函數說明圖

下方是定義採樣接口及實現函數的源碼。

在 inc 函數中,使用 atomic 包將競爭的接收器對象的參數變成局部變量,是學習 atomic 很好的實例。函數說明都寫在注釋裡。

// =========== sampler.go ===

// 採樣器接口
type Sampler interface {
 // 如果事件是樣本的一部分返回 true
 Sample(lvl Level) bool
}

// BasicSampler 基本採樣器
// 每 N 個事件發送一次,不考慮日誌級別
type BasicSampler struct {
 N
 counter uint32
}
// 實現採樣器接口
func (s *BasicSampler) Sample(lvl Level) bool {
 n := s.N
 if n == 1 {
  return true
 }
 c := atomic.AddUint32(&s.counter, 1)
 return c%n == 1
}


type BurstSampler struct {
  // 調用 NextSampler 之前每個時間段(Period)調用的最大事件數量
 Burst uint32
 // 如果為 0,則始終調用 NextSampler
 Period time.Duration
  // 採樣器
 NextSampler Sampler
  // 用於計數在一定時間內(Period)的調用數量
 counter uint32
  // 時間段的結束時間(納秒),即 當前時間+Period
 resetAt int64
}

// 實現 Sampler 接口
func (s *BurstSampler) Sample(lvl Level) bool {
  // 當設置了 Burst 和 Period,大於零時限制 一定時間內的最大事件數量
 if s.Burst > 0 && s.Period > 0 {
  if s.inc() <= s.Burst {
   return true
  }
 }
  // 沒有採樣器,結束
 if s.NextSampler == nil {
  return false
 }
  // 調用採樣器
 return s.NextSampler.Sample(lvl)
}

func (s *BurstSampler) inc() uint32 {
  // 當前時間 (納秒)
 now := time.Now().UnixNano()
  // 重置時間 (納秒)
 resetAt := atomic.LoadInt64(&s.resetAt)
 var c uint32
  // 當前時間 > 重置時間
 if now > resetAt {
  c = 1
    // 重置 s.counter 為 1
  atomic.StoreUint32(&s.counter, c)
    // 計算下一次的重置時間
  newResetAt := now + s.Period.Nanoseconds()
    // 比較函數開頭獲取的重置時間與存儲的時間是否相等
    // 相等時,將下一次的重置時間存儲到 s.resetAt,並返回 true
  reset := atomic.CompareAndSwapInt64(&s.resetAt, resetAt, newResetAt)
  if !reset {
      // 在上面比較賦值那一步沒有搶到的 goroutine 計數器+1
   c = atomic.AddUint32(&s.counter, 1)
  }
 } else {
  c = atomic.AddUint32(&s.counter, 1)
 }
 return c
}

在代碼中如何調用的呢?

Info 函數及其他級別函數都會調用 newEvent,在該函數的開頭, should 函數用來判斷是否需要記錄的日誌級別和採樣。

// ============ log.go ===
// should 如果應該被記錄,則返回 true
func (l *Logger) should(lvl Level) bool {
  if lvl < l.level || lvl < GlobalLevel() {
    return false
  }
  // 如果使用了採樣,則調用採樣函數,判斷本次事件是否記錄
  if l.sampler != nil && !samplingDisabled() {
    return l.sampler.Sample(lvl)
  }
  return true
}

Doc

關於更多 zerolog 的使用可以參考 https://pkg.go.dev/github.com/rs/zerolog

比較

說明 : 以下資料來源於 zerolog 官方。從性能分析上 zerolog 比 zap 和其他 logger 庫更勝一籌,關於 zerolog 和 zap 的使用,gopher 可根據實際業務場景具體考量。

記錄 10 個 KV 欄位的消息 :

LibraryTimeBytes AllocatedObjects Allocatedzerolog767 ns/op552 B/op6 allocs/op⚡ zap848 ns/op704 B/op2 allocs/op⚡ zap (sugared)1363 ns/op1610 B/op20 allocs/opgo-kit3614 ns/op2895 B/op66 allocs/oplion5392 ns/op5807 B/op63 allocs/oplogrus5661 ns/op6092 B/op78 allocs/opapex/log15332 ns/op3832 B/op65 allocs/oplog1520657 ns/op5632 B/op93 allocs/op

使用一個已經有 10 個 KV 欄位的 logger 記錄一條消息 :

LibraryTimeBytes AllocatedObjects Allocatedzerolog52 ns/op0 B/op0 allocs/op⚡ zap283 ns/op0 B/op0 allocs/op⚡ zap (sugared)337 ns/op80 B/op2 allocs/oplion2702 ns/op4074 B/op38 allocs/opgo-kit3378 ns/op3046 B/op52 allocs/oplogrus4309 ns/op4564 B/op63 allocs/opapex/log13456 ns/op2898 B/op51 allocs/oplog1514179 ns/op2642 B/op44 allocs/op

記錄一個字符串,沒有欄位或 printf 風格的模板 :

LibraryTimeBytes AllocatedObjects Allocatedzerolog50 ns/op0 B/op0 allocs/op⚡ zap236 ns/op0 B/op0 allocs/opstandard library453 ns/op80 B/op2 allocs/op⚡ zap (sugared)337 ns/op80 B/op2 allocs/opgo-kit508 ns/op656 B/op13 allocs/oplion771 ns/op1224 B/op10 allocs/oplogrus1244 ns/op1505 B/op27 allocs/opapex/log2751 ns/op584 B/op11 allocs/oplog155181 ns/op1592 B/op26 allocs/op相似的庫

logrus[5] 功能強大

zap[6] 非常快速,結構化,分級

參考資料

zerolog 官方文檔[7]

參考資料[1] 

性能: https://github.com/rs/zerolog#benchmarks

[2] 

zap: https://godoc.org/go.uber.org/zap

[3] 

這裡: https://pkg.go.dev/github.com/rs/zerolog#readme-standard-types

[4] 

issue 23199: https://golang.org/issue/23199

[5] 

logrus: https://github.com/sirupsen/logrus

[6] 

zap: https://github.com/uber-go/zap

[7] 

zerolog 官方文檔: https://pkg.go.dev/github.com/rs/zerolog

歡迎關注Go招聘公眾號,獲取更多精彩內容。

相關焦點

  • Kratos 漫遊指南 - 日誌
    Kratos 的日誌庫主要有如下特性:Logger 用於對接各種日誌庫或日誌平臺,可以用現成的或者自己實現Helper 是在您的項目代碼中實際需要調用的,用於在業務代碼裡打日誌Filter 用於對輸出日誌進行過濾或魔改(通常用於日誌脫敏)Valuer 用於綁定一些全局的固定值或動態值(比如時間戳、traceID 或者實例 id 之類的東西
  • GO語言自定義錯誤日誌追蹤功能實現
    go語言跟其他的程式語言不一樣,沒有異常只有錯誤,這點跟C語言倒是很像。基本上所有的錯誤都是實現了error的接口,但是我們在判斷錯誤的時候,由於有很多的模塊互相調用,因為就算函數返回的錯誤,也不知道具體是哪個函數在哪個文件,哪一行引發的。
  • go-carbon 1.2.3 版本發布了,新增毫秒、微妙、納秒級時間戳輸出
    carbon 是一個輕量級、語義化、對開發者友好的Golang時間處理庫,支持鏈式調用、農曆和gorm、xorm等主流orm
  • 整潔架構(Clean Architecture)的Go微服務: 程序結構
    1.在數據持久層上支持不同的資料庫(Sql 和 NoSql 資料庫)2.使用不同的協議(如 gRPC 或 REST)支持來自其他微服務的數據3.鬆散耦合和高度內聚4.支持簡單一致的日誌記錄,並能夠更改它(例如,日誌記錄級別和日誌記錄庫),而無需修改程序中的日誌記錄語句。
  • 構建微服務的十大 Golang 框架和庫
    現在已經有很多開源庫 golang 支持構建應用程式,這些庫設計簡單,代碼乾淨,性能良好,本文為大家精心挑選了十個實用的框架和庫。
  • Go 指南:頂級 Go 框架、IDE 和工具列表
    它還包括各種全面和高性能的功能,因此你不需要找外部庫集成到框架中。2、BeegoBeego 是一個完整的 MVC 框架,有自己的日誌庫、ORM 和 Web 框架。你不需要再去安裝第三方庫。Go-plus 還包括各種代碼片段和功能,如 gocode 的自動完成,gofmt、goreturns 或 goimports 等的代碼格式化。
  • iOS 崩潰日誌在線符號化實踐
    至此可以得出結論:符號化系統庫是很有必要的,特別是對一些 App 堆棧信息完全沒有的崩潰日誌。如何符號化系統庫符號符號化自己 App 的方法名,需要編譯生成的 dSYM 文件。而要將系統庫的符號化為完整的方法名,也需要 iOS 各系統庫的符號文件。用戶的崩潰日誌來自各種系統版本,需要對應版本的系統符號文件才能符號化。
  • 使用pprof和go-torch排查golang的性能問題
    使用 pprof  也證明了這一塊確實是熱點:$ go tool pprof http:...猜測問題有可能在日誌,代碼裡確實用得不少。日誌用的是 github.com/ngaut/logging 庫,每一次調用都會用到兩個全局mutex。但通過調整log level 為error級別,大幅減少了日誌量,並沒有看到性能的改善。經過搜索,發現 uber 基於 pprof 開發了一個神器 go-torch,可以生成火焰圖。
  • 【雲原生の微服務】29)錯誤排查的首要目標:系統日誌技術棧匯總
    日誌記錄是開發中的重要組成部分,這離不開日誌庫的支持。 日誌庫 在 Java 平臺上,直到 JDK 1.4 版本才在標準庫中增加了日誌記錄的 API,也就是 java.util.logging 包(JUL)。
  • 好書推薦|Go語言實戰(附PDF下載)
    1.1.1 開發速度 21.1.2 並發 31.1.3Go語言的類型系統 51.1.4 內存管理 71.2 你好,Go71.3 小結 8第2章 快速開始一個Go程序 92.1 程序架構 92.2main包 112.3search包 132.3.1search.go13
  • 【Go語言繪圖】gg 庫的基本使用
    最近接了個比較大的需求,需要做很多圖片處理的事情,比如圖片的旋轉裁截拼接,各種漸變處理,文字排列,一開始光是想想就頭疼。但沒有辦法,既然已經需求已經到手上了,那就得把它做好才行,於是便開始被迫營業,無證上崗了。
  • logpipe日誌採集工具
    logpipe是一個分布式、高可用的用於採集、傳輸、對接落地的日誌工具,採用了插件風格的框架結構設計,支持多輸入多輸出按需配置組件用於流式日誌收集架構,無第三方依賴。logpipe自帶了4個插件(今後將開發更多插件),分別是: logpipe-input-file 用inotify異步實時監控日誌目錄,一旦有文件新建或文件增長事件發生(注意:不是周期性輪詢文件修改時間和大小),立即捕獲文件名和讀取文件追加數據。該插件擁有文件大小轉檔功能,用以替代應用日誌庫對應功能,提高應用日誌庫寫日誌性能。該插件支持數據壓縮。
  • Golang 語言的標準庫 log 包怎麼使用?
    01 介紹Golang 語言的標準庫中提供了一個簡單的 log 日誌包,它不僅提供了很多函數,還定義了一個包含很多方法的類型但是它也有缺點,比如不支持區分日誌級別,不支持日誌文件切割等。使用 formatHeader() 函數來格式化日誌的信息,然後保存到 buf 中,然後再把日誌信息追加到 buf 的末尾,然後再通過判斷,查看日誌是否為空或末尾不是 \n,如果是就再把 \n 追加到 buf 的末尾,最後將日誌信息輸出。
  • JAVA程式設計師一定知道的優秀第三方庫(2016版)
    可能是國內用得最廣泛的持久層框架了,它非常強大,但用好它並不容易,你需要了解它的內部機制,否則可能會出現一些無法預見的性能問題,特別是在數據量特別大的時候。(GitHub上的代碼庫)>hibernate-core</artifactId>    <version>5.1.0.Final</version></dependency>日誌JAVA中也包含了日誌記錄功能,但它在處理日誌分級,日誌的存儲,以及日誌的備份、歸檔方面都不夠出色,因此在項目中我們一般都會使用第三方日誌庫來處理日誌。
  • GoVCL 2.0.3 正式發布,跨平臺 Go 語言 GUI 庫
    GoVCL是一款簡單+小巧+原生的go語言GUI庫,依靠著Lazarus LCL使得編寫一個跨平臺的GUI軟體不再是一件麻煩的事。
  • MySQL 中主庫跑太快,從庫追不上怎麼整?
    對於主從來說,通常的操作是主庫用來寫入數據,從庫用來讀取數據。這樣的好處是通過將讀寫壓力分散開,避免了所有的請求都打在主庫上。同時通過從庫進行水平擴展使系統的伸縮性及負載能力也得到了很大的提升。但是問題就來了,讀從庫時的數據要與主庫保持一致,那就需要主庫的數據在寫入後同步到從庫中。如何保持主庫與從庫的數據一致性,主庫又是通過什麼樣的方式將數據實時同步到從庫的?
  • PHP 錯誤與異常的日誌記錄
    提到 Nginx + PHP 服務的錯誤日誌,我們通常能想到的有 Nginx 的 access 日誌、error 日誌以及 PHP 的 error 日誌。日誌的記錄PHP 本身可配置的 log 大概有以下幾個:php-fpm error log(php-fpm.conf 中配置,記錄 php-fpm 進程的啟動和終止等信息)php-fpm slow log(也是在 php-fpm.conf 中配置,記錄慢執行)php error log(php.ini 中配置,記錄應用程式的錯誤日誌)
  • go 性能優化之 benchmark + pprof
    大師兄寫的AES加密函數func AesEncryptA(aesKey, IV, origin []byte) []byte { block, err := aes.NewCipher(aesKey) if err !
  • 日誌易:日誌大數據助力中國平安旗下iTutorGroup提升在線課堂體驗
    隨著開學季到來,學校、教育培訓機構無法正常開課,各大在線教育企業抓住機遇,紛紛免費開放課程資源、共享技術平臺,隨之而來的是學生訪問量激增,在線課堂系統面臨著巨大的網絡流量衝擊,知名教育機構、中國平安旗下麥奇教育科技(iTutorGroup)藉助日誌易的大數據分析及大屏展示技術,實時監控網絡健康狀態,及時發現並定位故障,確保學生時刻享受到流暢的在線課堂體驗,贏得了口碑。
  • 1000+ Python第三方庫大合集
    rich:一個在終端中支持富文本和格式美化的 Python 庫, 同時提供了RichHandler日誌處理程序。tqdm:一個可在循環和命令行中使用的快速、可擴展的進度條。aws-cli:Amazon Web Services 的通用命令行界面。caniusepython3:判斷是哪個項目妨礙你你移植到 Python3。