package
4.20.3
Repository: https://github.com/mailgun/holster.git
Documentation: pkg.go.dev

# README

Errors

Package is a fork of https://github.com/pkg/errors with additional functions for improving the relationship between structured logging and error handling in go.

Adding structured context to an error

Wraps the original error while providing structured context data

_, err := ioutil.ReadFile(fileName)
if err != nil {
        return errors.WithContext{"file": fileName}.Wrap(err, "read failed")
}

Retrieving the structured context

Using errors.WithContext{} stores the provided context for later retrieval by upstream code or structured logging systems

// Pass to logrus as structured logging
logrus.WithFields(errors.ToLogrus(err)).Error("open file error")

Stack information on the source of the error is also included

context := errors.ToMap(err)
context == map[string]interface{}{
      "file": "my-file.txt",
      "go-func": "loadFile()",
      "go-line": 146,
      "go-file": "with_context_example.go"
}

Conforms to the Causer interface

Errors wrapped with errors.WithContext{} are compatible with errors wrapped by github.com/pkg/errors

switch err := errors.Cause(err).(type) {
case *MyError:
        // handle specifically
default:
        // unknown error
}

Proper Usage

The context wrapped by errors.WithContext{} is not intended to be used to by code to decide how an error should be handled. It is a convenience where the failure is well known, but the context is dynamic. In other words, you know the database returned an unrecoverable query error, but creating a new error type with the details of each query error is overkill ErrorFetchPage{}, ErrorFetchAll{}, ErrorFetchAuthor{}, etc...

As an example

func (r *Repository) FetchAuthor(isbn string) (Author, error) {
    // Returns ErrorNotFound{} if not exist
    book, err := r.fetchBook(isbn)
    if err != nil {
        return nil, errors.WithContext{"isbn": isbn}.Wrap(err, "while fetching book")
    }
    // Returns ErrorNotFound{} if not exist
    author, err := r.fetchAuthorByBook(book)
    if err != nil {
        return nil, errors.WithContext{"book": book}.Wrap(err, "while fetching author")
    }
    return author, nil
}

You should continue to create and inspect error types

type ErrorAuthorNotFound struct {}

func isNotFound(err error) {
    _, ok := err.(*ErrorAuthorNotFound)
    return ok
}

func main() {
    r := Repository{}
    author, err := r.FetchAuthor("isbn-213f-23422f52356")
    if err != nil {
        // Fetch the original Cause() and determine if the error is recoverable
        if isNotFound(error.Cause(err)) {
                author, err := r.AddBook("isbn-213f-23422f52356", "charles", "darwin")
        }
        if err != nil {
                logrus.WithFields(errors.ToLogrus(err)).Errorf("while fetching author - %s", err)
                os.Exit(1)
        }
    }
    fmt.Printf("Author %+v\n", author)
}

Context for concrete error types

If the error implements the errors.HasContext interface the context can be retrieved

context, ok := err.(errors.HasContext)
if ok {
    fmt.Println(context.Context())
}

This makes it easy for error types to provide their context information.

type ErrorBookNotFound struct {
   ISBN string
}
// Implements the `HasContext` interface
func (e *ErrorBookNotFound) func Context() map[string]interface{} {
   return map[string]interface{}{
       "isbn": e.ISBN,
   }
}

Now we can create the error and logrus knows how to retrieve the context

func (* Repository) FetchBook(isbn string) (*Book, error) {
    var book Book
    err := r.db.Query("SELECT * FROM books WHERE isbn = ?").One(&book)
    if err != nil {
        return nil, ErrorBookNotFound{ISBN: isbn}
    }
}

func main() {
    r := Repository{}
    book, err := r.FetchBook("isbn-213f-23422f52356")
    if err != nil {
        logrus.WithFields(errors.ToLogrus(err)).Errorf("while fetching book - %s", err)
        os.Exit(1)
    }
    fmt.Printf("Book %+v\n", book)
}

A Complete example

The following is a complete example using http://github.com/mailgun/logrus-hooks/kafkahook to marshal the context into ES fields.

package main

import (
    "log"
    "io/ioutil"

    "github.com/mailgun/holster/v4/errors"
    "github.com/mailgun/logrus-hooks/kafkahook"
    "github.com/sirupsen/logrus"
)

func OpenWithError(fileName string) error {
    _, err := ioutil.ReadFile(fileName)
    if err != nil {
            // pass the filename up via the error context
            return errors.WithContext{
                "file": fileName,
            }.Wrap(err, "read failed")
    }
    return nil
}

func main() {
    // Init the kafka hook logger
    hook, err := kafkahook.New(kafkahook.Config{
        Endpoints: []string{"kafka-n01", "kafka-n02"},
        Topic:     "udplog",
    })
    if err != nil {
        log.Fatal(err)
    }

    // Add the hook to logrus
    logrus.AddHook(hook)

    // Create an error and log it
    if err := OpenWithError("/tmp/non-existant.file"); err != nil {
        // This log line will show up in ES with the additional fields
        //
        // excText: "read failed"
        // excValue: "read failed: open /tmp/non-existant.file: no such file or directory"
        // excType: "*errors.WithContext"
        // filename: "/src/to/main.go"
        // funcName: "main()"
        // lineno: 25
        // context.file: "/tmp/non-existant.file"
        // context.domain.id: "some-id"
        // context.foo: "bar"
        logrus.WithFields(logrus.Fields{
            "domain.id": "some-id",
            "foo": "bar",
            "err": err,
        }).Error("log messge")
    }
}

# Functions

As finds the first error in err's chain that matches target, and if so, sets target to that error value and returns true.
Cause returns the underlying cause of the error, if possible.
Errorf formats according to a format specifier and returns the string as a value that satisfies error.
Is reports whether any error in err's chain matches target.
New returns an error with the supplied message.
Creates a new error that becomes the cause even if 'err' is a wrapped error but preserves the Context() and StackTrace() information.
New returns an error with the supplied message.
NewWithType returns an error decorated with error class and type.
Returns the context and stacktrace information for the underlying error as logrus.Fields{} returns empty logrus.Fields{} if err has no context or no stacktrace logrus.WithFields(errors.ToLogrus(err)).WithField("tid", 1).Error(err).
Returns the context for the underlying error as map[string]interface{} If no context is available returns nil.
Unwrap returns the result of calling the Unwrap method on err, if err's type contains an Unwrap method returning error.
WithMessage annotates err with a new message.
WithStack annotates err with a stack trace at the point WithStack was called.
Wrap returns an error annotating err with a stack trace at the point Wrap is called, and the supplied message.
Wrapf returns an error annotating err with a stack trace at the point Wrapf is call, and the format specifier.
WrapStack returns an error annotating err with a stack trace at the depth indicated.
WrapWithType returns a wrapped error decorated with class and type.

# Structs

No description provided by the author
TypedError is used to decorate an error with classification metadata.

# Interfaces

Implement this interface to pass along unstructured context to the logger.
True if the interface has the format method (from fmt package).

# Type aliases

Creates errors that conform to the `HasContext` interface.