Nov 21st, ‘24/13 min read

Logging Errors in Go with ZeroLog: A Simple Guide

Learn how to log errors efficiently in Go using ZeroLog with best practices like structured logging, context-rich messages, and error-level filtering.

Logging Errors in Go with ZeroLog: A Simple Guide

ZeroLog is a high-performance, zero-allocation structured logging library for Go. It outputs logs in JSON format, making them easy to parse and analyze.

Designed for minimal overhead, it’s an excellent choice for modern applications where performance and scalability are key.

In this blog, we'll talk about ZeroLog, how to handle errors, some best practices, and more.

Getting Started with ZeroLog

To integrate ZeroLog into your project, first install the package:

go get -u github.com/rs/zerolog

Next, initialize a logger in your application:

package main

import (
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"os"
)

func main() {
	log.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()
	log.Info().Msg("Hello, ZeroLog!")
}

In this example, we initialize a global logger that outputs structured JSON logs to stdout.

Understanding Log Levels in ZeroLog

ZeroLog offers multiple log levels:

  • debuglevel: Used for debugging.
  • infolevel: Ideal for general informational messages.
  • warnlevel: Logs warning messages.
  • errorlevel: Logs errors that need immediate attention.
  • fatal message: Captures fatal errors and exits the application.

To set the log level globally:

zerolog.SetGlobalLevel(zerolog.WarnLevel)

To log messages at different levels:

log.Debug().Msgf("Debugging %s", "foo")
log.Warn().Str("warning", "potential issue").Msg("A warning message")
log.Error().Err(err).Msg("An error occurred")
Golang Logging: A Comprehensive Guide for Developers | Last9
Our blog covers practical insights into Golang logging, including how to use the log package, popular third-party libraries, and tips for structured logging

Working with Contextual Logging

ZeroLog's contextual logging allows you to attach additional fields to logs, making it easier to track important details for each log entry. For instance:

log.Info().Str("component", "API").Int("status_code", 200).Msg("Request completed")

In this example, we add contextual data like component and status_code to the log, helping to give more insights about the event.

Additionally, ZeroLog integrates with the ctx package to propagate contextual data across requests. This is particularly useful for web applications built with frameworks like Echo or Gin, where you can pass request-specific data through the lifecycle of an HTTP request.

Using Hooks for Custom Logic

ZeroLog allows you to extend its functionality with hooks, which are functions triggered during log events. You can use hooks to dynamically modify the log entries, such as adding extra fields, enriching data, or redirecting logs.

Here’s an example of using a hook:

log.Logger = log.Logger.Hook(zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    if level == zerolog.WarnLevel {
        e.Str("alert", "true")
    }
}))

log.Warn().Msg("This is a warning message")

In this example, we add an "alert" field to the log when a warning is logged. This enables dynamic behavior and customization of the log output.

How Structured Logging Makes Troubleshooting Easier | Last9
Structured logging organizes log data into a consistent format, making it easier to search and analyze. This helps teams troubleshoot issues faster and improve system reliability.

Handling Errors and Warnings

Error and warning logs are vital for understanding application issues. ZeroLog excels at handling errors with structured logs. Here’s how you can log errors and warnings effectively:

err := fmt.Errorf("dependency failed")
log.Error().Err(err).Msg("Failed to load dependency")

This generates a structured log with the error message, level, and stack trace (if enabled). This makes it easier to pinpoint where the issue occurred and what went wrong in the application.

Formatting Logs

ZeroLog’s integration with fmt.Sprintf allows for familiar string formatting, making it easy for Go developers to format log messages:

log.Info().Msgf("Welcome, %s!", "foo")

This example uses Msgf, which is similar to fmt.Printf, allowing you to include formatted values directly in the log message.

To customize the time format in your logs, ZeroLog provides an easy way to set the time field format globally:

zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Info().Msg("Custom time format")

In this case, the TimeFormatUnix format outputs timestamps as Unix timestamps, but you can adjust it to fit your needs.

Structured Logging with Headers and JSON

ZeroLog excels in structured logging, where logs are organized into fields. For instance, headers can be added to logs for better organization and context. Here’s how you can define a custom struct to represent headers and include it in your log entry:

type Header struct {
	Key   string `json:"key"`
	Value string `json:"value"`
}

log.Info().Interface("headers", Header{Key: "Authorization", Value: "Bearer token"}).Msg("Request headers")

This makes it easy to log complex, structured data (like HTTP headers) directly in the log entries. You can easily extend this to log any structured data that’s important to your use case.

