Logger implementation: circular dep between conf pkg and custom log pkg

Hi guys,

It’s the first time I use a logger. After a few searches, I ended up with zerolog.
I’ve created a simple web server that call a dummy api endpoint to illustrate my concern.
GitHub Repo of the example: https://github.com/BigBoulard/logger


var (
	router *gin.Engine
)

func main() {
	// load the env variables
	conf.LoadEnv()

	// GinMode is set to "debug" in the /.env file
	gin.SetMode(conf.Env.GinMode)
	router = gin.Default()

	// creates a controller that uses an httpclient to call https://jsonplaceholder.typicode.com/todos
	controller := controller.NewController(
		httpclient.NewClient(),
	)
	router.GET("/todos", controller.GetTodos)

	// run the router on host and port specified in the /.env file
	err := router.Run(fmt.Sprintf("%s:%s", conf.Env.Host, conf.Env.Port))
	if err != nil {
		log.Fatal("application/main", err)
	}
}

As you already noticed, This server uses environment variables that need to be loaded differently if I’m working locally or if the server is in a production environment, right? So to manage this, I’ve created a conf package which looks like this.

package conf

import (
	"log"
	"os"

	"github.com/joho/godotenv"
)

var Env = &env{}

type env struct {
	AppMode string
	GinMode string
	Host    string
	Port    string
}

var IsLoaded = false

func LoadEnv() {
	// the httpclient and the Gin server both need the env vars
	// but we don't want to load them twice
	if IsLoaded {
		return
	}

	// if not "prod"
	if Env.AppMode != "prod" {
		curDir, err := os.Getwd()
		if err != nil {
			log.Fatal(err, "conf", "LoadEnv", "error loading os.Getwd()")
		}
		// load the /.env file
		loadErr := godotenv.Load(curDir + "/.env")
		if loadErr != nil {
			log.Fatal(loadErr, "conf", "LoadEnv", "can't load env file from current directory: "+curDir)
		}
		Env.GinMode = "debug"
	} else {
		Env.GinMode = "release"
	}

	// load the env vars
	Env.AppMode = os.Getenv("APP_MODE")
	Env.Host = os.Getenv("HOST")
	Env.Port = os.Getenv("PORT")

	IsLoaded = true
}

To make a long story short, I’ve created a log package that I would include in each microservice.
This log package uses an environment variable at startup to determine the log level and decide whether to activate certain features. The log package looks like this:

package log

var l *logger = NewLogger()
const API = "test api"

type logger struct {
	logger zerolog.Logger // see https://github.com/rs/zerolog#leveled-logging
}

func NewLogger() *logger {
    loadEnvFile()
    var zlog zerolog.Logger
	if os.Getenv("APP_ENV") == "dev" {
           // setup for dev ...
        } else {
          // setup for prod ...
       }
return &logger{
  logger: zlog,
}

func Error(path string, err error) {
	l.logger.
		Error().
		Stack().
		Str("path", path).
		Msg(err.Error())
}

func Fatal(path string, err error) {
	l.logger.
		Fatal().
		Stack().
		Str("path", path).
		Msg(err.Error())
}

// PROBLEM: I need to duplicate loadEnvFile() from conf.load_env.go
// because conf uses log ... but conversely, log need conf cause it needs the env var
func loadEnvFile() {
	curDir, err := os.Getwd()
	if err != nil {
		log.Fatal(err, "test api", "App", "gw - conf - LoadEnv - os.Getwd()")
	}
	loadErr := godotenv.Load(curDir + "/.env")
	if loadErr != nil {
		log.Fatal(err, "test api", "conf - LoadEnv", "godotenv.Load("+curDir+"/.env\")")
	}
}

As you can see, I can’t use loadEnvFile() from the conf package as it causes a circular dependency, so I guess that there’s probably a better way to implement this…

Thank you so much.