玩轉(zhuǎn) Go 日志框架 zap
本文包括兩部分,一部分是源碼解讀,另一部分是對zap的增強(qiáng)。
由于zap是一個(gè)log庫,所以從兩方面來深入閱讀zap的源碼,一個(gè)是初始化logger的流程,一個(gè)是打一條log的流程。
Github地址:github.com/uber-go/zap…
初始化Logger
zap的Logger是一般通過一個(gè)Config結(jié)構(gòu)體初始化的,首先看下這個(gè)結(jié)構(gòu)體有哪些字段
type?Config?struct?{
???????//?日志Level,因?yàn)榭梢詣討B(tài)更改,所以是atomic類型的,畢竟比鎖的性能好
???????Level?AtomicLevel?`json:"level"?yaml:"level"`
???????//?dev模式,啟用后會更改在某些使用情形下的行為,后面源碼解讀模塊會具體看到有什么作用
???????Development?bool?`json:"development"?yaml:"development"`
???????//?禁用caller,caller就是會在打的log里加一條屬性,表示這條日志是在哪里打的,例如"httpd/svc.go:123"
???????DisableCaller?bool?`json:"disableCaller"?yaml:"disableCaller"`
???????//?是否要在log里加上調(diào)用棧,dev模式下只有WarnLevel模式以上有調(diào)用棧,prod模式下只有ErrorLevel以上有調(diào)用棧
???????DisableStacktrace?bool?`json:"disableStacktrace"?yaml:"disableStacktrace"`
???????//?采樣策略,控制打log的速率,也可以做一些其他自定義的操作,不難理解,具體看下面的SamplingConfig
???????Sampling?*SamplingConfig?`json:"sampling"?yaml:"sampling"`
???????//?log格式,自帶的有json和console兩種格式,可以通過使用RegisterEncoder來自定義log格式
???????Encoding?string?`json:"encoding"?yaml:"encoding"`
???????//?log格式具體配置,詳細(xì)看下面的EncoderConfig
???????EncoderConfig?zapcore.EncoderConfig?`json:"encoderConfig"?yaml:"encoderConfig"`
???????//?log輸出路徑,看結(jié)構(gòu)表示可以有多個(gè)輸出路徑
???????OutputPaths?[]string?`json:"outputPaths"?yaml:"outputPaths"`
???????//?內(nèi)部錯(cuò)誤輸出路徑,默認(rèn)是stderr
???????ErrorOutputPaths?[]string?`json:"errorOutputPaths"?yaml:"errorOutputPaths"`
???????//?每條log都會加上InitialFields里的內(nèi)容,顧名思義
???????InitialFields?map[string]interface{}?`json:"initialFields"?yaml:"initialFields"`
}
//?采樣策略配置,大致的邏輯是每秒超過Thereafter個(gè)相同msg的log會執(zhí)行自定義的Hook函數(shù)(第二個(gè)參數(shù)為一個(gè)標(biāo)志,LogDropped),具體邏輯可以看下面的源碼解析
type?SamplingConfig?struct?{
????????Initial????int???????????????????????????????????????????`json:"initial"?yaml:"initial"`
????????Thereafter?int???????????????????????????????????????????`json:"thereafter"?yaml:"thereafter"`
????????Hook???????func(zapcore.Entry,?zapcore.SamplingDecision)?`json:"-"?yaml:"-"`
}
const?(
????????_numLevels????????=?_maxLevel?-?_minLevel?+?1
????????_countersPerLevel?=?4096
)
//?用來記錄日志打了多少條
type?counter?struct?{
????????resetAt?atomic.Int64
????????counter?atomic.Uint64
}
type?counters?[_numLevels][_countersPerLevel]counter
//?這里可以看到sampler就是Core外面包了一層Wrapper
type?sampler?struct?{
????????Core
????????counts????????????*counters
????????tick??????????????time.Duration????????????????//?這里的tick在初始化Logger的時(shí)候已經(jīng)寫死了是time.Second,也就是1秒
????????first,?thereafter?uint64
????????hook??????????????func(Entry,?SamplingDecision)
}
//?所有的Core.Check都會先走sampler.Check,然后再走Core.Check
func?(s?*sampler)?Check(ent?Entry,?ce?*CheckedEntry)?*CheckedEntry?{
????????if?!s.Enabled(ent.Level)?{
???????????????return?ce
????????}
????????if?ent.Level?>=?_minLevel?&&?ent.Level?<=?_maxLevel?{
????????????????//?根據(jù)Message獲取counter,也就是這個(gè)Message打過幾次日志了
????????????????counter?:=?s.counts.get(ent.Level,?ent.Message)
????????????????//?打一條Message就會記錄一次到counters里,不過每秒會重置一次counter,具體看IncCheckReset里的邏輯
????????????????n?:=?counter.IncCheckReset(ent.Time,?s.tick)
????????????????//?first表示最初的first條日志調(diào)用hook時(shí)第二個(gè)參數(shù)傳LogSampled,超過first的日志,每threrafter條日志第二個(gè)參數(shù)傳LogSampled,否則傳LogDropped
????????????????//?假設(shè)first是100,thereafter是50,表示同一個(gè)Message的log,最初的100條全都會記錄,之后的log在每秒鐘內(nèi),每50條記錄一次
????????????????if?n?>?s.first?&&?(s.thereafter?==?0?||?(n-s.first)%s.thereafter?!=?0)?{
???????????????????????s.hook(ent,?LogDropped)
???????????????????????return?ce
????????????????}
????????????????s.hook(ent,?LogSampled)
???????}
???????return?s.Core.Check(ent,?ce)
}
//?這里可能會出現(xiàn)意想不到的情況
//?因?yàn)開countersPerLevel寫死了是4096,那么必然會存在不同的key做完hash后取模會路由到相同的counter里
//?那么就會有概率丟棄掉沒有達(dá)到丟棄閾值的log
//?假設(shè)abc和def的hash值一樣,first是0,thereafter是10,表示每秒鐘每種log每10條才會記錄1次,那么abc和def這兩種log就會共享同一個(gè)counter,這就是問題所在
func?(cs?*counters)?get(lvl?Level,?key?string)?*counter?{
???????i?:=?lvl?-?_minLevel
???????//?fnv32a是一個(gè)hash函數(shù)
???????//?_countersPerLevel固定是4096
???????j?:=?fnv32a(key)?%?_countersPerLevel
???????return?&cs[i][j]
}
func?(c?*counter)?IncCheckReset(t?time.Time,?tick?time.Duration)?uint64?{
???????tn?:=?t.UnixNano()
???????resetAfter?:=?c.resetAt.Load()
???????if?resetAfter?>?tn?{
??????????????return?c.counter.Inc()
???????}
???????c.counter.Store(1)
???????newResetAfter?:=?tn?+?tick.Nanoseconds()
???????if?!c.resetAt.CAS(resetAfter,?newResetAfter)?{
??????????????return?c.counter.Inc()
???????}
???????return?1
}
//?log格式的詳細(xì)設(shè)置
type?EncoderConfig?struct?{
??????//?設(shè)置log內(nèi)容里的一些屬性的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"`
??????//?顧名思義不解釋
??????SkipLineEnding?bool???`json:"skipLineEnding"?yaml:"skipLineEnding"`
??????LineEnding?????string?`json:"lineEnding"?yaml:"lineEnding"`
??????//?Configure?the?primitive?representations?of?common?complex?types.?For
??????//?example,?some?users?may?want?all?time.Times?serialized?as?floating-point
??????//?seconds?since?epoch,?while?others?may?prefer?ISO8601?strings.
??????//?自定義一些屬性的格式,例如指定Time字段格式化為2022-05-23?16:16:16
??????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"`
??????//?用于interface類型的encoder,可以自定義,默認(rèn)為jsonEncoder
??????NewReflectedEncoder?func(io.Writer)?ReflectedEncoder?`json:"-"?yaml:"-"`
??????//?console格式的分隔符,默認(rèn)是tab
??????ConsoleSeparator?string?`json:"consoleSeparator"?yaml:"consoleSeparator"`
}
Config里的大部分字段都有tag,可以通過UnmarshalJson或者UnmarshalYaml來配置,可以在全局的config文件來配置,非常方便。
通過以上的config就可以初始化一個(gè)logger,下面貼代碼
//?通過Config結(jié)構(gòu)體Build出一個(gè)Logger
func?(cfg?Config)?Build(opts?...Option)?(*Logger,?error)?{
???????//?核心函數(shù)buildEncoder
???????enc,?err?:=?cfg.buildEncoder()
???????if?err?!=?nil?{
??????????????return?nil,?err
???????}
???????//?核心函數(shù)openSinks
???????sink,?errSink,?err?:=?cfg.openSinks()
???????if?err?!=?nil?{
??????????????return?nil,?err
???????}
???????if?cfg.Level?==?(AtomicLevel{})?{
??????????????return?nil,?fmt.Errorf("missing?Level")
???????}
???????//?核心函數(shù)New
???????log?:=?New(
???????????????//?核心函數(shù)NewCore
???????????????zapcore.NewCore(enc,?sink,?cfg.Level),
???????????????cfg.buildOptions(errSink)...,
???????)
???????if?len(opts)?>?0?{
???????????????log?=?log.WithOptions(opts...)
???????}
???????return?log,?nil
}
//?核心函數(shù)buildEncoder
func?(cfg?Config)?buildEncoder()?(zapcore.Encoder,?error)?{
???????return?newEncoder(cfg.Encoding,?cfg.EncoderConfig)
}
//?_encoderNameToConstructor是一個(gè)map[string]constructor,plugin式寫法,可以通過RegisterEncoder函數(shù)注冊自定義的Encoder,默認(rèn)只有console和json
_encoderNameToConstructor?=?map[string]func(zapcore.EncoderConfig)?(zapcore.Encoder,?error){
???????"console":?func(encoderConfig?zapcore.EncoderConfig)?(zapcore.Encoder,?error)?{
??????????????return?zapcore.NewConsoleEncoder(encoderConfig),?nil
???????},
???????"json":?func(encoderConfig?zapcore.EncoderConfig)?(zapcore.Encoder,?error)?{
???????????????return?zapcore.NewJSONEncoder(encoderConfig),?nil
???????},
}
func?newEncoder(name?string,?encoderConfig?zapcore.EncoderConfig)?(zapcore.Encoder,?error)?{
???????if?encoderConfig.TimeKey?!=?""?&&?encoderConfig.EncodeTime?==?nil?{
??????????????return?nil,?fmt.Errorf("missing?EncodeTime?in?EncoderConfig")
???????}
???????_encoderMutex.RLock()
???????defer?_encoderMutex.RUnlock()
???????if?name?==?""?{
???????????????return?nil,?errNoEncoderNameSpecified
???????}
???????//?通過name,也就是Config.Encoding來決定使用哪種encoder
???????constructor,?ok?:=?_encoderNameToConstructor[name]
???????if?!ok?{
??????????????return?nil,?fmt.Errorf("no?encoder?registered?for?name?%q",?name)
???????}
???????return?constructor(encoderConfig)
}
//?這里只展示jsonEncoder的邏輯,consoleEncoder和jsonEncoder差別不大
func?NewJSONEncoder(cfg?EncoderConfig)?Encoder?{
???????return?newJSONEncoder(cfg,?false)
}
func?newJSONEncoder(cfg?EncoderConfig,?spaced?bool)?*jsonEncoder?{
???????if?cfg.SkipLineEnding?{
???????????????cfg.LineEnding?=?""
???????}?else?if?cfg.LineEnding?==?""?{
???????????????cfg.LineEnding?=?DefaultLineEnding
???????}
???????//?If?no?EncoderConfig.NewReflectedEncoder?is?provided?by?the?user,?then?use?default
???????if?cfg.NewReflectedEncoder?==?nil?{
???????????????cfg.NewReflectedEncoder?=?defaultReflectedEncoder
???????}
???????return?&jsonEncoder{
???????????????EncoderConfig:?&cfg,
???????????????//?這個(gè)buf是高性能的關(guān)鍵之一,使用了簡化的bytesBuffer和sync.Pool,代碼貼在下面
???????????????buf:???????????bufferpool.Get(),
???????????????spaced:????????spaced,
???????}
}
type?Buffer?struct?{
???????bs???[]byte
???????pool?Pool
}
type?Pool?struct?{
???????p?*sync.Pool
}
func?NewPool()?Pool?{
???????return?Pool{p:?&sync.Pool{
???????????????New:?func()?interface{}?{
??????????????????????return?&Buffer{bs:?make([]byte,?0,?_size)}
???????????????},
???????}}
}
//?從Pool里拿一個(gè)Buffer,初始化里面的[]byte
func?(p?Pool)?Get()?*Buffer?{
???????buf?:=?p.p.Get().(*Buffer)
???????buf.Reset()
???????//?這里賦值pool為當(dāng)前Pool,用于使用完Buffer后把Buffer后放回pool里,也就是下面的put函數(shù)
???????buf.pool?=?p
???????return?buf
}
func?(p?Pool)?put(buf?*Buffer)?{
???????p.p.Put(buf)
}
//?核心函數(shù)openSinks
func?(cfg?Config)?openSinks()?(zapcore.WriteSyncer,?zapcore.WriteSyncer,?error)?{
???????sink,?closeOut,?err?:=?Open(cfg.OutputPaths...)
???????if?err?!=?nil?{
???????return?nil,?nil,?err
???????}
???????errSink,?_,?err?:=?Open(cfg.ErrorOutputPaths...)
???????if?err?!=?nil?{
???????????????closeOut()
???????????????return?nil,?nil,?err
???????}
???????return?sink,?errSink,?nil
}
func?Open(paths?...string)?(zapcore.WriteSyncer,?func(),?error)?{
???????writers,?close,?err?:=?open(paths)
???????if?err?!=?nil?{
???????????????return?nil,?nil,?err
???????}
???????writer?:=?CombineWriteSyncers(writers...)
???????return?writer,?close,?nil
}
func?open(paths?[]string)?([]zapcore.WriteSyncer,?func(),?error)?{
???????writers?:=?make([]zapcore.WriteSyncer,?0,?len(paths))
???????closers?:=?make([]io.Closer,?0,?len(paths))
???????close?:=?func()?{
???????????????for?_,?c?:=?range?closers?{
???????????????????????c.Close()
???????????????}
???????}
???????var?openErr?error
???????for?_,?path?:=?range?paths?{
???????????????//?核心函數(shù)newSink
???????????????sink,?err?:=?newSink(path)
???????????????if?err?!=?nil?{
????????????????????openErr?=?multierr.Append(openErr,?fmt.Errorf("couldn't?open?sink?%q:?%v",?path,?err))
???????????????????continue
??????????????}
??????????????writers?=?append(writers,?sink)
??????????????closers?=?append(closers,?sink)
???????}
???????if?openErr?!=?nil?{
??????????????close()
??????????????return?writers,?nil,?openErr
???????}
???????return?writers,?close,?nil
}
//?這里也是plugin式寫法,可以通過RegisterSink來自定義sink,比如自定義一個(gè)支持http協(xié)議的sink,在文章的尾部會實(shí)現(xiàn)一個(gè)自定義的sink
_sinkFactories?=?map[string]func(*url.URL)?(Sink,?error){
????????schemeFile:?newFileSink,
}
func?newSink(rawURL?string)?(Sink,?error)?{
????????//?通過rawURL判斷初始化哪種sink,實(shí)際上zap只支持file,看上面的_sinkFactories
????????u,?err?:=?url.Parse(rawURL)
????????if?err?!=?nil?{
????????????????return?nil,?fmt.Errorf("can't?parse?%q?as?a?URL:?%v",?rawURL,?err)
????????}
????????//?如果url是類似于/var/abc.log這種的字符串,那么經(jīng)過Parse后的u.Scheme就是"",然后會被賦值schemeFile
????????//?如果url是類似于http://127.0.0.1:1234這種的字符串,那么經(jīng)過Parse后的u.Scheme就是"http",不過zap本身不支持http,可以自定義一個(gè)支持http的sink
????????if?u.Scheme?==?""?{
????????????????u.Scheme?=?schemeFile
????????}
????????_sinkMutex.RLock()
????????factory,?ok?:=?_sinkFactories[u.Scheme]
????????_sinkMutex.RUnlock()
????????if?!ok?{
????????????????return?nil,?&errSinkNotFound{u.Scheme}
????????}
????????return?factory(u)
}
//?這里的sink實(shí)際上就是一個(gè)*File
func?newFileSink(u?*url.URL)?(Sink,?error)?{
????????//?...
????????switch?u.Path?{
????????case?"stdout":
????????????????return?nopCloserSink{os.Stdout},?nil
????????case?"stderr":
????????????????return?nopCloserSink{os.Stderr},?nil
????????}
????????return?os.OpenFile(u.Path,?os.O_WRONLY|os.O_APPEND|os.O_CREATE,?0666)
}
//?核心函數(shù)NewCore
func?NewCore(enc?Encoder,?ws?WriteSyncer,?enab?LevelEnabler)?Core?{
????????return?&ioCore{
????????????????LevelEnabler:?enab,
????????????????enc:??????????enc,
????????????????out:??????????ws,
????????}
}
//?核心函數(shù)New
func?New(core?zapcore.Core,?options?...Option)?*Logger?{
????????if?core?==?nil?{
????????????????return?NewNop()
????????}
????????log?:=?&Logger{
????????????????core:????????core,
????????????????errorOutput:?zapcore.Lock(os.Stderr),
????????????????addStack:????zapcore.FatalLevel?+?1,
????????????????clock:???????zapcore.DefaultClock,
????????}
????????return?log.WithOptions(options...)
}
到New這里,就完成了一個(gè)logger的初始化,核心的結(jié)構(gòu)體就是Encoder、Sink和ioCore,邏輯還是比較簡單易懂的
打一條Log
下面寫一段簡單的demo
l,?_?:=?zap.NewProduction()
l.Error("Message?Content",?zap.String("tagA",?"tagAValue"))
func?(log?*Logger)?Error(msg?string,?fields?...Field)?{
????????//?核心函數(shù)?check
????????if?ce?:=?log.check(ErrorLevel,?msg);?ce?!=?nil?{
????????????????//?核心函數(shù)?Write
????????????????ce.Write(fields...)
????????}
}
//?核心函數(shù)check,實(shí)際邏輯就是檢查了下Level要不要打log,順便添加了調(diào)用棧和caller
func?(log?*Logger)?check(lvl?zapcore.Level,?msg?string)?*zapcore.CheckedEntry?{
????????//?跳過當(dāng)前這個(gè)check函數(shù)以及調(diào)用check的Error/Info/Fatal等函數(shù)
????????const?callerSkipOffset?=?2
????????//?檢查level
????????if?lvl?<?zapcore.DPanicLevel?&&?!log.core.Enabled(lvl)?{
????????????????return?nil
????????}
????????ent?:=?zapcore.Entry{
????????????????LoggerName:?log.name,
????????????????Time:???????log.clock.Now(),
????????????????Level:??????lvl,
????????????????Message:????msg,
????????}
????????//?核心函數(shù)?ioCore.Check
????????ce?:=?log.core.Check(ent,?nil)
????????willWrite?:=?ce?!=?nil
????????//?...
????????if?!willWrite?{
????????????????return?ce
????????}
????????//?添加stacktrace和caller相關(guān)
????????//?...
????????return?ce
}
//?實(shí)際就是把core添加到了CheckedEntry里,在后續(xù)的CheckedEntry.Write里會被調(diào)用
func?(c?*ioCore)?Check(ent?Entry,?ce?*CheckedEntry)?*CheckedEntry?{
????????if?c.Enabled(ent.Level)?{
????????????????return?ce.AddCore(ent,?c)
????????}
????????return?ce
}
func?(ce?*CheckedEntry)?AddCore(ent?Entry,?core?Core)?*CheckedEntry?{
????????if?ce?==?nil?{
????????????????//?getCheckedEntry使用了sync.Pool
????????????????ce?=?getCheckedEntry()
????????????????ce.Entry?=?ent
????????}
????????ce.cores?=?append(ce.cores,?core)
????????return?ce
}
//?核心函數(shù)?Write
func?(ce?*CheckedEntry)?Write(fields?...Field)?{
????????//?...
????????var?err?error
????????//?實(shí)際就是調(diào)用了Core.Write
????????for?i?:=?range?ce.cores?{
????????????????err?=?multierr.Append(err,?ce.cores[i].Write(ce.Entry,?fields))
????????}
????????if?err?!=?nil?&&?ce.ErrorOutput?!=?nil?{
????????????????fmt.Fprintf(ce.ErrorOutput,?"%v?write?error:?%v\n",?ce.Time,?err)
????????????????ce.ErrorOutput.Sync()
????????}
????????should,?msg?:=?ce.should,?ce.Message
????????//?把CheckedEntry放回到pool里
????????putCheckedEntry(ce)
????????//?...
}
func?(c?*ioCore)?Write(ent?Entry,?fields?[]Field)?error?{
????????//?首先Encode,高性能的核心就在EncodeEntry里
????????buf,?err?:=?c.enc.EncodeEntry(ent,?fields)
????????if?err?!=?nil?{
????????????????return?err
????????}
????????//?然后Write,out就是sink
????????_,?err?=?c.out.Write(buf.Bytes())
????????//?然后把buf放回到pool里
????????buf.Free()
????????if?err?!=?nil?{
????????????????return?err
????????}
????????if?ent.Level?>?ErrorLevel?{
????????????????//?Since?we?may?be?crashing?the?program,?sync?the?output.?Ignore?Sync
????????????????//?errors,?pending?a?clean?solution?to?issue?#370.
????????????????c.Sync()
????????}
????????return?nil
}
//?zap并沒有使用類似marshalJson的方法來encode,而是使用了拼接字符串的方式手動拼出了一個(gè)json字符串,這種方式的性能比marshalJson的性能要好很多
//?里面的具體邏輯很簡單,就是append一個(gè)key,append一個(gè)value
func?(enc?*jsonEncoder)?EncodeEntry(ent?Entry,?fields?[]Field)?(*buffer.Buffer,?error)?{
????????final?:=?enc.clone()
????????final.buf.AppendByte('{')
????????if?final.LevelKey?!=?""?&&?final.EncodeLevel?!=?nil?{
????????????????final.addKey(final.LevelKey)
????????????????cur?:=?final.buf.Len()
????????????????final.EncodeLevel(ent.Level,?final)
????????????????if?cur?==?final.buf.Len()?{
????????????????????????//?User-supplied?EncodeLevel?was?a?no-op.?Fall?back?to?strings?to?keep
????????????????????????//?output?JSON?valid.
????????????????????????final.AppendString(ent.Level.String())
????????????????}
????????}
????????if?final.TimeKey?!=?""?{
????????????????final.AddTime(final.TimeKey,?ent.Time)
????????}
????????if?ent.LoggerName?!=?""?&&?final.NameKey?!=?""?{
????????????????final.addKey(final.NameKey)
????????????????cur?:=?final.buf.Len()
????????????????nameEncoder?:=?final.EncodeName
????????????????//?if?no?name?encoder?provided,?fall?back?to?FullNameEncoder?for?backwards
????????????????//?compatibility
????????????????if?nameEncoder?==?nil?{
????????????????????????nameEncoder?=?FullNameEncoder
????????????????}
????????????????nameEncoder(ent.LoggerName,?final)
????????????????if?cur?==?final.buf.Len()?{
????????????????????????//?User-supplied?EncodeName?was?a?no-op.?Fall?back?to?strings?to
????????????????????????//?keep?output?JSON?valid.
????????????????????????final.AppendString(ent.LoggerName)
????????????????}
????????}
????????if?ent.Caller.Defined?{
????????????????if?final.CallerKey?!=?""?{
????????????????????????final.addKey(final.CallerKey)
????????????????????????cur?:=?final.buf.Len()
????????????????????????final.EncodeCaller(ent.Caller,?final)
????????????????????????if?cur?==?final.buf.Len()?{
????????????????????????????????//?User-supplied?EncodeCaller?was?a?no-op.?Fall?back?to?strings?to
????????????????????????????????//?keep?output?JSON?valid.
????????????????????????????????final.AppendString(ent.Caller.String())
????????????????????????}
????????????????}
????????????????if?final.FunctionKey?!=?""?{
????????????????????????final.addKey(final.FunctionKey)
????????????????????????final.AppendString(ent.Caller.Function)
????????????????}
????????}
????????if?final.MessageKey?!=?""?{
????????????????final.addKey(enc.MessageKey)
????????????????final.AppendString(ent.Message)
????????}
????????if?enc.buf.Len()?>?0?{
????????????????final.addElementSeparator()
????????????????final.buf.Write(enc.buf.Bytes())
????????}
????????addFields(final,?fields)
????????final.closeOpenNamespaces()
????????if?ent.Stack?!=?""?&&?final.StacktraceKey?!=?""?{
????????????????final.AddString(final.StacktraceKey,?ent.Stack)
????????}
????????final.buf.AppendByte('}')
????????final.buf.AppendString(final.LineEnding)
????????ret?:=?final.buf
????????putJSONEncoder(final)
????????return?ret,?nil
}
encode完之后就是Write了,實(shí)際調(diào)用的就是Sink.Write,如果log是寫到文件里的,那么調(diào)用的就是File.Write,至此一條日志記錄完成
總結(jié)
zap記錄一條日志的流程可以概括為3步
-
check -
encode -
write
zap在性能優(yōu)化方面有一些值得借鑒的地方
-
多處使用sync.Pool和bytes.Buffer優(yōu)化GC -
使用了自實(shí)現(xiàn)的jsonEncoder,簡化了encode邏輯
不過zap的log抑制,也就是sampler實(shí)現(xiàn)有些過于簡單,可能會出現(xiàn)log丟失的問題,下面的代碼可以完美復(fù)現(xiàn)這個(gè)問題
lc?:=?zap.NewProductionConfig()
lc.Encoding?=?"console"
lc.Sampling.Initial?=?1????//?當(dāng)Initial為1時(shí),第二條日志不會打印出來,改為大于1時(shí)第二條日志才會打印出來
lc.Sampling.Thereafter?=?10
l,?_?:=?lc.Build()
l.Info("abc")
l.Info("yTI")
l.Info("def")
增強(qiáng)zap
自定義sink
在閱讀源碼部分已經(jīng)提到了zap只支持log寫到文件里,一般業(yè)務(wù)日志都會統(tǒng)一收集到日志中心,那么就需要自定義一個(gè)sink,通過網(wǎng)絡(luò)發(fā)送到某個(gè)地方統(tǒng)一收集起來,下面寫一個(gè)簡單的http協(xié)議的sink。
func?init()?{
????????//?這里注冊http?sink
????????err?:=?zap.RegisterSink("http",?httpSink)
????????if?err?!=?nil?{
????????????????fmt.Println("Register?http?sink?fail",?err)
????????}
}
func?httpSink(url?*url.URL)?(zap.Sink,?error)?{
????????return?&Http{
????????????????//?httpc是我封裝的httpClient,沒什么別的邏輯,直接當(dāng)成http.Client就好
????????????????httpc:?httpc.New(httpc.NewConfig(),?context.Background()),
????????????????url:???url,
????????},?nil
}
type?Http?struct?{
????????httpc?*httpc.HttpC
????????url???*url.URL
}
//?主要邏輯就是Write
func?(h?*Http)?Write(p?[]byte)?(n?int,?err?error)?{
????????//?初始化request
????????req,?err?:=?http.NewRequest("POST",?h.url.String(),?bytes.NewReader(p))
????????if?err?!=?nil?{
????????????????return?0,?err
????????}
????????//?執(zhí)行http請求
????????resp,?err?:=?h.httpc.Do(req)
????????if?err?!=?nil?{
????????????????return?0,?err
????????}
????????defer?resp.Body.Close()
????????//?獲取response
????????respBody,?err?:=?ioutil.ReadAll(resp.Body)
????????if?err?!=?nil?{
????????????????return?0,?err
????????}
????????if?resp.StatusCode?!=?http.StatusNoContent?&&?resp.StatusCode?!=?http.StatusOK?{
????????????????return?0,?errors.New(util.Bytes2String(respBody))
????????}
????????return?len(p),?nil
}
//?可以搞個(gè)內(nèi)置的queue或者[]log,在Sync函數(shù)里用來做批量發(fā)送提升性能,這里只是簡單的實(shí)現(xiàn),所以Sync沒什么邏輯
func?(h?*Http)?Sync()?error?{
????????return?nil
}
func?(h?*Http)?Close()?error?{
????????return?h.httpc.Close()
}
寫完sink后,只需要在Config里的outputPaths里添加一條"http://xxx:xxx" ,所有的log就會走到自定義的sink邏輯,通過http發(fā)送出去。
來點(diǎn)騷操作,在源碼閱讀部分,可以看到zap是把url.Parse后的scheme當(dāng)作sink名稱的,例如在Config里的outputPaths里添加一條"wtf://xxx:xxx",zap就會去尋找名稱為wtf的sink,我們把上面的http sink的zap.RegisterSink("http", httpSink)改為zap.RegisterSink("wtf", httpSink),然后在Write函數(shù)的邏輯里把"wtf://"后面的內(nèi)容拼成一個(gè)完整的http url,同樣可以運(yùn)行,操作是不是很騷。
error調(diào)用棧
當(dāng)使用zap打Error日志時(shí),如果配置了addStack,那么zap會自動把調(diào)用棧寫到log里,下面是一個(gè)例子
package?main
import?(
????????"go.uber.org/zap"
)
var?l?*zap.Logger
func?test_a()?{
????????test_b()
}
func?test_b()?{
????????test_c()
}
func?test_c()?{
????????l.Error("err?content")
}
func?main()?{
????????l,?_?=?zap.NewDevelopment()
????????test_a()
}
這是log內(nèi)容,當(dāng)使用jsonEncoder時(shí),調(diào)用棧會在stacktrace字段里,下面是console格式的
2022-05-23T23:16:36.598+0800 ERROR gtil/main.go:20 err content
main.test_c
D:/workspace/code/go/gtil/main.go:20
main.test_b
D:/workspace/code/go/gtil/main.go:16
main.test_a
D:/workspace/code/go/gtil/main.go:12
main.main
D:/workspace/code/go/gtil/main.go:25
runtime.main
D:/workspace/env/scoop/apps/go/current/src/runtime/proc.go:250
這么一看好像很完美,有error日志了還可以看到調(diào)用棧,但是我們一般打log時(shí),總是會在最上層打log,而不是每一層都打log,拿上面的代碼舉例子
func?test_a()?error?{
????????return?test_b()
}
func?test_b()?error?{
????????return?test_c()
}
func?test_c()?error?{
????????//?底層的函數(shù)出現(xiàn)error應(yīng)該return,而不是打log
????????return?errors.new("do?test_c?fail")
}
func?main()?{
????????l,?_?=?zap.NewProduction()
????????err?:=?test_a()
????????if?err?!=?nil?{
????????????????l.Error("main?error",?zap.Error(err))
????????}
}
下面是log內(nèi)容
2022-05-23T23:16:54.955+0800 ERROR gtil/main.go:27 main error {"error": "do test_c fail"}
main.main
D:/workspace/code/go/gtil/main.go:27
runtime.main
D:/workspace/env/scoop/apps/go/current/src/runtime/proc.go:250
這就出現(xiàn)了幾個(gè)問題,1. 調(diào)用棧只到了main,沒有更底層的, 2. 如果test_b接受到test_c的error時(shí),想加上一些自己的error content返回出去,這兩個(gè)問題就體現(xiàn)出了golang在錯(cuò)誤處理方面的不足,不過有一個(gè)庫可以解決這兩個(gè)問題,github.com/pkg/errors?,這個(gè)庫自定義了error,可以在error里添加調(diào)用棧或額外的信息,下面寫個(gè)demo
func?test_a()?error?{
????????err?:=?test_b()
????????if?err?!=?nil?{
????????????????return?errors.Wrap(err,?"do?test_a?fail")
????????}
????????return?nil
}
func?test_b()?error?{
????????err?:=?test_c()
????????if?err?!=?nil?{
????????????????return?errors.Wrap(err,?"do?test_b?fail")
????????}
????????return?nil
}
func?test_c()?error?{
????????return?errors.New("do?test_c?fail")
}
func?main()?{
????????l,?_?=?zap.NewDevelopment()
????????err?:=?test_a()
????????if?err?!=?nil?{
????????????????l.Error("main?error",?zap.Error(err))
????????}
}
下面是輸出內(nèi)容,可以看到在errorVerbose字段里每一個(gè)函數(shù)的error都返回了出來,并帶上了調(diào)用棧,不過error字段有點(diǎn)亂七八糟,并且還顯示了zap自帶的調(diào)用棧
2022-05-23T23:34:13.339+0800 ERROR gtil/main.go:34 main error {"error": "do test_a fail: do test_b fail: do test_c fail", "errorVerbose": "do test_c fail\nmain.test_c\n\tD:/workspace/code/go/gtil/main.go:27\nmain.test_b\n\tD:/workspace/code/go/gtil/main.go:19\nmain.test_a\n\tD:/workspace/code/go/gtil/main.go:11\nmain.main\n\tD:/workspace/code/go/gtil/main.go:32\nruntime.main\n\tD:/workspace/env/scoop/apps/go/current/src/runtime/proc.go:250\nruntime.goexit\n\tD:/workspace/env/scoop/apps/go/current/src/runtime/asm_amd64.s:1571\ndo test_b fail\nmain.test_b\n\tD:/workspace/code/go/gtil/main.go:21\nmain.test_a\n\tD:/workspace/code/go/gtil/main.go:11\nmain.main\n\tD:/workspace/code/go/gtil/main.go:32\nruntime.main\n\tD:/workspace/env/scoop/apps/go/current/src/runtime/proc.go:250\nruntime.goexit\n\tD:/workspace/env/scoop/apps/go/current/src/runtime/asm_amd64.s:1571\ndo test_a fail\nmain.test_a\n\tD:/workspace/code/go/gtil/main.go:13\nmain.main\n\tD:/workspace/code/go/gtil/main.go:32\nruntime.main\n\tD:/workspace/env/scoop/apps/go/current/src/runtime/proc.go:250\nruntime.goexit\n\tD:/workspace/env/scoop/apps/go/current/src/runtime/asm_amd64.s:1571"}
main.main
D:/workspace/code/go/gtil/main.go:34
runtime.main
D:/workspace/env/scoop/apps/go/current/src/runtime/proc.go:250
要做的就是去掉自帶的調(diào)用棧,把error字段搞地好看點(diǎn),只需要自定義一個(gè)Core就可以,下面貼出代碼
func?NewErrStackCore(c?zapcore.Core)?zapcore.Core?{
????return?&errStackCore{c}
}
type?errStackCore?struct?{
????zapcore.Core
}
func?(c?*errStackCore)?With(fields?[]zapcore.Field)?zapcore.Core?{
????return?&errStackCore{
????????c.Core.With(fields),
????}
}
func?(c?*errStackCore)?Write(ent?zapcore.Entry,?fields?[]zapcore.Field)?error?{
????//?判斷fields里有沒有error字段
????if?!hasStackedErr(fields)?{
????????return?c.Core.Write(ent,?fields)
????}
????//?這里是重點(diǎn),從fields里取出error字段,把內(nèi)容放到ent.Stack里,邏輯就是這樣,具體代碼就不給出了
????ent.Stack,?fields?=?getStacks(fields)
????return?c.Core.Write(ent,?fields)
}
func?(c?*errStackCore)?Check(ent?zapcore.Entry,?ce?*zapcore.CheckedEntry)?*zapcore.CheckedEntry?{
????return?c.Core.Check(ent,?ce)
}
轉(zhuǎn)自:https://juejin.cn/post/7125012224623509540