Search code examples
gologginggo-zap

How to access Fields in zap Hooks?


How can I access the full information about the logging event in uber-zap's hooks?

For example, I am trying to add a zapcore.Field to the logging event, but it does not show up in the zapcore.Entry.

If it is not possible, can I at least have the fully-formatted string somehow? The goal is to send an email/automated message/Sentry/etc in case of errors.

package main

import (
    "log"

    "github.com/davecgh/go-spew/spew"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

func main() {
    prodLogger, err := zap.NewProduction(zap.Hooks(func(entry zapcore.Entry) error {
        if entry.Level == zapcore.ErrorLevel {
            spew.Dump(entry) // fancy console printer
        }

        return nil
    }))

    if err != nil {
        log.Fatal(err)
    }

    prodLogger.
        Named("logger_name").
        Error("something happened", zap.String("foo", "bar"))
}

Output - no traces of foo or bar:

{"level":"error","ts":1640722252.899601,"logger":"logger_name","caller":"awesomep2/main.go:23","msg":"something happened","foo":"bar","stacktrace":"main.main\n\t/Users/xxx/GitHub/awesomep2/main.go:23\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:255"}
(zapcore.Entry) {
 Level: (zapcore.Level) error,
 Time: (time.Time) 2021-12-28 13:10:52.899601 -0700 MST m=+0.000629089,
 LoggerName: (string) (len=11) "logger_name",
 Message: (string) (len=18) "something happened",
 Caller: (zapcore.EntryCaller) /Users/xxx/GitHub/awesomep2/main.go:23,
 Stack: (string) (len=103) "main.main\n\t/Users/xxx/GitHub/awesomep2/main.go:23\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:255"
}

Solution

  • Fields are not available in Zap hooks. The docs of zap.Hooks say this explicitly:

    [...] Hooks are useful for simple side effects, like capturing metrics for the number of emitted logs. More complex side effects, including anything that requires access to the Entry's structured fields, should be implemented as a zapcore.Core instead. [...]

    So to dump logs with go-spew, you need a custom core. You have two main options.

    Custom core with custom encoder

    This has the advantage of allowing more customization.

    The entry's fields are available in zapcore.Encoder.EncodeEntry. The strategy is, as usual, to embed a zapcore.Encoder into your struct and reimplement EncodeEntry:

    type spewDumpEncoder struct {
        zapcore.Encoder
    }
    
    func (e *spewDumpEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
        if entry.Level == zapcore.ErrorLevel {
            spew.Dump(entry, fields)
        }
        return e.Encoder.EncodeEntry(entry, fields)
    }
    

    Remember to implement Clone() as well, if you plan to use structured logging.

    Custom core with Write

    This has the advantage of allowing simpler initialization.

    Similarly to the first option, zapcore.Core is also an interface, so you can implement it by embedding in your struct, and reimplement only Write:

    type MyCore struct {
        zapcore.Core
    }
    
    func (c *MyCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry {
        if c.Enabled(entry.Level) {
            return checked.AddCore(entry, c)
        }
        return checked
    }
    
    func (c *MyCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
        if entry.Level == zapcore.ErrorLevel {
            spew.Dump(entry, fields)
        }
        return c.Core.Write(entry, fields)
    }
    

    and instantiate it by taking the existing core from a default zap logger:

        l, _ := zap.NewProduction()
        logger := zap.New(&MyCore{Core: l.Core()})