<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          全面分析Uber的高性能日志庫Zap

          共 27319字,需瀏覽 55分鐘

           ·

          2021-07-04 13:06

          簡介

          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()

          例四 自定義格式

          自定義一個日志消息格式,帶著問題看下列代碼。

          1. debug 級別的日志打印到控制臺了嗎?
          2. 最后的 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é)果見下圖

          image-20210525201656456

          問題 1:

          沒有打印。AtomicLevel 是原子性可更改的動態(tài)日志級別,通過調(diào)用 atom.SetLevel 更改日志級別為 infoLevel 。

          問題 2:

          沒有打印。zap.Development() 啟用了開發(fā)模式,在開發(fā)模式下 DPanic 函數(shù)會引發(fā) panic,所以最后的 error 不會打印到控制臺。

          源碼分析

          此次源碼分析基于 Zap 1.16

          zap概覽

          上圖僅表示 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)化。所以對于每個日志級別,都提供了三種方法。

          level

          [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("...") 打上斷點,從這開始追蹤源碼。

          image-20210519111252185

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

          infof工作流程

          Info , InfofInfow 三個函數(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í)行。

          為什么沒有LoggerSugaredLogger接口?

          不像熟悉的io.Writerhttp.Handler、LoggerSugaredLogger接口將包括很多方法。正如 Rob Pike 諺語指出[2]的,"The bigger the interface, the weaker the abstraction"(接口越大,抽象越弱)。接口也是嚴格的,任何更改都需要發(fā)布一個新的主版本,因為它打破了所有第三方實現(xiàn)。

          LoggerSugaredLogger成為具體類型并不會犧牲太多抽象,而且它允許我們在不引入破壞性更改的情況下添加方法。您的應(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)雅地處理錯誤,而不是使用panicos.Exit。但是,每個規(guī)則都有例外,當(dāng)錯誤確實無法恢復(fù)時,崩潰是很常見的。為了避免丟失任何信息(尤其是崩潰的原因),記錄器必須在進程退出之前沖洗任何緩沖條目。

          Zap 通過提供在退出前自動沖洗的PanicFatal記錄方法來使這一操作變得簡單。當(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/zapextSentry, syslog
          github.com/fgrosse/zaptestGinkgo
          github.com/blendle/zapdriverStackdriver
          github.com/moul/zapgormGorm

          性能比較

          說明 : 以下資料來源于 zap 官方,Zap 提供的基準測試清楚地表明,zerolog[5]是與 Zap 競爭最激烈的。zerolo還提供結(jié)果非常相似的基準測試[6]

          記錄一個10個kv字段的消息:

          PackageTimeTime % to zapObjects Allocated
          ? zap862 ns/op+0%5 allocs/op
          ? zap (sugared)1250 ns/op+45%11 allocs/op
          zerolog4021 ns/op+366%76 allocs/op
          go-kit4542 ns/op+427%105 allocs/op
          apex/log26785 ns/op+3007%115 allocs/op
          logrus29501 ns/op+3322%125 allocs/op
          log1529906 ns/op+3369%122 allocs/op

          使用一個已經(jīng)有10個kv字段的logger記錄一條消息:

          PackageTimeTime % to zapObjects Allocated
          ? zap126 ns/op+0%0 allocs/op
          ? zap (sugared)187 ns/op+48%2 allocs/op
          zerolog88 ns/op-30%0 allocs/op
          go-kit5087 ns/op+3937%103 allocs/op
          log1518548 ns/op+14621%73 allocs/op
          apex/log26012 ns/op+20544%104 allocs/op
          logrus27236 ns/op+21516%113 allocs/op

          記錄一個字符串,沒有字段或printf風(fēng)格的模板:

          PackageTimeTime % to zapObjects Allocated
          ? zap118 ns/op+0%0 allocs/op
          ? zap (sugared)191 ns/op+62%2 allocs/op
          zerolog93 ns/op-21%0 allocs/op
          go-kit280 ns/op+137%11 allocs/op
          standard library499 ns/op+323%2 allocs/op
          apex/log1990 ns/op+1586%10 allocs/op
          logrus3129 ns/op+2552%24 allocs/op
          log153887 ns/op+3194%23 allocs/op

          相似的庫

          logrus[7] 功能強大

          zerolog[8] 性能相當(dāng)好的日志庫

          參考資料

          [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




          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進階看什么。關(guān)注公眾號 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 78
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  伊人网在线观看了 | 先锋影音Vs男人资源站 | 五十路老熟女 码一区二区 | 青青草免费在线看视频 | 欧美三级无码 |