Creating a Custom Logger in Go with Zerolog: A Step-by-Step Guide

Atharva PandeyAtharva Pandey
5 min read

Introduction

Logging is an essential part of any software application, as it allows developers to track and debug issues, as well as understand how the application is being used. In this tutorial, we will look at how to create a custom logger using Zerolog, a high-performance, zero-allocation logging library for Go.

What is Zerolog?

Zerolog is a logging library for the Go programming language that provides fast, structured logging with zero allocations. It is designed to be easy to use and customizable, with support for a wide range of log levels, outputs, and formatting options. Zerolog logs messages in JSON format, which makes it easy to parse and analyze log data using tools such as Elasticsearch, Splunk, or Logstash.

Creating a Custom Logger with Zerolog

Let's start by looking at the code snippet provided:

package log

import (
    "fmt"
    "os"

    "github.com/rs/zerolog"
)

// Logger is a custom logger that logs messages using Zerolog.
type Logger struct {
    logger zerolog.Logger
}

// Log is a global instance of the Logger type.
var Log Logger

The code begins by defining a Logger struct, which will be used to store our custom logger. The Logger struct has a single field, logger, which is of type zerolog.Logger from the Zerolog library.

We also define a global variable Log of type Logger, which will be used to access our custom logger from anywhere in the code.

// Init initializes the logger with the given log level and output.
// It should be called only once, in the main function.
func Init(level string, output string) error {
    // Set the log level
    zerolog.SetGlobalLevel(zerolog.InfoLevel)
    switch level {
    case "debug":
        zerolog.SetGlobalLevel(zerolog.DebugLevel)
    case "info":
        zerolog.SetGlobalLevel(zerolog.InfoLevel)
    case "warn":
        zerolog.SetGlobalLevel(zerolog.WarnLevel)
    case "error":
        zerolog.SetGlobalLevel(zerolog.ErrorLevel)
    case "fatal":
        zerolog.SetGlobalLevel(zerolog.FatalLevel)
    case "panic":
        zerolog.SetGlobalLevel(zerolog.PanicLevel)
    }
// omitted code .....

The Init function starts by setting the global log level to InfoLevel, which means that only log messages with log level Info or higher will be logged. If the level argument passed to the Init function is different, the global log level is set accordingly using a switch statement.

For example, if the level argument is debug, the global log level will be set to DebugLevel, which means that all log messages will be logged. On the other hand, if the level argument is error, the global log level will be set to ErrorLevel, which means that only log messages with log level Error, Fatal, or Panic will be logged.

    // Set the log output
    var err error

    // to optimise disable console writer on prod JSON could be mucheasier to parse
    if output == "stdout" && os.Getenv("ENV") != "production" {
        Log.logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr, NoColor: false}).With().Timestamp().Logger()
    } else {
        if output == "stdout" {
            Log.logger = zerolog.New(os.Stdout).With().Timestamp().Logger()
        } else if output == "stderr" {
            Log.logger = zerolog.New(os.Stderr).With().Timestamp().Logger()
        } else {
            f, err := os.OpenFile(output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
            if err != nil {
                fmt.Println(err)
                return err
            }
            Log.logger = zerolog.New(f).With().Timestamp().Logger()
        }
    }

The Init function checks the value of the output argument to determine where the log messages should be written. If output is stdout and the environment variable ENV is not set to production, the log messages are written to the console using the zerolog.ConsoleWriter type. This is useful for development and testing, as it allows developers to see the log messages in real-time.

If output is stdout and the environment variable ENV is set to production, or if output is stderr, the log messages are written to the standard output or standard error stream, respectively.

If output is a file path, the log messages are written to the specified file using the os.OpenFile function. The function opens the file in append mode, creates it if it does not exist, and sets the permissions to 0644 (readable and writable by the owner, readable by everyone else).

Finally, we create a new zerolog.Logger instance using the chosen output and configure it to include timestamps in the log messages.

Logging Functions

The code snippet also defines a set of logging functions that can be used to log messages at different log levels:

// Debug logs a debug message.
func Debug(msg string) {
    Log.logger.Debug().Msg(msg)
}

// Info logs an info message.
func Info(msg string) {
    Log.logger.Info().Msg(msg)
}

// Info logs an info message.
func Infof(format string, v ...interface{}) {
    Log.logger.Info().Msgf(format, v...)
}

// Warn logs a warning message.
func Warn(msg string) {
    Log.logger.Warn().Msg(msg)
}

// Error logs an error message.
func Error(msg string, err error) {
    Log.logger.Err(err).Msg(msg)
}

// Fatal logs a fatal message and exits the program.
func Fatal(msg string, err error) {
    Log.logger.Fatal().Err(err).Msg(msg)
}

// Panic logs a panic message and panics.
func Panic(msg string, err error) {
    Log.logger.Panic().Err(err).Msg(msg)
}

These functions use the zerolog.Logger instance created in the Init function to log messages at different log levels. The Debug, Info, Warn, and Error functions log messages at the corresponding log levels, while the Fatal and Panic functions log messages at the Fatal and Panic levels, respectively, and exit or panic the program.

The Infof function allows you to log messages using a format string and a variable number of arguments, similar to the fmt.Printf function.

Using the Custom Logger

To use the custom logger in your Go application, you need to import the log package and call the Init function in the main function, passing the desired log level and output as arguments. For example:

package main

import "mysexypackage/log"

func main() {
    err := log.Init("info", "stdout")
    if err != nil {
        panic(err)
    }
    // ...
}

Conclusion

In this tutorial, we saw how to create a custom logger using Zerolog, a high-performance logging library for Go. We learned how to initialize the logger with a specific log level and output, and how to use the logging functions provided to log messages at different log levels. The only problem in the above code that i have faced is unit testing Fatal function. When testing it does not log anything while it works when I run it. If someone knows the good solution to it, I would be glad to know.

I hope this tutorial was helpful! Let me know if you have any questions or suggestions

0
Subscribe to my newsletter

Read articles from Atharva Pandey directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Atharva Pandey
Atharva Pandey

Hi, I have been software developer now for almost 7 years. Mostly working in web3/crypto space. Aspiring to build and architect new things and learn along the way. I primarily work on golang/node/ts. Developing end to end solutions for different products mostly working on microservices and developing SDKs. Currently focusing more to get into blockchain development p2p.