Getting Started Quickly With Go Logging

It’s time to talk about how to get started with logging again. The languages we’ve covered so far are C#, Java, Python, Ruby, Node.js, and JavaScript. Today, we’re going to be talking about the Go programming language, also known as Golang.

Go is a statically compiled, open-source programming language created by Google in 2009. It runs on Windows, Linux, and Mac and has been increasingly adopted in the last couple of years because of its simplicity and concurrency mechanisms. And in case you didn’t know, Docker and Kubernetes were written in Go.

Go is quite an interesting language when we talk about logging. Why? For one thing, it includes a really simple native library for logging. But the most interesting thing is that in Go there are no exceptions—only errors. That means when you want to log application errors, things get interesting. We’ll cover that later in this post.

But let’s start by taking a look at how easily you can get started logging in Go.

Go Beaver With Scalyr Colors

The Simplest Go Logging That Could Possibly Work

Personally, I prefer to use the JetBrains offer for Go, but in this tutorial, I’m going to use VS Code because it’s free.

Let’s start by creating an empty folder. In my case, I created the folder at C:\Projects\go\go-logging.

Open VS Code. Then, click on File > Open Folder.

Go to the location of the folder you just created and select it by clicking on the Select Folder button:

In the left panel of VS Code, click on the icon indicated below to create a new file.

Name the new file console.go. The left panel should look like this now:

As I mentioned before, Go has a simple native logging library that you can use. By default, the output goes to the standard error of printing the date and time of the messages logged in the application. The simplest code that you can use to write logs is the following:

package main

import (
    "log"
)

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

In order to easily test the code without leaving VS Code, let’s enable the terminal by clicking on View > Integrated Terminal.

Because Go is a compiled language, we need to compile it by running the following command in the terminal:

go build console.go

There shouldn’t be any errors when running this command—the absence of a message means it succeeded.

In the same terminal, run the following command to execute the compiled program:

.\console.exe

The terminal will display the date, time, and message logged, like this:

Great—it works! But what if we need to store the logged messages in files? For that, you need to create a new file (or open an existing file), then set that file as the output of the log, like the following code:

package main

import (
    "log"
    "os"
)

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

    defer file.Close()

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

Now go back to the terminal and build and run the program. This time, no output will appear in the terminal because it was written to the file.

In the left panel, you’ll see the info.log file. Open it, and the same message you saw in the terminal should appear here:

And that’s it! You just created your first Go logger.

But as simple and easy as this could be, hello-world examples are not enough. Log messages could get messy and difficult to read if we continue logging manually like we just did. We need to know a few other things before we can seriously integrate logging into our applications.

What Is Application Logging?

Let’s take Erik’s definition from the first logging series post:

Application logging involves recording information about your application’s runtime behavior to a more persistent medium.

So what exactly does that mean? When problems arise, we want to know what caused the issue. I wish I could be physically inside the server to watch what’s happening in real time to spot the problem. But as far as I know, humans can’t be inside a server physically or virtually. Right?

But seriously, in a world where containers are getting more attention every day and the cloud is becoming de facto, instances of our application are very volatile. One day you could have access to logs on a server, and the next day the server could be gone. Even if you stored your logs in files locally, you lose valuable information when things like this happen. You need to have a persistent medium so that you can do post-mortem analysis more accurately.

Why Bother Logging? What’s the Motivation?

I remember my first days at a new job several years ago. I was learning how the application worked, and one day I decided to pair-troubleshoot in production with one of the original programmers. At that time, the company was a startup, so developers had access to production servers. In order to know what was wrong with the application, we needed to scratch the log files. But the application depended on several microservices, and there were times that one of those services was down.

The problem wasn’t really that one service was down. The problem was that we had to go through several files before we knew what was happening. So we decided to change the approach and started sending the log files to a centralized logging platform—a more persistent medium.

When you have application logging in place, you’ll be able to:

  • Resolve problems faster because you won’t need to go to each server to read logs.
  • Have a standard format to quickly spot issues. (If you read logs on a server, log formats can vary greatly from one application to another.)
  • Enable visibility to everyone that needs it. When you move logs to a centralized place, you can easily give access to developers so they can see for themselves what’s happening with the application without having access to the servers. It also helps you avoid the temptation to implement a fixhr inside the servers that could make things worse.
  • Automate notifications so you know when problems are happening. You might be able to catch and fix problems before your users notice it.

In a nutshell, you need application logs to go back in time and understand how the application behaves in the real world.

What Should You Log?

The data that companies are storing with applications are becoming more relevant each day. Want proof? Think of the companies going to court to explain what they’re doing with the data they collect. Or consider compliance initiatives like GDPR, SOX, or HIPAA. We need to be careful about what we put on those log files. And let’s be honest—there’s a lot of data we’ll never need.

Let me give you a short list of what I’ve found useful to log:

  • Date and time of the event in a standard timezone like UTC.
  • In distributed systems, a tracking flag to replicate the request across all the different points of the system.
  • Data that will help you to understand and make diagnostics of the application—things like error stacks, the external service name, header’s request, the URL path and its parameters, and similar kinds of data that will help you to reproduce the error and understand what happened.
  • Context data that you might need for all events like the ones I mentioned before so logging doesn’t get bulky in code.
  • The environment where the application is running—you could easily ignore anything else that’s not production.
  • The IP address of the server so you’ll know where the problem is.
  • Log levels such as panic, fatal, debug, or informational logs to give more context.

With the above data, I’ve been able to spot problems quickly and easily. Now, let’s take a look at how you can easily log in Go by using a logging framework. With the simple approach we saw before, we might have some spaghetti code that will be hard to maintain and extend.

