How to initialize a zap logger once and reuse it in other Go files?
Asked Answered
S

4

30

I'm trying to migrate my application from the beautiful Logrus (very helpful for debug) and introducing the Uber log framework Zap.

With Logrus, I can initialize the logger only once and reuse it from other Go file, an example:

package main
import(
    // Print filename on log
    filename "github.com/onrik/logrus/filename"
    // Very nice log library
    log "github.com/sirupsen/logrus"
)

func main(){

// ==== SET LOGGING
    Formatter := new(log.TextFormatter)
    Formatter.TimestampFormat = "Jan _2 15:04:05.000000000"
    Formatter.FullTimestamp = true
    Formatter.ForceColors = true
    log.AddHook(filename.NewHook()) // Print filename + line at every log
    log.SetFormatter(Formatter)

}

From other Go file, I'm able to reuse that logger without any other initialization:

// VerifyCommandLineInput is delegated to manage the inputer parameter provide with the input flag from command line
func VerifyCommandLineInput() datastructures.Configuration {
    log.Debug("VerifyCommandLineInput | Init a new configuration from the conf file")
    c := flag.String("config", "./conf/test.json", "Specify the configuration file.")
    flag.Parse()
    if strings.Compare(*c, "") == 0 {
        log.Fatal("VerifyCommandLineInput | Call the tool using --config conf/config.json")
    }
    file, err := os.Open(*c)
    if err != nil {
        log.Fatal("VerifyCommandLineInput | can't open config file: ", err)
    }
    defer file.Close()
    decoder := json.NewDecoder(file)
    cfg := datastructures.Configuration{}
    err = decoder.Decode(&cfg)
    if err != nil {
        log.Fatal("VerifyCommandLineInput | can't decode config JSON: ", err)
    }
    log.Debug("VerifyCommandLineInput | Conf loaded -> ", cfg)

    return cfg
}

My question is: using the Zap log framework, how can I initialize the log in the main function and use that logger from the other Go file?

Sandstrom answered 1/9, 2019 at 10:3 Comment(1)
One library author recommends dependency injection by passing *zap.Logger as part of constructors. github.com/uber-go/zap/issues/924#issuecomment-789461851Alveolus
R
13

You can set up your logger in the main function and call zap.ReplaceGlobals to use it as a default global logger.

ReplaceGlobals replaces the global Logger and SugaredLogger, and returns a function to restore the original values. It's safe for concurrent use.

Rapid answered 1/9, 2019 at 10:16 Comment(2)
The FAQ on Github specifically says that you should avoid using globals when possible.Dullard
I think it is fair that the topic is more complicated and the note mentioned above isn't a definitive reason not to do it. Truth is the incorporation of zap with frameworks like cobra and passing everything around isn't practical.Pekan
S
16

Replacing the default go Global logger with zaps' implementation is possible, but discouraged.

Per their FAQ

Why include package-global loggers? Since so many other logging packages include a global logger, many applications aren't designed to accept loggers as explicit parameters. Changing function signatures is often a breaking change, so zap includes global loggers to simplify migration.

Avoid them where possible.

Depending on your needs, you can create a logger in main and pass it around or create a new logger in each package. I elected to create one in main and pass it around since I'm using an Atomic logger, which allows me to change log levels while my app is running via an API call. With a long history of using DI and consolidating code in general it does feel like code smell, but apparently it's significantly more performant for how zap works to pass it around over a singleton or global.

Soothsay answered 2/1, 2020 at 19:30 Comment(3)
Couldn't agree more: "feel like code smell".Pekan
Another option if you want to avoid passing around "logger" is to pass around "context" object and store logger there. You can use ctxzap to make it easier. pkg.go.dev/github.com/grpc-ecosystem/go-grpc-middleware/logging/…Deaton
An interesting discussion, which I don't necessarily agree with, about global vs ctx vs DI: gogoapps.io/blog/…Affray
S
16

I suppose it's a good idea to avoid replacing the default go logger, as was mentioned by @ammills01. My approach is to create a separate package for zap logger’s configuration and initialization. Also it provides wrapper functions implementation which can be useful a lot in case of changing zap to another logging tool.

