Using a logger that is initialized in main in other parts of codebase

Viewed 1458

I am struggling with how to use a zap. In the docs, they provide a few examples on how to configure a logger and how to use a preset. However, I do not understand how one is supposed to use the logger that is initialized in main.go, which lives in cmd/server/server.go. According to this SO post, and several others, one cannot import from the main package outside of the package. So, based on the Zap examples, how am I supposed to use the logger in say pkg/endpoint/my_requests (pkg is at same level as cmd)? I have not been able to find any explicit examples (even unrelated to zap) on how to accomplish something like this; yet, I am certain this is a very simple question.

2 Answers

I personally favor treating it as any other normal dependency and pass it where it is need it

package foo

type Bar struct {
    Logger logger.Logger
}

func (b *Bar) Something() {
    b.Logger.Debug("starting something")
}

func DoSomething(logger logger.Logger) {
    b := Bar{Logger: logger}
    b.Something()
}

Anything that involves an init function is basically a global variable

I also tend to use an abstraction over any logger that I use, and give the zero value a no-op behavior (doesn't log anything), especially helpful while testing, the downside it's a bit slower since all method are not a pointer receiver and require a copy and that I have to define the same methods (in fact I abstract the sugared version so I don't import zap on my packages)

package logger

import "go.uber.org/zap"

type Logger struct {
    zap *zap.Logger
}

func Must(logger *Logger, err error) *Logger {
    if err != nil {
        panic(err)
    }
    return logger
}

func NewLogger(logFile string) (*Logger, error) {
    zap.NewProductionConfig()
    config := zap.NewProductionConfig()
    config.OutputPaths = []string{"stdout", "./logs/" + logFile}
    logger, err := config.Build(zap.AddCaller())
    if err != nil {
        return nil, err
    }   
    return &Logger{zap: logger}, err
}

func (l Logger) Debug(msg string, fields ...zap.Field) {
    l.writer().Debug(msg, fields...)
}

func (l Logger) Info(msg string, fields ...zap.Field) {
    l.writer().Info(msg, fields...)
}
// define all the methdos

var noOpLogger = zap.NewNop()

func (l Logger) writer() *zap.Logger {
    if l.zap == nil {
        return noOpLogger
    }
    return l.zap
}

The zero value no-op logger is safe for concurrent use and the logger doesn't get in the middle anymore during testing

var b Bar
b.Something() // no panics

A good idea would be to create a package responsible for the logger or just a package like "config" or "settings" to handle the global configurations like logger.

I do use zap in my projects and I usually have a package named logger which provides a base method called NewLogger which I call from my other packages to create a package-specific logger when my packages generate a lot of logs and they are pretty big. In rather smaller projects, I just initialize the logger in the logger package and call it from outside.

// /my-project/pkg/logging/logging.go
func NewLogger(logFile string) *zap.Logger {
    zap.NewProductionConfig()
    config := zap.NewProductionConfig()
    config.OutputPaths = []string{"stdout", "./logs/" + logFile}
    logger, err := config.Build(zap.AddCaller())
    if err != nil {
        panic(err)
    }
    return logger
}

And then use it in other packages:

// /my-project/pkg/a/a.go

package a

var logger *zap.Logger

func init() {
    logger = logging.NewLogger("a.log")
}

func MyFunction() {
    logger.Info("log from package a to a.log")
}

another package:

// /my-project/pkg/b/b.go

package b

var logger *zap.Logger

func init() {
    logger = logging.NewLogger("b.log")
}

func MyFunction() {
    logger.Info("log from package b to b.log")
}

You can also initialize the logger directly in your packages but having your logger in a separate package helps you change the configuration or the entire logger whenever you want without the need to make changes everywhere.

You can also add more methods and helpers to your logger package to unify your logs or just make your life easier for logging stuff.

Related