Go日志框架zap增強及源碼解讀
正文
本文包括兩部分,一部分是源碼解讀,另一部分是對zap的增強。
由于zap是一個log庫,所以從兩方面來深入閱讀zap的源碼,一個是初始化logger的流程,一個是打一條log的流程。
初始化Logger
zap的Logger是一般通過一個Config結(jié)構(gòu)體初始化的,首先看下這個結(jié)構(gòu)體有哪些字段
type Config struct {
// 日志Level,因為可以動態(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格式具體配置,詳細看下面的EncoderConfig
EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
// log輸出路徑,看結(jié)構(gòu)表示可以有多個輸出路徑
OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
// 內(nèi)部錯誤輸出路徑,默認是stderr
ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
// 每條log都會加上InitialFields里的內(nèi)容,顧名思義
InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}
// 采樣策略配置,大致的邏輯是每秒超過Thereafter個相同msg的log會執(zhí)行自定義的Hook函數(shù)(第二個參數(shù)為一個標志,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的時候已經(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,也就是這個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ù)傳LogSampled,超過first的日志,每threrafter條日志第二個參數(shù)傳LogSampled,否則傳LogDropped
// 假設first是100,thereafter是50,表示同一個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)意想不到的情況
// 因為_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函數(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格式的詳細設置
type EncoderConfig struct {
// 設置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,可以自定義,默認為jsonEncoder
NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
// console格式的分隔符,默認是tab
ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}
Config里的大部分字段都有tag,可以通過UnmarshalJson或者UnmarshalYaml來配置,可以在全局的config文件來配置,非常方便。
通過以上的config就可以初始化一個logger,下面貼代碼
// 通過Config結(jié)構(gòu)體Build出一個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是一個map[string]constructor,plugin式寫法,可以通過RegisterEncoder函數(shù)注冊自定義的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函數(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,比如自定義一個支持http協(xié)議的sink,在文章的尾部會實現(xiàn)一個自定義的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這種的字符串,那么經(jīng)過Parse后的u.Scheme就是"",然后會被賦值schemeFile
// 如果url是類似于http://127.0.0.1:1234這種的字符串,那么經(jīng)過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)
}
// 核心函數(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這里,就完成了一個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,實際邏輯就是檢查了下Level要不要打log,順便添加了調(diào)用棧和caller
func (log *Logger) check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry {
// 跳過當前這個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相關
// ...
return ce
}
// 實際就是把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
// 實際就是調(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,而是使用了拼接字符串的方式手動拼出了一個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了,實際調(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
- 使用了自實現(xiàn)的jsonEncoder,簡化了encode邏輯
不過zap的log抑制,也就是sampler實現(xiàn)有些過于簡單,可能會出現(xiàn)log丟失的問題,下面的代碼可以完美復現(xiàn)這個問題
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
自定義sink
在閱讀源碼部分已經(jīng)提到了zap只支持log寫到文件里,一般業(yè)務日志都會統(tǒng)一收集到日志中心,那么就需要自定義一個sink,通過網(wǎng)絡發(fā)送到某個地方統(tǒng)一收集起來,下面寫一個簡單的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,沒什么別的邏輯,直接當成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
}
// 可以搞個內(nèi)置的queue或者[]log,在Sync函數(shù)里用來做批量發(fā)送提升性能,這里只是簡單的實現(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ā)送出去。
來點騷操作,在源碼閱讀部分,可以看到zap是把url.Parse后的scheme當作sink名稱的,例如在Config里的outputPaths里添加一條"wtf://xxx:xxx",zap就會去尋找名稱為wtf的sink,我們把上面的http sink的zap.RegisterSink("http", httpSink)改為zap.RegisterSink("wtf", httpSink),然后在Write函數(shù)的邏輯里把"wtf://"后面的內(nèi)容拼成一個完整的http url,同樣可以運行,操作是不是很騷。
error調(diào)用棧
當使用zap打Error日志時,如果配置了addStack,那么zap會自動把調(diào)用棧寫到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內(nèi)容,當使用jsonEncoder時,調(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時,總是會在最上層打log,而不是每一層都打log,拿上面的代碼舉例子
func test_a() error {
return test_b()
}
func test_b() error {
return test_c()
}
func test_c() error {
// 底層的函數(shù)出現(xiàn)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內(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)了幾個問題,1. 調(diào)用棧只到了main,沒有更底層的, 2. 如果test_b接受到test_c的error時,想加上一些自己的error content返回出去,這兩個問題就體現(xiàn)出了golang在錯誤處理方面的不足,不過有一個庫可以解決這兩個問題,github.com/pkg/errors ,這個庫自定義了error,可以在error里添加調(diào)用?;蝾~外的信息,下面寫個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字段里每一個函數(shù)的error都返回了出來,并帶上了調(diào)用棧,不過error字段有點亂七八糟,并且還顯示了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字段搞地好看點,只需要自定義一個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字段,把內(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)
}以上就是Go日志框架zap增強及源碼解讀的詳細內(nèi)容,更多關于Go日志框架zap增強的資料請關注腳本之家其它相關文章!
相關文章
Go 實現(xiàn)一次性打包各個平臺的可執(zhí)行程序
這篇文章主要介紹了Go 實現(xiàn)一次性打包各個平臺的可執(zhí)行程序,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12
Go開發(fā)go-optioner工具實現(xiàn)輕松生成函數(shù)選項模式代碼
go-optioner?是一個在?Go?代碼中生成函數(shù)選項模式代碼的工具,可以根據(jù)給定的結(jié)構(gòu)定義自動生成相應的選項代碼,下面就來聊聊go-optioner是如何使用的吧2023-07-07

