Skip to content
/ rich Public

errors with structured contextual information

License

Notifications You must be signed in to change notification settings

awfm9/rich

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Rich Contextual Go Errors

godoc license Build Status

The rich package provides errors with structured context information for popular Go logging libraries:

Installation

go get -u github.com/awfm/rich

Examples

Zerolog

package main

import (
  "io"
  "os"

  rich "github.com/awfm/rich/zerolog"
  "github.com/rs/zerolog/log"
)

func main() {

  log := zerolog.New(os.Stderr)

  var src, dst *os.File

  err := copyFile(src, dst)
  if err != nil {
    rich.Log(log.Fatal).
      Err(err).
      Str("src", src.Name()).
      Str("dst", dst.Name()).
      Msg("could not copy file")
  }

  os.Exit(0)
}

func copyFile(src *os.File, dst *os.File) error {

  n, err := io.Copy(src, dst)
  if err != nil {
    return rich.Errorf("could not copy contents: %w", err).Int64("bytes_written", n)
  }
  
  return nil
}

Zap

package main

import (
  "io"
  "os"

  rich "github.com/awfm/rich/zap"
  "go.uber.org/zap"
)

func main() {

  log := zap.NewProduction()

  var src, dst *os.File

  err := copyFile(src, dst)
  if err != nil {
    rich.Log(log).
      With(
        zap.Error(err), // has to be first item to use rich error context
        zap.String("src", src.Name()),
        zap.String("dst", src.Name()),
      ).
      Fatal("could not copy file")
  }

  os.Exit(0)
}

func copyFile(src *os.File, dst *os.File) error {

  n, err := io.Copy(src, dst)
  if err != nil {
    return rich.Errorf("could not copy contents: %w", err).With(zap.Int64("bytes_written", n))
  }
  
  return nil
}

Zap (sugared)

package main

import (
  "io"
  "os"

  rich "github.com/awfm/rich/zap"
  "go.uber.org/zap"
)

func main() {

  sugar := zap.NewProduction().Sugar()

  var src, dst *os.File

  err := copyFile(src, dst)
  if err != nil {
    rich.Sugar(sugar).
      With(
        "error", err, // has to be first item to use rich error context
        "src", src.Name(),
        "dst", dst.Name(),
      ).
      Fatal("could not copy file")
  }

  os.Exit(0)
}

func copyFile(src *os.File, dst *os.File) error {

  n, err := io.Copy(src, dst)
  if err != nil {
    return rich.Errorf("could not copy contents: %w", err).Sugar().With("bytes_written", n)
  }
  
  return nil
}

Logrus

package main

import (
  "io"
  "os"

  rich "github.com/awfm/rich/logrus"
  "github.com/sirupsen/logrus"
)

func main() {

  log := logrus.New()

  var src, dst *os.File

  err := copyFile(src, dst)
  if err != nil {
    rich.Log(log).
      WithError(err).
      WithFields(logrus.Fields{
        "src": src.Name(),
        "dst", dst.Name(),
      }).
      Fatal("could not copy file")
  }

  os.Exit(0)
}

func copyFile(src *os.File, dst *os.File) error {

  n, err := io.Copy(src, dst)
  if err != nil {
    return rich.Errorf("could not copy contents: %w", err).WithField("bytes_written", n)
  }
  
  return nil
}

Output

{"level": "fatal", "src": "file1", "dst": "file2", "bytes_written": 123, "err": "could not copy contents: some file error"}

Rationale

I like Go error handling. While verbose, it offers a pragmatic and unambiguous way to handle failure. With the introduction of error wrapping into the standard library in Go 1.13, we now even have a portable way to pass contextual information for errors across API boundaries.

I also love structured JSON logging. With tools such as jq, it becomes easy to analyze your application's logic in detail. The visibility gained is an indispensable part of monitoring and maintenance in large-scale deployments of production applications.

Unfortunately, while both the logging libraries and the error wrapping idiom offer ways to add context to error messages, they are not compatible in a meaningful way. The rich library was created to bridge this gap and log the contextual information of an error in a structured manner.

For instance, consider some of the code from the Zerolog example above, when not using rich:

n, err := io.Copy(in, out)
if err != nil {
  return fmt.Errorf("could not copy contents (bytes written: %d): %w", n, err)
}

When logging the error, the context - unknown to the calling function - cannot be part of structured logging:

if err != nil {
  log.Error().Str("src", src).Str("dst", dst).Err(err).Msg("could not copy file")
}

The output will mix structured contextual information from the caller with unstructured error context:

{"level": "fatal", "src": "file1", "dst": "file2", "err": "could not copy contents (bytes written: 123): some file error"}

The rich error package bridges this gap between error context and logging context, bringing all of the advantages of structured logging to errors.

Tips

Error handling with go is simple; however, there are still a few tips to keep in mind to get the most out of it.

Don't add function parameters to the error context

When a function is called, the caller already has access to all the information on the parameters. It should therefore be left to the caller which information is included in the context for logging.

Don't do:

func do(p1 string, p2 uint64) error {
  return rich.Errorf("could not do stuff: %w", err).Str("p1", p1).Uint64("p2", p2)
}

Instead, do:

func do(p1 string, p2 uint64) error {
   return rich.Errorf("could not do stuff: %w", err)
}

The caller can than choose:

err := do(p1, p2)
if err != nil {
  rich.Log(log.Warn).Str("p1", p1).Uint64("p2", p2)
}

Only provide context relevant for the error path

When you have multiple error paths in a function, don't include all of the information in each of them. If context information isn't relevant for a path, don't include it.

If you have this:

n, err := f.Write(data)
if err != nil {
  return rich.Errorf("could not write data: %w", err).Int64("bytes_written", n)
}

Don't do:

err = f.Close()
if err != nil {
  return rich.Errorf("could not close file: %w", err).Int64("bytes_written", n)
}

Instead, do:

err = f.Close()
if err != nil {
  return rich.Errorf("could not close file: %w", err)
}

About

errors with structured contextual information

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages