0

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…

Side question: I can create a logger web service that would receive log messages from all the other services and just print them in a file but I'm not sure if it's a good practice. I the end, I would like to be able to push the log files into something like Kibana to get a better view of how all this performs but maybe it's better that each service print into the standard input that centralizing le logs into a dedicated service I don't know.

Thank you so much.

Big_Boulard
  • 799
  • 1
  • 13
  • 28
  • "This log package uses an environment variable at startup to determine the log level and [...]". So just use os.GetEnv and call it a day. – Volker Apr 03 '23 at 12:43
  • Agree but when working locally env vars must be loaded using something like `godotenv` (or `viper`), hence the call to `loadEnvFile()`. A workaround would be to launch the service using docker and just pass the env vars in the command line, but it's not practical in dev from my perspective. – Big_Boulard Apr 04 '23 at 07:11
  • 1
    "env vars must be loaded using something like godotenv" This is a funny misconception on what environment variables are, how the work and why one should use them. This .env-file stuff is pretty nonsensical as this constitutes basically a config file. – Volker Apr 04 '23 at 07:24
  • I got mixed up, you're right, we can still pass env var passing them to the go run command (like `host=localhost port=9090 go run src/main.go`). My understanding of the `.env` file is that it sets environment variables in dev without having to set them through the `go run` command line or in a shell config file like `.bashrc`. So yes, the `loadEnvFile` should only be called in dev. Let me know if I'm missing something else or whatever, I would appreciate it. Thank you. – Big_Boulard Apr 11 '23 at 07:11
  • One more: Please stop using go run with filename arguments. – Volker Apr 11 '23 at 07:48
  • I think I don't get it. What's the problem with that? – Big_Boulard Apr 11 '23 at 08:00
  • 1
    It's a loaded footgun, almost impossible to get right and mire complicated that just `go run .` – Volker Apr 11 '23 at 09:00

0 Answers0