Enter the Logging Framework

Logging is an already-solved problem, and there are logging frameworks that will help you log easily. A framework will let you do the same things we did at the beginning of this guide but with fewer lines of code and a standard way of logging. Logging frameworks cover almost all common scenarios, so it’s better that you choose a framework rather than do everything manually like in my first example. (The only case where I’d say you need to think twice about which framework you use is if your application’s performance is crucial.)

In the Go ecosystem, there are actually a lot of logging frameworks. You can see them listed on the Awesome Go page on GitHub.

The two most popular logging frameworks are Glog and Logrus. Google is the author of Glog. It keeps the simple style from Go, but no one has updated the GitHub repository for two years now. On the other hand, Logrus has been constantly supported by the community. In this guide, I’m going to use Logrus. Now, even though I like Glog’s simplicity, there are valuable things from Logrus that might be worth taking a look.

Getting Started With Logrus

Let’s start by installing the Logrus module by running the following command in the terminal of your computer:

go get "github.com/Sirupsen/logrus"

We’ll keep updating the code that we used before just to demonstrate how easily you could introduce logging frameworks in Go without having to change too many things in the code. So let’s replace the log import with the Logrus import, like this:

import (
    "os"

    log "github.com/sirupsen/logrus"
)

Keep everything as it’s now in the code and run it. The output of the log will look like this:

2018/05/22 17:02:43 Logging to a file in Go!
time="2018-05-22T17:33:03-06:00" level=info msg="Logging to a file in Go!"

A new line was added but in a different format: the default Logrus format. We’re still seeing the timestamp and the messages logged, but we’re also seeing the info level.

What does “level=info” mean? It’s the category of a message in the log.

And why would you need levels in logs? Well, let’s explain that with some readable code from Logrus GitHub page:

log.Debug("Useful debugging information.")
log.Info("Something noteworthy happened!")
log.Warn("You should probably take a look at this.")
log.Error("Something failed but I'm not quitting.")
// Calls os.Exit(1) after logging
log.Fatal("Bye.")
// Calls panic() after logging
log.Panic("I'm bailing.")

Order lines here are very important. I’ll explain why later in this guide.

Log levels don’t just give more context to the logged message by categorizing in groups. Log levels are also useful when you don’t need to have too much information about the application’s behavior. And vice-versa, there will be times when you’ll need to have all the information available to make a better analysis. This will also help you make better use of the storage or data transfer in your infrastructure.

Let’s add some interesting code here. Then I’ll explain the lines that are adding something different to the application that we’ve been coding.

package main

import (
    "os"

    log "github.com/sirupsen/logrus"
)

func main() {
    file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND, 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!")
}

Let’s start with this line:

log.SetFormatter(&log.JSONFormatter{})

One of the main purposes of using a framework is to have log data standardized. This is not only because it will be easy to read and understand; it’s also because the idea should be to move this log data to a centralized logging platform so that you can read logs from several sources in one place.

JSON is the standard format when you want to move logs somewhere else. That’s because JSON’s key/value style will make it easier to parse the data.

log.SetLevel(log.WarnLevel)

This line sets the log level we talked about before. Remember when I said earlier that the order of lines is important? The reason is that the log level also sets the importance of the logged message. The first level, debug, is the least important. And the last level, panic, is the most important. When you set a level in the code, as the line above does, you’re saying that all the log messages from that level down will be logged.

In this case, all debug and info messages won’t be written to the log.

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

I also mentioned that it’s important to include some context to the logged messages. In this line, we’re doing it by using the WithFields and Fields functions. That’s dummy data, but in the real world, you could use a data structure like the request parameters so that you can reproduce the problem.

Let’s compile and run the application. The log will look like this now:

2018/05/22 17:02:43 Logging to a file in Go!
time="2018-05-22T17:33:03-06:00" level=info msg="Logging to a file in Go!"
{"level":"warning","msg":"The group's number increased tremendously!","number":122,"omg":true,"time":"2018-05-22T17:52:47-06:00"}
{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,"time":"2018-05-22T17:52:47-06:00"}

You can now see the difference between this and other approaches we took in this guide. Only the warning and fatal log messages were written.

If you want to take a look at the code, you can check out the different approaches I’ve uploaded to GitHub as examples.

I can’t stress enough that you need to move these log files to a centralized logging platform. Even though you now have a standard format and it’s easy to add logging to your applications, you can’t keep going to the servers to read logs. It will be time-consuming as the application keeps growing.

Where Do You Go Now?

Logging frameworks are there to help you add logs to the application with just a few lines of code. What I really like about these frameworks is that you can guarantee that important data (such as timestamps, context, and log levels) is logged. Log levels can become your best friend when problems arise and you need to troubleshoot. By using log levels, you avoid being ashamed in demos with funny log messages that you might accidentally leave in the code.

Go’s native library for logging is really simple, but if you want to know more about it, you can take a look at the docs site.

In this post, I only just scratched the surface of Logrus logging framework. You can find useful information and examples by just going to the home page of the project on GitHub. You can configure hooks, log messages according to the environment, use formatters, and a lot of other things as per the docs site.

Since we just learned how easy it is to add logs to your applications, there’s no reason why you shouldn’t! If you’re worried about performance, just make sure you can turn off logging or easily change log levels. Trust me, you won’t regret it—you’ll need logs when you want to find out what’s wrong.

This post was written by Christian Meléndez. Christian is a technologist that started as a software developer and has more recently become a cloud architect focused on implementing continuous delivery pipelines with applications in several flavors, including .NET, Node.js, and Java, often using Docker containers.