Go Logging

If you write software that runs in production, you likely want to include logging. You could use fmt.Println but I strongly recommend against it. Go has a dedicated, built-in log package. In addition, there are third-party packages offering more features and better performance. Let’s dive into Go logging!

Introduction to Logging

Before delving into the specifics of logging in Go, it’s essential to understand what logging is and why it’s necessary. Logging means recording application events, also known as log messages, during runtime.

These events can include user transactions, system errors, or diagnostic information about the software’s execution. Developers, system administrators, and analysts utilize these logs to understand the application’s behavior, troubleshoot issues, and potentially detect and prevent security incidents.

Without an efficient logging mechanism, it becomes an arduous task to understand the operations of a large-scale or complex system. Logging provides the transparency necessary to gain insights into application behaviors, track down bugs, optimize performance, and maintain a robust and reliable system.

Standard Logging in Go

Go has a built-in logging library, making it easy for developers to emit basic log messages. To use the standard logging facility of Go, you’ll first need to import the log package. The following is a basic example:

package main

import (
    "log"
)

func main() {
    log.Println("This is a basic log message.")
}
Code language: Go (go)

In this example, log.Println writes the string "This is a basic log message." to the standard error (stderr), along with the timestamp of the event. The log package also provides a log.Fatal, which prints the log message and terminates the program.

In the following interactive code snippet, you can see both log calls at work:

Notice the output is sent to stderr? In addition, the call to log.Fatal causes the program the exit with an error code of 1. Now go ahead and remove the log.Fatal line, and you’ll see that the program exits normally (exit code 0).

Why stderr?

Standard output (stdout) and standard error (stderr) are output streams in Unix and Unix-like operating systems, such as Linux. They are both used by command line (CLI) programs to output text but serve different purposes.

The standard output, stdout, is the default file descriptor where a process can write the output. In the command line, this text is typically displayed on the screen. In the context of a script or a pipeline, it could be redirected or piped to a file or another command.

On the other hand, standard error, stderr, is designed to output error messages. Like stdout, it is also displayed on the screen by default. However, stderr is unbuffered and independent from stdout, which makes it ideal for showing error messages and log messages immediately when they occur.

So the main reason why the Go log library, along with many other logging systems, outputs to stderr instead of stdout is convention. This convention allows for the separation of regular program output from error messages and log information.

For instance, when running a command-line program, the user might want to redirect the program’s output (stdout) to a file for further processing while still wanting to see log messages and errors on the console. If log messages were sent to stdout, they would be mixed with the program’s output, which could be undesirable. By sending log messages to stderr, they remain visible on the console even if stdout is redirected.

Another reason is that stderr is unbuffered, meaning messages appear immediately when the program emits them, which is often desirable for logging.

Formatted Go logging

In addition to Println and Fatal, the log package offers functions to format log output. They work similarly to the fmt.Printf function, so I’d like to refer you to the documentation on Printf instead of repeating the information here.

Shortcomings of the Standard Go Logging

The standard log package in Go is a great starting point, but it has its limitations:

  1. One of the main limitations of the standard log package is that it doesn’t support log levels. Log levels (e.g., INFO, WARN, ERROR, FATAL) are useful for categorizing and filtering log messages based on severity.
  2. The standard library doesn’t offer the ability to customize timestamps or to redirect log output to different destinations easily.
  3. Modern logging practices often demand structured logs, which are machine-readable (e.g., in JSON format) for easy and efficient parsing and processing. The standard Go log package doesn’t support structured logging.

To overcome the limitations of the standard log package in Go, developers have two main options: extending the standard library or utilizing alternative logging libraries.

Extending the Standard Log Library

Many developers opt to write a custom log package that wraps the standard log library, providing additional features such as log levels. E.g., you could define a struct with functions like Info, Warn, Error. These functions would, in turn, call the regular log.Printf function and include a label like [INFO], [WARN], and [ERROR].

Using Alternative Logging Libraries

Several third-party logging libraries in Go provide advanced features missing in the standard library. Some of the most popular alternative loggers include:

My personal favorite is Zap. It is a blazing fast, structured, leveled logging library. It allows for flexible encoding of logs (both structured and non-structured), making it a versatile choice for different logging needs. I encourage you to check out the other though, and perhaps search the web for alternatives that might better suit your needs.