Log Analytics 101: Everything You Need to Know | Last9
Get a clear understanding of log analytics—what it is, why it matters, and how it helps you keep your systems running efficiently by analyzing key data from your infrastructure.

Zero Allocation and Performance

One of ZeroLog’s standout features is zero allocation, which ensures minimal impact on your application’s performance.

Unlike many logging libraries, ZeroLog doesn’t allocate memory for each log entry, reducing garbage collection overhead and making it an excellent choice for high-performance applications.

This feature is particularly useful in systems with high log throughput, where maintaining performance is critical.

Integrating ZeroLog with Go Web Frameworks

ZeroLog integrates easily with web frameworks like Echo and Gin for logging HTTP requests. Here's an example of how to add HTTP request logging in an Echo application:

e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        log.Info().
            Str("method", c.Request().Method).
            Str("path", c.Request().URL.Path).
            Msg("HTTP request")
        return next(c)
    }
})

In this example, every incoming HTTP request is logged with its HTTP method and URL path. This makes it easy to track and debug HTTP traffic in your web application.

The Developer’s Handbook to Centralized Logging | Last9
This guide walks you through the implementation process, from defining requirements to choosing the right tools, setting up log storage, and configuring visualization dashboards.

Advanced ZeroLog Usage

Custom Logger Instances

In some cases, you might want to create different logger instances for different modules or contexts. This allows for more focused and contextual logging. Here's how you can do it:

customLogger := zerolog.New(os.Stdout).With().Str("module", "auth").Logger()
customLogger.Info().Msg("Authentication successful")

By using With() to add a custom field (e.g., "module": "auth"), you create a logger that clearly identifies the context of the log. This is especially useful in larger applications where you need to separate logs by components.

Global Logger Configuration

For consistent logging across your entire application, you can configure a global logger that applies to all parts of your program:

zerolog.SetGlobalLevel(zerolog.InfoLevel)
log.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()

This ensures that all log messages, regardless of where they come from, adhere to the same log level and include timestamps for better traceability.

Log Anything vs Log Everything | Last9
Explore the logging spectrum from “Log Anything” chaos to “Log Everything” clarity. Learn structured logging best practices in Go with zap!

Logging Events

For event-driven applications, ZeroLog provides an easy-to-use zerolog.event API that allows you to log events and capture specific actions. Here's how you can log an event like a user login:

event := log.Info()
event.Str("action", "user_login").Send()

In this example, a structured event is logged, which includes the action (user_login) as part of the log entry.

Testing ZeroLog

Testing logs is straightforward with ZeroLog. You can test log output by writing logs to an in-memory buffer and validating the content. Here's an example:

import (
    "bytes"
    "testing"
    "github.com/rs/zerolog"
)

func TestLogOutput(t *testing.T) {
    buf := &bytes.Buffer{}
    logger := zerolog.New(buf)
    logger.Info().Msg("Test log")

    if !bytes.Contains(buf.Bytes(), []byte("Test log")) {
        t.Fail()
    }
}

This test checks that the log message "Test log" is present in the output buffer, ensuring your logging logic is working as expected.

Encoding and Decoding Logs

ZeroLog allows you to encode complex fields in your logs, such as arrays or objects. For example, you can log an array of values:

log.Info().Array("ids", zerolog.Arr().Int(1).Int(2).Int(3)).Msg("Array encoded log")

This approach is useful when you need to log-structured data, like a list of IDs, and want to keep the log message easy to process and analyze.

Error Handling and Fatal Logs

Handling errors and logging critical issues is a fundamental part of logging. ZeroLog makes it easy to log errors and fatal messages:

log.Fatal().Msg("Critical error occurred")

This logs a fatal error and immediately terminates the application. It’s ideal for situations where the application cannot continue running due to a critical issue.

For non-fatal errors, you can log errors with additional context:

log.Error().Err(fmt.Errorf("file not found")).Msg("Error logging")

This captures the error (in this case, a file not found) and includes it in the log output.

Dependencies and Hooks

ZeroLog makes it easy to enrich your logs with additional context, such as dependency details. For example, you can log information about external dependencies like Redis:

log.Info().Str("dependency", "Redis").Msg("Dependency initialized")

Hooks allow you to dynamically add context based on the log level. Here's an example of how to add a critical field when logging an error:

log.Logger = log.Logger.Hook(zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    if level == zerolog.ErrorLevel {
        e.Bool("critical", true)
    }
}))

This ensures that all error-level logs are enriched with a critical field, making it clear when an error is more serious.

Python Logging Best Practices: The Ultimate Guide | Last9
This guide covers setting up logging, avoiding common mistakes, and applying advanced techniques to improve your debugging process, whether you’re working with small scripts or large applications.

