<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
本文包括兩部分,一部分是原始碼解讀,另一部分是對zap的增強。
由於zap是一個log庫,所以從兩方面來深入閱讀zap的原始碼,一個是初始化logger的流程,一個是打一條log的流程。
zap的Logger是一般通過一個Config結構體初始化的,首先看下這個結構體有哪些欄位
type Config struct { // 紀錄檔Level,因為可以動態更改,所以是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里加上呼叫棧,dev模式下只有WarnLevel模式以上有呼叫棧,prod模式下只有ErrorLevel以上有呼叫棧 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格式具體設定,詳細看下面的EncoderConfig EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"` // log輸出路徑,看結構表示可以有多個輸出路徑 OutputPaths []string `json:"outputPaths" yaml:"outputPaths"` // 內部錯誤輸出路徑,預設是stderr ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"` // 每條log都會加上InitialFields裡的內容,顧名思義 InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"` }
// 取樣策略設定,大致的邏輯是每秒超過Thereafter個相同msg的log會執行自定義的Hook函數(第二個引數為一個標誌,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的時候已經寫死了是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 { // 根據Message獲取counter,也就是這個Message打過幾次紀錄檔了 counter := s.counts.get(ent.Level, ent.Message) // 打一條Message就會記錄一次到counters裡,不過每秒會重置一次counter,具體看IncCheckReset裡的邏輯 n := counter.IncCheckReset(ent.Time, s.tick) // first表示最初的first條紀錄檔呼叫hook時第二個引數傳LogSampled,超過first的紀錄檔,每threrafter條紀錄檔第二個引數傳LogSampled,否則傳LogDropped // 假設first是100,thereafter是50,表示同一個Message的log,最初的100條全都會記錄,之後的log在每秒鐘內,每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) } // 這裡可能會出現意想不到的情況 // 因為_countersPerLevel寫死了是4096,那麼必然會存在不同的key做完hash後取模會路由到相同的counter裡 // 那麼就會有概率丟棄掉沒有達到丟棄閾值的log // 假設abc和def的hash值一樣,first是0,thereafter是10,表示每秒鐘每種log每10條才會記錄1次,那麼abc和def這兩種log就會共用同一個counter,這就是問題所在 func (cs *counters) get(lvl Level, key string) *counter { i := lvl - _minLevel // fnv32a是一個hash函數 // _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格式的詳細設定 type EncoderConfig struct { // 設定log內容裡的一些屬性的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,可以自定義,預設為jsonEncoder NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"` // console格式的分隔符,預設是tab ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"` }
Config裡的大部分欄位都有tag,可以通過UnmarshalJson或者UnmarshalYaml來設定,可以在全域性的config檔案來設定,非常方便。
通過以上的config就可以初始化一個logger,下面貼程式碼
// 通過Config結構體Build出一個Logger func (cfg Config) Build(opts ...Option) (*Logger, error) { // 核心函數buildEncoder enc, err := cfg.buildEncoder() if err != nil { return nil, err } // 核心函數openSinks sink, errSink, err := cfg.openSinks() if err != nil { return nil, err } if cfg.Level == (AtomicLevel{}) { return nil, fmt.Errorf("missing Level") } // 核心函數New log := New( // 核心函數NewCore zapcore.NewCore(enc, sink, cfg.Level), cfg.buildOptions(errSink)..., ) if len(opts) > 0 { log = log.WithOptions(opts...) } return log, nil }
// 核心函數buildEncoder func (cfg Config) buildEncoder() (zapcore.Encoder, error) { return newEncoder(cfg.Encoding, cfg.EncoderConfig) } // _encoderNameToConstructor是一個map[string]constructor,plugin式寫法,可以通過RegisterEncoder函數註冊自定義的Encoder,預設只有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, // 這個buf是高效能的關鍵之一,使用了簡化的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裡拿一個Buffer,初始化裡面的[]byte func (p Pool) Get() *Buffer { buf := p.p.Get().(*Buffer) buf.Reset() // 這裡賦值pool為當前Pool,用於使用完Buffer後把Buffer後放回pool裡,也就是下面的put函數 buf.pool = p return buf } func (p Pool) put(buf *Buffer) { p.p.Put(buf) }
// 核心函數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 { // 核心函數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,比如自定義一個支援http協定的sink,在文章的尾部會實現一個自定義的sink _sinkFactories = map[string]func(*url.URL) (Sink, error){ schemeFile: newFileSink, } func newSink(rawURL string) (Sink, error) { // 通過rawURL判斷初始化哪種sink,實際上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這種的字串,那麼經過Parse後的u.Scheme就是"",然後會被賦值schemeFile // 如果url是類似於http://127.0.0.1:1234這種的字串,那麼經過Parse後的u.Scheme就是"http",不過zap本身不支援http,可以自定義一個支援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實際上就是一個*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) }
// 核心函數NewCore func NewCore(enc Encoder, ws WriteSyncer, enab LevelEnabler) Core { return &ioCore{ LevelEnabler: enab, enc: enc, out: ws, } } // 核心函數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這裡,就完成了一個logger的初始化,核心的結構體就是Encoder、Sink和ioCore,邏輯還是比較簡單易懂的
下面寫一段簡單的demo
l, _ := zap.NewProduction() l.Error("Message Content", zap.String("tagA", "tagAValue"))
func (log *Logger) Error(msg string, fields ...Field) { // 核心函數 check if ce := log.check(ErrorLevel, msg); ce != nil { // 核心函數 Write ce.Write(fields...) } }
// 核心函數check,實際邏輯就是檢查了下Level要不要打log,順便新增了呼叫棧和caller func (log *Logger) check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry { // 跳過當前這個check函數以及呼叫check的Error/Info/Fatal等函數 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, } // 核心函數 ioCore.Check ce := log.core.Check(ent, nil) willWrite := ce != nil // ... if !willWrite { return ce } // 新增stacktrace和caller相關 // ... return ce }
// 實際就是把core新增到了CheckedEntry裡,在後續的CheckedEntry.Write裡會被呼叫 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 }
// 核心函數 Write func (ce *CheckedEntry) Write(fields ...Field) { // ... var err error // 實際就是呼叫了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: %vn", 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,而是使用了拼接字串的方式手動拼出了一個json字串,這種方式的效能比marshalJson的效能要好很多 // 裡面的具體邏輯很簡單,就是append一個key,append一個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了,實際呼叫的就是Sink.Write,如果log是寫到檔案裡的,那麼呼叫的就是File.Write,至此一條紀錄檔記錄完成
zap記錄一條紀錄檔的流程可以概括為3步
zap在效能優化方面有一些值得借鑑的地方
不過zap的log抑制,也就是sampler實現有些過於簡單,可能會出現log丟失的問題,下面的程式碼可以完美復現這個問題
lc := zap.NewProductionConfig() lc.Encoding = "console" lc.Sampling.Initial = 1 // 當Initial為1時,第二條紀錄檔不會列印出來,改為大於1時第二條紀錄檔才會列印出來 lc.Sampling.Thereafter = 10 l, _ := lc.Build() l.Info("abc") l.Info("yTI") l.Info("def")
在閱讀原始碼部分已經提到了zap只支援log寫到檔案裡,一般業務紀錄檔都會統一收集到紀錄檔中心,那麼就需要自定義一個sink,通過網路傳送到某個地方統一收集起來,下面寫一個簡單的http協定的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,沒什麼別的邏輯,直接當成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 } // 執行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 } // 可以搞個內建的queue或者[]log,在Sync函數裡用來做批次傳送提升效能,這裡只是簡單的實現,所以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傳送出去。
來點騷操作,在原始碼閱讀部分,可以看到zap是把url.Parse後的scheme當作sink名稱的,例如在Config裡的outputPaths裡新增一條"wtf://xxx:xxx",zap就會去尋找名稱為wtf的sink,我們把上面的http sink的zap.RegisterSink("http", httpSink)改為zap.RegisterSink("wtf", httpSink),然後在Write函數的邏輯裡把"wtf://"後面的內容拼成一個完整的http url,同樣可以執行,操作是不是很騷。
當使用zap打Error紀錄檔時,如果設定了addStack,那麼zap會自動把呼叫棧寫到log裡,下面是一個例子
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內容,當使用jsonEncoder時,呼叫棧會在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紀錄檔了還可以看到呼叫棧,但是我們一般打log時,總是會在最上層打log,而不是每一層都打log,拿上面的程式碼舉例子
func test_a() error { return test_b() } func test_b() error { return test_c() } func test_c() error { // 底層的函數出現error應該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內容
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
這就出現了幾個問題,1. 呼叫棧只到了main,沒有更底層的, 2. 如果test_b接受到test_c的error時,想加上一些自己的error content返回出去,這兩個問題就體現出了golang在錯誤處理方面的不足,不過有一個庫可以解決這兩個問題,github.com/pkg/errors ,這個庫自定義了error,可以在error裡新增呼叫棧或額外的資訊,下面寫個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)) } }
下面是輸出內容,可以看到在errorVerbose欄位裡每一個函數的error都返回了出來,並帶上了呼叫棧,不過error欄位有點亂七八糟,並且還顯示了zap自帶的呼叫棧
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 failnmain.test_cntD:/workspace/code/go/gtil/main.go:27nmain.test_bntD:/workspace/code/go/gtil/main.go:19nmain.test_antD:/workspace/code/go/gtil/main.go:11nmain.mainntD:/workspace/code/go/gtil/main.go:32nruntime.mainntD:/workspace/env/scoop/apps/go/current/src/runtime/proc.go:250nruntime.goexitntD:/workspace/env/scoop/apps/go/current/src/runtime/asm_amd64.s:1571ndo test_b failnmain.test_bntD:/workspace/code/go/gtil/main.go:21nmain.test_antD:/workspace/code/go/gtil/main.go:11nmain.mainntD:/workspace/code/go/gtil/main.go:32nruntime.mainntD:/workspace/env/scoop/apps/go/current/src/runtime/proc.go:250nruntime.goexitntD:/workspace/env/scoop/apps/go/current/src/runtime/asm_amd64.s:1571ndo test_a failnmain.test_antD:/workspace/code/go/gtil/main.go:13nmain.mainntD:/workspace/code/go/gtil/main.go:32nruntime.mainntD:/workspace/env/scoop/apps/go/current/src/runtime/proc.go:250nruntime.goexitntD:/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
要做的就是去掉自帶的呼叫棧,把error欄位搞地好看點,只需要自定義一個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) } // 這裡是重點,從fields裡取出error欄位,把內容放到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) }
以上就是Go紀錄檔框架zap增強及原始碼解讀的詳細內容,更多關於Go紀錄檔框架zap增強的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45