package logger
import (
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

var zapLog *zap.Logger

func init() {
    var err error
    config := zap.NewProductionConfig()
    enccoderConfig := zap.NewProductionEncoderConfig()
    zapcore.TimeEncoderOfLayout("Jan _2 15:04:05.000000000")
    enccoderConfig.StacktraceKey = "" // to hide stacktrace info
    config.EncoderConfig = enccoderConfig

    zapLog, err = config.Build(zap.AddCallerSkip(1))
    if err != nil {
        panic(err)
    }
}

func Info(message string, fields ...zap.Field) {
    zapLog.Info(message, fields...)
}

func Debug(message string, fields ...zap.Field) {
    zapLog.Debug(message, fields...)
}

func Error(message string, fields ...zap.Field) {
    zapLog.Error(message, fields...)
}

func Fatal(message string, fields ...zap.Field) {
    zapLog.Fatal(message, fields...)
}

So any other file in your project will be as follows

import "github.com/[your_repo_path_here]/logger"

func SomeFunc() datastructures.Configuration {
    logger.Debug("VerifyCommandLineInput | Init a new configuration from the conf file")
    c := flag.String("config", "./conf/test.json", "Specify the configuration file.")
    flag.Parse()if strings.Compare(*c, "") == 0 {
        logger.Fatal("VerifyCommandLineInput | Call the tool using --config conf/config.json")
    }
    file, err := os.Open(*c)
    if err != nil {
        logger.Fatal("VerifyCommandLineInput | can't open config file: ", err)
    }
    defer file.Close()
    decoder := json.NewDecoder(file)
    cfg := datastructures.Configuration{}
    err = decoder.Decode(&cfg)
    if err != nil {
        logger.Fatal("VerifyCommandLineInput | can't decode config JSON: ", err)
    }
    logger.Debug("VerifyCommandLineInput | Conf loaded -> ", cfg)

    return cfg
}
Safko answered 3/2, 2022 at 15:48 Comment(3)
creating the functions info, debug,.. as in the example will cause that caller and function names will be reported incorrectlyStith
@Stith I’m not sure what you mean by “reported incorrectly“, so I apologize if I interpreted something improperly. If you are talking about reporting the wrapper code, please draw your attention to the zap.AddCallerSkip(1) option which helps to avoid it.Safko
zap.AddCallerSkip(1) really helped. I was using the same approach with wrapping zap logger into my own module and it was reporting the wrong code wrapper as the caller. AddCallerSkip solves the exact same issue. Docs: pkg.go.dev/go.uber.org/zap#AddCallerSkipLicense
R
13

You can set up your logger in the main function and call zap.ReplaceGlobals to use it as a default global logger.

ReplaceGlobals replaces the global Logger and SugaredLogger, and returns a function to restore the original values. It's safe for concurrent use.

Rapid answered 1/9, 2019 at 10:16 Comment(2)
The FAQ on Github specifically says that you should avoid using globals when possible.Dullard
I think it is fair that the topic is more complicated and the note mentioned above isn't a definitive reason not to do it. Truth is the incorporation of zap with frameworks like cobra and passing everything around isn't practical.Pekan
B
2

Answer by @alessiosavi originally posted into the question.


Initialize a new log and set as global as pointed by @Mikhail.

package main

import(
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

func initZapLog() *zap.Logger {
    config := zap.NewDevelopmentConfig()
    config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
    config.EncoderConfig.TimeKey = "timestamp"
    config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
    logger, _ := config.Build()
    return logger
}

func main() {

    loggerMgr := initZapLog()
    zap.ReplaceGlobals(loggerMgr)
    defer loggerMgr.Sync() // flushes buffer, if any
    logger := loggerMgr.Sugar()
    logger.Debug("START!")
    db2.GetToken(`alessio`, `savi`, `pass`)
    datastructure.LoadConfiguration()
}

Than you can use the logger in the other Go file:

func GetToken(url, user, pass string) string {
    var User datastructure.User
    var data string
    var jsonData []byte

    User.User = user
    User.Pass = pass
    jsonData, err := json.Marshal(User)
    if err != nil {
        zap.S().Errorw("Error during marshalling...", err)
        return ""
    }
    data = string(jsonData)
    zap.S().Info("Data encoded => ", data)
    return ""
}
Buster answered 1/9, 2019 at 10:3 Comment(1)
I believe the preferred way is to use dependency injection, so passing the logger as an argument in to GetToken. I wish the docs were more clear on this though.Dullard

© 2022 - 2025 — McMap. All rights reserved.