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

Time:12-28

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

CodePudding user response:

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. 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
  • Related