Working with ConsoleWriter

While ZeroLog defaults to JSON for structured logs, you can also use ConsoleWriter it for human-readable output. This is useful for local development or debugging when you prefer logs in a more readable format.

writer := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "15:04:05"}
log.Logger = zerolog.New(writer).With().Logger()
log.Info().Msg("Console formatted log")

This example formats the log message with a human-readable timestamp and outputs it to the console. It’s an easy way to quickly view logs during development without dealing with raw JSON logs.

Logging Errors with ZeroLog

ZeroLog makes logging errors both efficient and straightforward, thanks to its structured logging capabilities. Here’s how you can leverage ZeroLog to handle error logs effectively in your Go applications.

Basic Error Logging

ZeroLog captures errors in a structured format, including error details and context, to help you diagnose issues easily:

package main

import (
    "fmt"
    "github.com/rs/zerolog/log"
)

func main() {
    err := fmt.Errorf("file not found")
    log.Error().Err(err).Msg("An error occurred while accessing the log file")
}

This produces a JSON-formatted output:

{
  "level": "error",
  "error": "file not found",
  "message": "An error occurred while accessing the log file",
  "time": "2024-11-21T10:00:00Z"
}

Adding Context to Error Logs

Enrich your error logs with additional fields to provide more context, making troubleshooting easier. For example:

log.Error().
    Err(fmt.Errorf("database connection failed")).
    Str("service", "user-auth").
    Int("retry_count", 3).
    Msg("Service unavailable")

The additional fields (service, retry_count) help provide context about the error's occurrence.

Capturing Line Numbers in Logs

To pinpoint the exact location of an error, you can capture the line number using Go’s runtime package:

import (
    "fmt"
    "runtime"
    "github.com/rs/zerolog/log"
)

func logWithLineNumber() {
    _, file, line, _ := runtime.Caller(1)
    log.Error().Str("file", file).Int("line", line).Msg("Error with line number")
}

This provides specific details about where the error occurred in the code, helping speed up the debugging process.

Using GetLogger for Customization

For modular applications, you can create a custom logger instance for different parts of your application:

import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)

func getLogger() zerolog.Logger {
    return log.With().Str("module", "error-handler").Logger()
}

func main() {
    logger := getLogger()
    logger.Error().Msg("Custom logger for error handling")
}

This ensures that each module has access to a logger tailored to its needs, providing better flexibility.

Handling Fatal Errors

ZeroLog’s Fatal method logs critical errors and immediately exits the application. Use it with caution to ensure proper cleanup before the program terminates:

log.Fatal().Err(fmt.Errorf("critical dependency unavailable")).Msg("Application shutting down")

The Fatal method logs the error and forces the application to stop, making it suitable for unresolvable issues.

Appending Context to Errors

Sometimes, you may need to append additional context dynamically to error logs. ZeroLog allows you to chain multiple log methods for this:

log.Error().
    Err(fmt.Errorf("network timeout")).
    Str("operation", "data-sync").
    Append("servers", "server1", "server2").
    Msg("Failed operation")

The Append method adds an array of values, providing detailed insights into the context of the failure.

Best Practices for Logging Errors

  • Use Constants for Uniformity: Define constants for log messages and field names to maintain consistency across logs.
  • Add Line Numbers: Always log the line number for easier debugging.
  • Redirect Critical Logs: Ensure error logs are written to dedicated files for better monitoring.
  • Use Context-Rich Logs: Include as much detail as possible using fields like headers or stack traces.
  • Simulate Printf-Style Logging: When necessary, format your logs dynamically with Msgf.
  • Use Modular Loggers: Use getLogger to create scoped loggers for specific components.
With Last9, we eliminated the toil. It just works.

— Matt Iselin, Head of SRE, Replit

Conclusion

ZeroLog brings efficiency, simplicity, and structure to logging in Go. Its zero-allocation design, rich API, and seamless integration make it an indispensable tool for modern applications.

ZeroLog's features like hooks, contextual logging, and custom encoders, help you create scalable and maintainable logging systems for your Go projects.

🤝
We’d love to hear your thoughts on SRE, reliability, observability, and monitoring! Join our SRE Discord community and connect with like-minded folks!

FAQs

What is ZeroLog?
ZeroLog is a high-performance, zero-allocation structured logging library for Go. It helps developers log messages in a structured, JSON format with minimal overhead, making it an ideal choice for applications where performance is critical.

