gosystemdsystemd-journaldgo-zap

Uber Zap Logger: how to prepend every log entry with a string


I am using my app as a SystemD service and need to prepend every message with an entry level <LEVEL> for JournalD like:

<6> this is info
<7> this is debug
<4> this is warning

Otherwise, JournalD treats all the entries the same level and I want to use its advanced capabilities for displaying logs only of certain level.

How can I prepend every log entry with the correct level label (like for Info it would be <6>) with uber-zap library?

EDIT: This is the relevant part of my logger configuration:

    var config zap.Config

    if production {
        config = zap.NewProductionConfig()
        config.Encoding = `console`
        config.EncoderConfig.TimeKey = "" // no time as SystemD adds timestamp
    } else {
        config = zap.NewDevelopmentConfig()
    }

    config.DisableStacktrace = true
    config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder // colors
    config.OutputPaths = []string{"stdout"}

Solution

  • You can use a custom encoder that embeds a zapcore.Encoder.

    Embedding the encoder gives you the implementation of all methods "for free" with the same configuration you have now. Then you can implement only EncodeEntry with the additional logic you require.

    NOTE: You still have to implement Clone() if you plan to use structured logging, e.g. logger.With(). More info: Why custom encoding is lost after calling logger.With in Uber Zap?

    Back to your main question, this is a working example; see the comments in code for additional explanation:

    type prependEncoder struct {
        // embed a zapcore encoder
        // this makes prependEncoder implement the interface without extra work
        zapcore.Encoder
    
        // zap buffer pool
        pool buffer.Pool
    }
    
    // implementing only EncodeEntry
    func (e *prependEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
        // new log buffer
        buf := e.pool.Get()
    
        // prepend the JournalD prefix based on the entry level
        buf.AppendString(e.toJournaldPrefix(entry.Level))
        buf.AppendString(" ")
    
        // calling the embedded encoder's EncodeEntry to keep the original encoding format 
        consolebuf, err := e.Encoder.EncodeEntry(entry, fields)
        if err != nil {
            return nil, err
        }
    
        // just write the output into your own buffer
        _, err = buf.Write(consolebuf.Bytes())
        if err != nil {
            return nil, err
        }
        return buf, nil
    }
    
    // some mapper function
    func (e *prependEncoder) toJournaldPrefix(lvl zapcore.Level) string {
        switch lvl {
        case zapcore.DebugLevel:
            return "<7>"
        case zapcore.InfoLevel:
            return "<6>"
        case zapcore.WarnLevel:
            return "<4>"
        }
        return ""
    }
    

    Later you construct a logger with a custom core that uses the custom encoder. You initialize the embedded field with the same encoder you are using now. The options you see below mimic the options you currently have.

    package main
    
    import (
        "go.uber.org/zap"
        "go.uber.org/zap/buffer"
        "go.uber.org/zap/zapcore"
        "os"
    )
    
    func getConfig() zap.Config {
        // your current config options
        return config
    }
    
    func main() {
        cfg := getConfig()
    
        // constructing our prependEncoder with a ConsoleEncoder using your original configs
        enc := &prependEncoder{
            Encoder: zapcore.NewConsoleEncoder(cfg.EncoderConfig),
            pool:    buffer.NewPool(),
        }
    
        logger := zap.New(
            zapcore.NewCore(
                enc,
                os.Stdout,
                zapcore.DebugLevel,
            ),
            // this mimics the behavior of NewProductionConfig.Build
            zap.ErrorOutput(os.Stderr), 
        )
    
        logger.Info("this is info")
        logger.Debug("this is debug")
        logger.Warn("this is warn")
    }
    

    Test run output (INFO is printed in blue, DEBUG in pink and WARN in yellow as per your zapcore.CapitalColorLevelEncoder):

    <6> INFO        this is info
    <7> DEBUG       this is debug
    <4> WARN        this is warn