DEV Community

Matija Krajnik
Matija Krajnik

Posted on • Edited on • Originally published at letscode.blog

3 2

Logging

#go

Logging is also pretty important part of most web applications because we usually want to know what requests are coming in, and what's more important, whether there are any unexpected errors. So, as you may have guessed, this section will introduce logging and i will show you how to setup logging, and how to separate logging in dev and prod environments. Now we will use -env flag added in previous section.

For logging we will use zerolog module, which you can get by running go get github.com/rs/zerolog/log.

Now let's create another directory internal/logging/ and file logging.go inside of it:

package logging

import (
  "fmt"
  "io"
  "io/ioutil"
  "os"
  "path/filepath"
  "strings"
  "time"

  "github.com/gin-gonic/gin"
  "github.com/rs/zerolog"
  "github.com/rs/zerolog/log"
)

const (
  logsDir = "logs"
  logName = "gin_production.log"
)

var logFilePath = filepath.Join(logsDir, logName)

func SetGinLogToFile() {
  gin.SetMode(gin.ReleaseMode)
  logFile, err := os.Create(logFilePath)
  if err != nil {
    log.Panic().Err(err).Msg("Error opening Gin log file")
  }
  gin.DefaultWriter = io.MultiWriter(logFile)
}

func ConfigureLogger(env string) {
  zerolog.SetGlobalLevel(zerolog.DebugLevel)
  switch env {
  case "dev":
    stdOutWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "15:04:05.000"}
    logger := zerolog.New(stdOutWriter).With().Timestamp().Logger()
    log.Logger = logger
  case "prod":
    createLogDir()
    backupLastLog()
    logFile := openLogFile()
    logFileWriter := zerolog.ConsoleWriter{Out: logFile, NoColor: true, TimeFormat: "15:04:05.000"}
    logger := zerolog.New(logFileWriter).With().Timestamp().Logger()
    log.Logger = logger
  default:
    fmt.Printf("Env not valid: %s\n", env)
    os.Exit(2)
  }
}

func createLogDir() {
  if err := os.Mkdir(logsDir, 0744); err != nil && !os.IsExist(err) {
    log.Fatal().Err(err).Msg("Unable to create logs directory.")
  }
}

func backupLastLog() {
  timeStamp := time.Now().Format("20060201_15_04_05")
  base := strings.TrimSuffix(logName, filepath.Ext(logName))
  bkpLogName := base + "_" + timeStamp + "." + filepath.Ext(logName)
  bkpLogPath := filepath.Join(logsDir, bkpLogName)

  logFile, err := ioutil.ReadFile(logFilePath)
  if err != nil {
    if os.IsNotExist(err) {
      return
    }
    log.Panic().Err(err).Msg("Error reading log file for backup")
  }

  if err = ioutil.WriteFile(bkpLogPath, logFile, 0644); err != nil {
    log.Panic().Err(err).Msg("Error writing backup log file")
  }
}

func openLogFile() *os.File {
  logFile, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE, 0644)
  if err != nil {
    log.Panic().Err(err).Msg("Error while opening log file")
  }
  return logFile
}

func curentDir() string {
  path, err := os.Executable()
  if err != nil {
    log.Panic().Err(err).Msg("Can't get current directory.")
  }
  return filepath.Dir(path)
}
Enter fullscreen mode Exit fullscreen mode

And then we can update internal/cli/cli.go to configure logging based on environment instead of just printing it:

package cli

import (
  "flag"
  "fmt"
  "os"

  "rgb/internal/logging"
)

func usage() {
  fmt.Print(`This program runs RGB backend server.

Usage:

rgb [arguments]

Supported arguments:

`)
  flag.PrintDefaults()
  os.Exit(1)
}

func Parse() {
  flag.Usage = usage
  env := flag.String("env", "dev", `Sets run environment. Possible values are "dev" and "prod"`)
  flag.Parse()
  logging.ConfigureLogger(*env)
  if *env == "prod" {
    logging.SetGinLogToFile()
  }
}
Enter fullscreen mode Exit fullscreen mode

This seems like a lot of code, but it's quite simple. First we configure our logger based on environment. If env is dev we will log everything to stdout, while for prod environment we will log into file. When logging into file we will first create logs directory if needed and backup previous log, so we have fresh log for every server start. Of course, you can create different logic for log rotation which better suits your needs. Another thing we need to do in this case is to tell Gin to run in release mode, which will reduce unnecessary and noisy output, and then also set default Gin writer to write into log file. You can also do that inside of a prod case block, but since we actually have 2 different loggers (Gin's attached logger and our zerolog logger) i like to separate those 2 parts of code. It's just a personal preference and you can do it your way. With this set we can now start logging some errors. For example, let's update logAndPanic() function inside internal/conf/conf.go:

func logAndPanic(envVar string) {
  log.Panic().Str("envVar", envVar).Msg("ENV variable not set or value not valid")
}
Enter fullscreen mode Exit fullscreen mode

We can also log if error happens while generating password salt inside of internal/store/users.go:

func GenerateSalt() ([]byte, error) {
  salt := make([]byte, 16)
  if _, err := rand.Read(salt); err != nil {
    log.Error().Err(err).Msg("Unable to create salt")
    return nil, err
  }
  return salt, nil
}
Enter fullscreen mode Exit fullscreen mode

It's entirely up to you to log whatever you want. So feel free to update code with logging of your choice or check out RGB GitHub repo to see how i did it. Let's now move on to the next section.

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (0)

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay