Search code examples
gologginggo-zap

How can I duplicate entry keys and show it in the same log with Uber Zap?


I would like to duplicate entry keys like caller with another key name method and show both in the log...

{"level":"error", "caller: "testing/testing.go:1193", "method": "testing/testing.go:1193", "message": "foo"}

Any ideas?


Solution

  • You can't change the fields of a zapcore.Entry. You may change how it is marshalled, but honestly adding ghost fields to a struct is a bad hack. What you can do is use a custom encoder, and append to []zapcore.Field a new string item with a copy of the caller. In particular, the default output of the JSON encoder is obtained from Caller.TrimmedPath():

    type duplicateCallerEncoder struct {
        zapcore.Encoder
    }
    
    func (e *duplicateCallerEncoder) Clone() zapcore.Encoder {
        return &duplicateCallerEncoder{Encoder: e.Encoder.Clone()}
    }
    
    func (e *duplicateCallerEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
        // appending to the fields list
        fields = append(fields, zap.String("method", entry.Caller.TrimmedPath()))
        return e.Encoder.EncodeEntry(entry, fields)
    }
    

    Note that the above implements Encoder.Clone(). See this for details: Why custom encoding is lost after calling logger.With in Uber Zap?

    And then you can use it by either constructing a new Zap core, or by registering the custom encoder. The registered constructor embeds a JSONEncoder into your custom encoder, which is the default encoder for the production logger:

    func init() {
        // name is whatever you like
        err := zap.RegisterEncoder("duplicate-caller", func(config zapcore.EncoderConfig) (zapcore.Encoder, error) {
            return &duplicateCallerEncoder{Encoder: zapcore.NewJSONEncoder(config)}, nil
        })
        // it's reasonable to panic here, since the program can't initialize
        if err != nil {
            panic(err)
        }
    }
    
    func main() {
        cfg := zap.NewProductionConfig()
        cfg.Encoding = "duplicate-caller"
        logger, _ := cfg.Build()
        logger.Info("this is info")
    }
    

    The above replicates the initialization of a production logger with your custom config.

    For such a simple config, I prefer the init() approach with zap.RegisterEncoder. It makes it faster to refactor code, if needed, and/or if you place this in some other package to begin with. You can of course do the registration in main(); or if you need additional customization, then you may use zap.New(zapcore.NewCore(myCustomEncoder, /* other args */))

    You can see the full program in this playground: https://go.dev/play/p/YLDXbdZ-qZP

    It outputs:

    {"level":"info","ts":1257894000,"caller":"sandbox3965111040/prog.go:24","msg":"this is info","method":"sandbox3965111040/prog.go:24"}