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

Solution 1:

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"}