gologginggo-zap

How to log to stdout or stderr based on log level using uber-go/zap?


I am trying to set up logging using this package github.com/uber-go/zap. I need to write:

I tried to do this by setting up and building zap.Config like this:

    cfg = &zap.Config{
        Encoding:         "json",
        Level:            zap.NewAtomicLevelAt(zapcore.DebugLevel),
        OutputPaths:      []string{"stdout"},
        ErrorOutputPaths: []string{"stderr"},
        EncoderConfig: zapcore.EncoderConfig{
            MessageKey: "message",

            LevelKey:    "level",
            EncodeLevel: zapcore.CapitalLevelEncoder,

            TimeKey:    "time",
            EncodeTime: zapcore.ISO8601TimeEncoder,

            CallerKey:    "caller",
            EncodeCaller: zapcore.ShortCallerEncoder,

            EncodeDuration: zapcore.MillisDurationEncoder,
        },
    }

Also I tried this way:

cfg = zap.NewProductionConfig()
    cfg.OutputPaths = []string{"stdout"}
    logger, err = cfg.Build(zap.AddCaller(), zap.AddCallerSkip(1))

But all logs are written to either stdout or stderr. How can I separate it?


Solution

  • Use zapcore.NewTee with two cores:

    This is a minimal program to demonstrate the usage:

    package main
    
    import (
        "go.uber.org/zap"
        "go.uber.org/zap/zapcore"
        "os"
    )
    
    func main() {
        // info level enabler
        infoLevel := zap.LevelEnablerFunc(func(level zapcore.Level) bool {
            return level == zapcore.InfoLevel
        })
    
        // error and fatal level enabler
        errorFatalLevel := zap.LevelEnablerFunc(func(level zapcore.Level) bool {
            return level == zapcore.ErrorLevel || level == zapcore.FatalLevel
        })
    
        // write syncers
        stdoutSyncer := zapcore.Lock(os.Stdout)
        stderrSyncer := zapcore.Lock(os.Stderr)
    
        // tee core
        core := zapcore.NewTee(
            zapcore.NewCore(
                zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
                stdoutSyncer,
                infoLevel,
            ),
            zapcore.NewCore(
                zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
                stderrSyncer,
                errorFatalLevel,
            ),
        )
    
        // finally construct the logger with the tee core
        logger := zap.New(core)
    
        logger.Info("info log")
        logger.Error("error log")
    }
    

    Note that the program above logs two messages, one at info level and one at error level. To see that it prints both to the correct device, run it and redirect stdout or stderr to /dev/null — thus printing to the console only the other one:

    $ go build main.go
    
    $ ./main 2>/dev/null # shows only stdout
    {"level":"info","ts":1626900981.520349,"msg":"info log"}
    
    $ ./main 1>/dev/null # shows only stderr
    {"level":"error","ts":1626901025.056065,"msg":"error log"}