Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

logging

go

go-zap

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?

like image 464
alessiosavi Avatar asked Sep 01 '19 10:09

alessiosavi


4 Answers

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.

like image 200
Mikhail Avatar answered Dec 20 '22 19:12

Mikhail


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.

like image 45
ammills01 Avatar answered Dec 20 '22 19:12

ammills01


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
}
like image 35
Gleb Avatar answered Dec 20 '22 19:12

Gleb


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 ""
}
like image 20
anon Avatar answered Dec 20 '22 19:12

anon