What format is the ZeroLog log?
ZeroLog outputs logs in JSON format by default. This structured format makes it easy to parse and analyze log data in modern logging and monitoring systems.

What is the default level of ZeroLog?
The default log level in ZeroLog is debuglevel. You can adjust the log level globally or per logger using zerolog.SetGlobalLevel() or specific logger configurations.

How do I write Golang ZeroLogs to Redis?
To write ZeroLog logs to Redis, you can use a Redis client to send log messages to a Redis database:

import (
    "github.com/go-redis/redis/v8"
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
    "context"
)

func main() {
    client := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    logger := zerolog.New(client.Writer()).With().Timestamp().Logger()
    logger.Info().Msg("Log message sent to Redis")
}

How do I test that ZeroLog logger raised a log event of type error?
You can test ZeroLog output by capturing logs in a buffer and asserting the content:

import (
    "bytes"
    "testing"
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)

func TestErrorLog(t *testing.T) {
    buf := &bytes.Buffer{}
    log.Logger = zerolog.New(buf).With().Logger()
    log.Error().Msg("Test error")
    if !bytes.Contains(buf.Bytes(), []byte(`"level":"error"`)) {
        t.Errorf("Expected log level error not found")
    }
}

Which Logger is Best in Golang?
The best logger depends on your use case:

  • ZeroLog: Best for performance and structured JSON logging.
  • Logrus: Ideal for flexibility and compatibility.
  • Zap: Great for high-speed applications requiring structured logging.

Why is it desirable to switch logging libraries easily?
Switching libraries easily ensures flexibility to:

  • Adapt to changing project requirements.
  • Leverage performance or feature improvements in other libraries.
  • Maintain consistent log formats across multiple projects.

A common interface or wrapper makes transitions seamless.

What is the purpose of the blackbody radiation graph to be graphed using the below parameters?
This question seems unrelated to ZeroLog, but for blackbody radiation, graphs are plotted using parameters like temperature and wavelength to study emitted radiation and energy distribution.

How to use ZeroLog to filter INFO logs to stdout and ERROR logs to stderr?
You can configure separate loggers for stdout and stderr:

infoLogger := zerolog.New(os.Stdout).With().Timestamp().Logger()
errorLogger := zerolog.New(os.Stderr).With().Timestamp().Logger()
infoLogger.Info().Msg("This goes to stdout")
errorLogger.Error().Msg("This goes to stderr")

How do you integrate ZeroLog with an existing Go application for structured logging?
Follow these steps to integrate ZeroLog:

  1. Install the ZeroLog library.
  2. Replace your existing logging calls with ZeroLog's API.
  3. Configure a global or local logger:
import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)

func main() {
    log.Info().Msg("Application started")
}

How can I integrate ZeroLog with an existing Golang project?
You can integrate ZeroLog by:

  • Replacing your current logger initialization with ZeroLog.
  • Configuring contextual logging for structured logs.
  • Adjusting log levels and outputs to fit your existing setup.

How do I integrate ZeroLog with the Echo framework in Go?
Integrate ZeroLog with Echo for HTTP logging like this:

import (
    "github.com/labstack/echo/v4"
    "github.com/rs/zerolog/log"
)

func main() {
    e := echo.New()
    e.GET("/", func(c echo.Context) error {
        log.Info().Msg("Root endpoint hit")
        return c.String(200, "Hello, ZeroLog!")
    })
    e.Logger.Fatal(e.Start(":8080"))
}

How do you configure ZeroLog for structured logging in Go?
Use ZeroLog's API to configure structured logging:

import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)

func main() {
    log.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()
    log.Info().Str("component", "auth").Int("user_id", 101).Msg("User logged in")
}

How do you integrate ZeroLog with the Echo framework for logging HTTP requests?
Set up middleware in Echo to use ZeroLog for HTTP request logging:

e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        log.Info().Str("method", c.Request().Method).Str("path", c.Request().URL.Path).Msg("HTTP request")
        return next(c)
    }
})

How to integrate ZeroLog with a Golang web framework like Echo or Gin?
For Echo:

  • Use ZeroLog to log HTTP requests via middleware (as shown above).

For Gin:

  • Replace Gin’s default logger with ZeroLog:
gin.DefaultWriter = zerolog.ConsoleWriter{Out: os.Stdout}
r := gin.Default()

Contents


Newsletter

Stay updated on the latest from Last9.

Authors

Prathamesh Sonpatki

Prathamesh works as an evangelist at Last9, runs SRE stories - where SRE and DevOps folks share their stories, and maintains o11y.wiki - a glossary of all terms related to observability.

Topics

Handcrafted Related Posts