Logging from Go

Logging from Go - much easier than in that other language.

I recently wrote about some of the features of Go that I really like for getting code into production. After my initial excitement there were a couple of things I had to check before being certain Go would be a winner. The first was logging. Can I easily get log messages from my app to where I can see them? For us that means the excellent Logentries service.

I’ve written before about reconfiguring Java syslogging in Mule to send to Logentries. It was hard won ground and not an experience I would like to repeat. Logging from Java is a mess. Throw in multi line strack traces caused by exceptions “bubbling up the stack” and getting a readable message into a syslog server is suddenly an exercise in frustration. There is an excellent blog about this from ZeroTurnaround - The State of Logging in Java 2013. An unsurprising finding is that 87% of respondents to a questionnaire have no real-time way of seeing logs from their applications in production. In 2013 there was already a confusing array of Java logging frameworks and facades. Figuring out how to make them play nicely with your application, your dev, test, and prod environments, and your syslog server is a problem that quickly goes into the basket marked Too Hard. By 2015 things in Java land have not got simpler. If you’re in that 87% of developers I sympathize - there is little to make logging from your application as easy as it should be.

It turns out logging from Go is so easy I was left wondering how it ever got so hard in that other language. There are two core Go packages.

  • log - a simple logging package.
  • log/syslog - an interface to the system log service.

Check the Go example for using the log package. It writes to stderr, if you’re a Twelve-Factor App acolyte then you are nearly done (you will need to switch stderr to stdout). We like to go a step further and have an application in production send it’s log messages straight to Logentries.

This is easy to achieve using the log and log/syslog packages using TCP or UDP:

package main

import (
    "log"
    "log/syslog"
)

func main() {
    w, err := syslog.Dial("tcp", "api.logentries.com:10000", syslog.LOG_NOTICE, "LE_TOKEN")
    if err != nil {
        log.Fatal(err)
    }

    log.SetOutput(w)

    log.Println("Hello Logentries.")
}

The Logentries docs suggest the use of TLS for untrusted networks. When we’re running code in The Cloud and sending log messages to somewhere else in The Cloud, that means all networks. By using the crypto packages (also in the Go core) this is easy to achieve. It’s made even easier by the Go code being open source - I can largely copy the experts by reading the syslog code.

log/logentries is a Go package I wrote that makes it easy to reconfigure log to send messages to Logentries. Before you jump in it’s worth understanding my requirements:

  • I’m usually most interested in log messages during app start up (when configuration errors tend to show up).
  • Once an app is up and running we use metrics (not log messages) to track application performance.
  • I don’t ever want an app to block on logging.
  • If Logentries is not available for a some time I’m happy to log to stderr and then manually retrieve the logs later if I really need them. An alternative would be to store and forward.
  • A logger is for logging and a debugger is for, well, debugging. If you have to use logging for debugging then DELETE those calls before you commit.
  • I want to deploy applications without having to set up a syslog server as well. Syslog servers and their configuration are an arcane art best left to a specialist.

If you need more features, the code is open source. I hope it makes a useful starting point for you.

Sending our log message to Logentries from Go is now a simple case of calling one method during init. The rest of the application code is unchanged. During development we set an empty Logentries token. This causes the Init method to no-op and the app continues to log only to stderr. Here’s an example app, using the package, that will log to Logentries every five seconds. Create your own Logentries token and try it out.

package main

import (
	"github.com/GeoNet/log/logentries"
	"log"
	"time"
)

func init() {
  // If there is an env var LOGENTRIES_TOKEN then call to Init is not needed.
	logentries.Init("LOGENTRIES_TOKEN")
}

func main() {
	for {
		log.Print("Hello Logentries.")
		time.Sleep(time.Duration(5) * time.Second)
	}
}

This is how easy logging should be. It’s important, I need it, but it shouldn’t be a battle. Go makes it easy to log what I need and spend my energy focusing on the business problem.

Written on March 26, 2015