/lg

Golang logging library

Primary LanguageGoMIT LicenseMIT

Lg

A simple, structured logger for Go.

CircleCI GoDoc Go Report Card

Basic Usage

Lg provides timestamped logging with levels and/or Printf formatting:

package main

import (
  "github.com/autopilothq/lg"
)

func main() {

  // Simple logging
  lg.Println("starting")

  // Levels (trace, debug, info, warn, error, fatal)
  lg.Warn("danger")

  // Printf formatting
  thing := &map[string]string{
    "foo": "bar",
  }
  lg.Debugf("what is this %#v", thing)

}

Will output:

2017-09-15T00:08:54.851998345Z info  starting
2017-09-15T00:08:54.852009867Z warn  danger
2017-09-15T00:08:54.852022087Z debug what is this &map[string]string{"foo":"bar"}

Fields

Fields (key-value pairs) can be added to logging to provide context:

  // Add fields for context
  lg.Debug("things happened",
           lg.F{"traceid", "1234abcd"},
           lg.F{"module", "thinginator"})

Will output:

2017-09-15T00:16:43.848278652Z debug [traceid:1234abcd module:thinginator] things happened

Extending

Custom loggers can be created with pre-defined fields:

  // Extend logs to add default fields

  log1 := lg.Extend(lg.F{"traceid", "1234abcd"})

  log2 := log1.Extend(lg.F{"module", "thinginator"})

  log2.Debug("things happened")

Will output:

2017-09-15T00:19:16.813565094Z debug [traceid:1234abcd module:thinginator] things happened

Custom loggers conform to the interface lg.Log which makes them easy to store and re-use.

Output

By default, lg will output in plain text format to stdout. You can add and remove outputs as needed, configure their log levels and/or use JSON format:

// remove default stdout output
lg.RemoveOutput(os.Stdout)

// write logging at level info or higher to a file
f, err := os.OpenFile("/tmp/log", os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
  panic(err)
}
lg.AddOutput(f, lg.MinLevel(lg.LevelInfo))

// write logging at level error or higher to stderr
lg.AddOutput(os.Stderr, lg.MinLevel(lg.LevelError))

// write logging at any level to stdout, in JSON format
lg.AddOutput(os.Stdout, lg.JSON())

Hooks

Hooks are similar to outputs, but instead of writing to an output stream, a hook function is called with a log entry.

handler = func(e *lg.Entry) {
  fmt.Println(e.Timestamp, e.Level.String(), e.Message, e.Fields)
}

hookID := lg.AddHook(handler)

// A hook can be removed by passing the hookId to lg.RemoveHook:
// lg.RemoveHook(hookID)

Mocking

Lg's strangest feature is it's mock loggers. But they can be very helpful for testing.

Say you have a function you want to unit test:

func ChurnSomeNumbers(log lg.Log, a int, b int) (int, error) {
  if a < b {
    err := fmt.Errorf("%d cannot be less than %d", a, b)
    log.Error(err)
    return 0, err
  }

  return a - b, nil
}

As well as testing passing cases, you'll also want to test failing cases, but that means your test output will contain errors, which are expected. Using a mock log can help here, you can silence output when the test passes, verify the function under test logs the way you expect it to, and you can still see the log output on failure, by including it in an assertion/expectation failure message:

Describe("ChurnSomeNumbers()", func() {
  BeforeEach({
    log := lg.Mock()
  })

  It("fails when a is less than b", func() {
    _, err := ChurnSomeNumbers(log, 3, 5)
    Expect(err).To(HaveOccurred())
    Expect(log.Count(
      lg.AtLevel(lg.LevelError),
    )).To(Equal(1), "log should contain exactly one error:\n%s", log.Dump())
  })
})

If the test passes, it will produce no extra output, but if it fails, the log will be included in the expectation failure message.

Running tests

To run tests, you'll need ginkgo and gomega:

$ go get github.com/onsi/ginkgo github.com/onsi/gomega

Then run the tests with ginkgo:

$ ginkgo