What do Docker, Kubernetes, and Prometheus have in common? All of these cloud-native technologies are written in the Go programming language, often referred to as Golang. Go is a highly efficient, minimalist language with rich features such as channels and goroutines for system-level concurrent programming. If you build or support Go applications, the following strategies and best practices can help you isolate, triage, and resolve performance anomalies.

This blog covers different Go logging packages and their relative strengths. We will go a bit deeper into collecting, centralizing, and managing logs with DataSet in Go environments.

The Simplest and Easiest Way to Start

There are several options for choosing a logging package in Go. The easiest way to get started is the built-in logging library called log. This library is conveniently located, available in the Go runtime, easy to use, and generates helpful timestamps without any configuration. Let's start with the simplest of examples:

package main

import (
  "log"
)

func main() {
  log.Print("Logging in Go!")
}

When you compile and run the executable, it will output the following log message:

$ go build basicLog.go
$ ./basicLog
2022/03/07 09:01:51 Logging in Go!

Certainly, it's better than nothing, but basically, it is an unstructured string with a timestamp. Although Print and Println are the most commonly used functions in the log package, and log doesn't support levels without another action, it offers functions that output error messages coupled with another action. Functions such as log.Fatal logs a message at a Level Fatal then exits the process. log.Panic logs a message at level Panic followed by a call to panic(), a built-in Go function.

Logging to a Custom Writer

The log package prints to stderr by default, but what if you want to write logs elsewhere? The log.SetOutput function allows you to do exactly that by letting you specify a custom io.Writer to write to, for example, a file:

package main

import (
  "log"
  "os"
)

func main() {
  file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
  if err != nil {
    log.Fatal(err)
  }

  defer file.Close()

  log.SetOutput(file)
  log.Print("Logging to a file in Go!")
}

When you build and run the executable, it will create a file called info.log, and the messages will be written and appended to the file.

The log package provides a way to add contextual information such as file name, line number, date and time, etc. by using Log Flags, for example:

log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
$ ./logs 
2022/03/07 15:43:22 logs.go:17: Logging to a file in Go!

As you can see, it includes the date, time.  filename and line number of the log call

Choosing a Logging Package for Formatted Logs

In the Go ecosystem, there are several logging frameworks. The two most popular logging frameworks are Logrus and Zap.

Logrus is a logging package designed for structured logging and well-suited for formatting in JSON. The JSON format makes it easy to parse and understand your Go application logs. Logrus is in maintenance mode. New features are no longer added, although it is supported for security, backward compatibility, and performance.

Using Logrus, you can define standard fields to add to your JSON logs by using the function WithFields. Logrus comes with seven logging levels: Trace, Debug, Info, Warn, Error, Fatal, and Panic. You can make calls to the logger with appropriate levels:

Log levels don't just give more context to the logged message by categorizing it into groups; they are also helpful when you don't need all the information about the application's behavior.

Begin by downloading and installing the Logrus package in your go environment:

$ go get github.com/sirupsen/logrus

Let’s add some interesting code to understand how to use Logrus with Levels

package main

import (
  "os"

  log "github.com/sirupsen/logrus"
)

func main() {
  file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
  if err != nil {
    log.Fatal(err)
  }

  defer file.Close()

  log.SetOutput(file)
  log.SetFormatter(&log.JSONFormatter{})
  log.SetLevel(log.WarnLevel)

  log.WithFields(log.Fields{
    "animal": "walrus",
    "size": 10,
  }).Info("A group of walrus emerges from the ocean")

  log.WithFields(log.Fields{
    "omg": true,
    "number": 122,
  }).Warn("The group's number increased tremendously!")

  log.WithFields(log.Fields{
    "omg": true,
    "number": 100,
  }).Fatal("The ice breaks!")
}

When you build and execute this code, it will add a file called info.log with the following content:

{"level":"warning","msg":"The group's number increased tremendously!","number":122,"omg":true,"time":"2022-03-10T10:56:04-08:00"}
{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,"time":"2022-03-10T10:56:04-08:00"}

As we can see, warning and fatal messages are appended to the file because we have set the log level of log.SetLevel(log.WarnLevel).

Zap

Zap is a JSON-formatted logger designed to allocate memory as infrequently as possible and to use reflection and string formatting as little as possible. Zap is known for its speed and lower memory footprint, which is desirable for running applications at scale.

How fast is Zap? Blazing fast! According to their benchmark, Zap is about 500% faster than Logrus when a message with ten contextual fields is created.

Like Speed? You’ll Love DataSet.

DataSet is the log analytics platform that enables teams to quickly get answers from all of their log data, across different use cases, and from all time periods – streaming or historical. Teams choose DataSet to elastically scale to petabytes of data while delivering real-time performance at a fraction of the cost.

Once you have chosen a library to collect application logs, consider the following best practices to centralize logs and analyze to get real-time insights from them.

  • Write logs to a local file: Although you can ingest logs to DataSet using APIs, we recommend writing logs to a local file system so you can decouple tasks of collecting logs and ingesting logs to a central logs platform. We have optimized the DataSet agent to pick up logs from files automatically, so you don't have to do the heavy lifting yourself. You can use system utilities such as Logrotate that can compress and delete old log messages from the filesystem.
  • Don’t create new goroutines for outputting logs: Goroutines are functions that execute concurrently along with the main program flow. Log libraries create new goroutines for outputting logs, so you don't have to create those yourself in your main application.
  • Standardize log fields, formats, and context: If teams use different ways to describe attributes and contextual fields, it won't be easy to quickly understand logs across all services.  You can create a wrapper with standardized types to create logs in a consistent format.
  • Use a centralized log management platform: Collect, aggregate, search and manage logs from the entire stack – applications and infrastructure with DataSet. It will be severely inefficient to SSH into individual hosts and analyze logs. Use DataSet for contextual log analytics in real-time. With DataSet, you can detect, triage, and troubleshoot errors much more efficiently and reduce your time to resolve issues.

Final Thoughts

A well-thought-out logging strategy can help you gain insights quickly about your Go applications. There are multiple options to consider for logging in Go environments. Pick the library that works best for your requirements and centralize logs across the entire stack with DataSet. Get started by installing the DataSet agent in your environment.

Don’t yet have a DataSet account? Try fully-featured DataSet free for 30 days and immediately start getting value from your logs.