elk

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2024 License: BSD-3-Clause Imports: 9 Imported by: 0

README

🦌 elk

An extensive error package with focus on comprehensiveness, tracability and ease of use.

Warning
This package is currently still in a proof-of-concept state and might undergo breaking API changes in the future until v1.0.0 is released.

Getting started

elk provides a simple error model called Error. It is classified by an ErrorCode and either wraps a given inner error or creates one if there is no underlying error. You can also pass an optional message to provide more detailed context to the error. Errors also record the callstack from where they have been created so that they can be easily traced thorugh the codebase, if necessary.

Create a detailed error with an error code and message.

const ErrDeviceNotFound = elk.ErrorCode("device-not-found")

err := elk.NewError(ErrDeviceNotFound, "the device could not be found")

Wrap a previous error with an error code and message.

device, err := db.GetDevice(id)
if err != nil {
    err = elk.Wrap(elk.CodeUnexpected, err,
        "failed receiving device from database")
}

Error also implements the fmt.Formatter interface so you can granularly control how errors are displayed. See the Formatting section for more information.

The recommended way to use this construct is to wrap an error on each layer in your application where the error changes the state of the outcome of the error. In example, when your database returns an ErrNoRows error and in your controller, that means that no values could be found for the given request, you can wrap the original database error with an error Code (ErrObjectNotFound i.E.) and an additional message to clarify what went wrong to either the user or developers of the layers above, if desired.

This way, you can give other meaning to errors on each layer without losign details about each consecutive error.

How to distinct Errors

The Error model is designed with clear error codes in mind to distinct them in a higher level in your application to finely control error behavior.

A specific example could be the top level route handler in a web server that calls a controller method which can fail in multiple different ways.

func handleGetData(ctl *Controller, w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")
    if id == "" {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    res, err := ctl.GetData(id)
    if err != nil {
        // Cast always returns an error of type `Error`, even if the returned
        // err is not. Then, it will be wrapped into an `Error` with code
        // elk.CodeUnexpected.
        switch elk.Cast(err).Code() {
        case ErrorDataNotFound:
            w.WriteHeader(http.StatusNotFound)
        case ErrorNoPermission:
            w.WriteHeader(http.StatusForbidden)
        default:
            // These are errors that might hint to a missbehavior of the 
            // application and thus, errors are logged using the detailed
            // format.
            log.Printf("error: %+.5v\n", err)
            w.WriteHeader(http.StatusInternalServerError)
        }
        // Display a comprehensive JSON representation of the error
        // containing the error code and the potential message.
        // The underlying error is not shown by default to prevent
        // leakage of internal application information.
        w.Write(elk.MustJson(err))
        return
    }

    d, _ := json.MarshalIndent(res, "", "  ")
    w.Write(d)
}
Formatting

In examples/formatting, you can find the different formatting options in use. Execute it to see them in action in your terminal!

As mentioned above, Error implements fmt.Formatter. So there are some custom options for printing Error instances.

%s or %q

Prints a single message in a single line. If the error has a message, the message is shown. Otherwise, the %s formatted contents of the inner error is displayed.

const MyErrorCode = elk.ErrorCode("my-error-code")

err := elk.Wrap(MyErrorCode,
    errors.New("something went wrong"),
    "Damn, what happened?")

fmt.Printf("%s\n", err)
// Output: Damn, what happened?
%v

Without any further flags, this prints a single line combined output of the wrapped errors code, message (if set) and inner errors text.

const MyErrorCode = elk.ErrorCode("my-error-code")

err := elk.Wrap(MyErrorCode,
    errors.New("something went wrong"),
    "Damn, what happened?")

fmt.Printf("%v\n", err)
// Output: <my-error-code> Damn, what happened? (something went wrong)

With the additional flag +, more details are shown like the callstack (see Callstack secion) of the error and the inner error. By passing the precision parameter (i.E. %+.5v), you can specify the maximum depth of the shown callstack. By default, a depth of 1000 is assumed. If you set this to 0, no call stack is printed.

const MyErrorCode = elk.ErrorCode("my-error-code")

err := elk.Wrap(MyErrorCode,
    errors.New("something went wrong"),
    "Damn, what happened?")

fmt.Printf("%+.5v\n", err)
// Output:
// <my-error-code> Damn, what happened?
// stack:
//   main.main             /home/foo/dev/lib/whoops/examples/formatting/main.go:50
//   runtime.main          /home/foo/.local/goup/current/go/src/runtime/proc.go:250
//   runtime.goexit        /home/foo/.local/goup/current/go/src/runtime/asm_amd64.s:1598
// inner error:
//   something went wrong

By setting the flag #, you can enable a verbose view of the error. This unwraps all layers of the error and prints a detailed overview of each visted error containing the error string, origin (where it has been wrapped) and the type of the error. You can also specify the maximum depth that shall be displayed by giving the precision parameter (i.E. %#.5v). When not specified, a default value of 1000 is assumed.

const MyErrorCode = elk.ErrorCode("my-error-code")

err := elk.Wrap(MyErrorCode,
    errors.New("something went wrong"),
    "Damn, what happened?")

fmt.Printf("%#.5v\n", err)
// Output:
// <my-error-code> Damn, what happened?
// originated:
//   main.main /home/foo/dev/lib/whoops/examples/formatting/main.go:59
// type:
//   elk.Error
// ----------
// something went wrong
// type:
//   *errors.errorString
// ----------
Callstack

When creating an Error–either by wrapping a previous error using Wrap or creating it using NewError–, it records where it has been wrapped in the Code in a CallStack object. This can then be accessed via the CallStack getter or is displayed when using the detailed and verbose formatting options as shown previously.

The CallStack contains a list of subsequent callers starting from the point where the CallStack has been created (when creating an Error instance, i.E.) followed by each previous caller of that function.

This CallStack object efficiently stores the frame pointers and resolves the context when calling the Frames getter on it.

Inner frames are wrapped using the CallFrame type, which also provides some formatting utilities.

Using the %s formatting verb, the CallFrame is printed in the following format.

main.main /home/me/dev/lib/elk/examples/formatting/main.go:59

When using the %v verb, it is formatted using the %v formatting on the underlying runtime.Frame.

Contribute

If you find any issues, want to submit a suggestion for a new feature or improvement of an existing one or just want to ask a question, feel free to create an Issue.

If you want to contribute to the project, just create a fork and create a pull request with your changes. We are happy to review your contribution and make you a part of the project. 😄


© 2023 B12-Touch GmbH
https://b12-touch.de

Covered by the BSD 3-Clause License.

Documentation

Overview

Package elk provides comprehensive models and utilities for better error handling with the focus on comprehensiveness, tracability and ease of use.

Example (Error)
package main

import (
	"encoding/json"
	"log"
	"os"

	"github.com/studio-b12/elk"
)

var (
	ErrorReadFile      = elk.ErrorCode("files:failed-reading-file")
	ErrorParsingConfig = elk.ErrorCode("config:failed-parsing")
	ErrorReadingConfig = elk.ErrorCode("config:failed-reading")
)

func readFile() ([]byte, error) {
	data, err := os.ReadFile("does/not/exist")
	if err != nil {
		return nil, elk.Wrap(ErrorReadFile, err, "failed reading file")
	}
	return data, nil
}

type configModel struct {
	BindAddress string
	LogLevel    int
}

func parseConfig() (cfg configModel, err error) {
	data, err := readFile()
	if err != nil {
		return configModel{},
			elk.Wrap(ErrorReadFile, err, "failed reading config file")
	}

	err = json.Unmarshal(data, &cfg)
	if err != nil {
		return configModel{},
			elk.Wrap(ErrorParsingConfig, err, "failed parsing config data")
	}

	return cfg, nil
}

func main() {
	_, err := parseConfig()
	if err != nil {
		log.Fatalf("config parsing failed: %v", err)
	}
}
Output:

Example (Formatting)
package main

import (
	"errors"
	"fmt"

	"github.com/studio-b12/elk"
)

func main() {
	err := errors.New("some normal error")
	fmt.Printf("%s\n", err)

	err = elk.Wrap(elk.CodeUnexpected, err, "Oh no!", "anyway")
	fmt.Printf("%s\n", err)

	// Print with callstack of depth 5
	fmt.Printf("%+5v\n", err)

	// Print detailed error stack
	fmt.Printf("%#v\n", err)
}
Output:

Example (Inner)
package main

import (
	"errors"
	"fmt"

	"github.com/studio-b12/elk"
)

type StatusError struct {
	elk.InnerError

	StatusCode int
}

func NewStatusError(inner error, status int) error {
	var s StatusError
	s.Inner = inner
	s.StatusCode = status
	return s
}

func (t StatusError) Error() string {
	return fmt.Sprintf("%s (%d)", t.Inner.Error(), t.StatusCode)
}

func main() {
	err := errors.New("not found")
	statusErr := NewStatusError(err, 404)
	fmt.Println(statusErr.Error())

	// Because elk.InnerError implements the Error()
	// as well as the Unwrap() method, StatusError inherits
	// these methods unless they are overridden.
	inner := errors.Unwrap(statusErr)
	fmt.Println(inner == err)

}
Output:

not found (404)
true

Index

Examples

Constants

View Source
const (
	CodeUnexpected = ErrorCode("unexpected-error")
)

Variables

This section is empty.

Functions

func As

func As[T error](err error) (t T, ok bool)

As applies errors.As() on the given err using the given type T as target for the unwrapping.

Refer to the documentation of errors.As() for more details: https://pkg.go.dev/errors#As

Example
package main

import (
	"errors"
	"fmt"

	"github.com/studio-b12/elk"
)

func main() {
	const ErrUnexpected = elk.ErrorCode("unexpected-error")

	type WrappedError struct {
		elk.InnerError
	}

	err := errors.New("some error")
	err = elk.Wrap(elk.CodeUnexpected, err, "Some message")
	err = WrappedError{InnerError: elk.InnerError{Inner: err}}

	Error, ok := elk.As[elk.Error](err)
	if ok {
		message := Error.Message()
		fmt.Println(message)
	}

}
Output:

Some message

func IsOfType

func IsOfType[T error](err error) bool

IsOfType returns true when the given error is of the type of T.

If not and the error can be unwrapped, the unwrapped error will be checked until it either matches the type T or can not be further unwrapped.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/studio-b12/elk"
)

func main() {
	type WrappedError struct {
		elk.InnerError
	}

	innerError := errors.New("some error")
	var err error = elk.Wrap(elk.CodeUnexpected, innerError, "Some message")
	err = WrappedError{InnerError: elk.InnerError{Inner: err}}

	is := elk.IsOfType[elk.Error](innerError)
	fmt.Println("innerError:", is)

	is = elk.IsOfType[elk.Error](err)
	fmt.Println("err:", is)

}
Output:

innerError: false
err: true

func Json

func Json(err error, statusCode int) ([]byte, error)

Json takes an error and marshals it into a JSON byte slice.

If err is a wrapped error, the inner error will be represented in the "error" field. Otherwise, the result of Error() on err will be represented in the "error" field. This does only apply though if exposeError is passed as true. By default, "error" will contain no information about the actual error to prevent unintended information leakage.

If the err implements HasCode, the code of the error will be represented in the "code" field of the JSON result.

If the err implements HasMessage, the JSON object will contain it as "message" field, if present.

When the JSON marshal fails, an error is returned.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/studio-b12/elk"
)

type DetailedError struct {
	elk.InnerError
	details any
}

func (t DetailedError) Details() any {
	return t.details
}

func main() {
	strErr := errors.New("some error")
	mErr := elk.Wrap("some-error-code", strErr, "some message")

	json, _ := elk.Json(strErr, 0)
	fmt.Println(string(json))

	json, _ = elk.Json(strErr, 400)
	fmt.Println(string(json))

	json, _ = elk.Json(mErr, 0)
	fmt.Println(string(json))

	json, _ = elk.Json(mErr, 400)
	fmt.Println(string(json))

	dtErr := DetailedError{}
	dtErr.Inner = elk.NewError("some-error", "an error with details")
	dtErr.details = struct {
		Foo string
		Bar int
	}{
		Foo: "foo",
		Bar: 123,
	}

	json, _ = elk.Json(dtErr, 500)
	fmt.Println(string(json))

	dteErr := elk.Wrap("some-detailed-error-wrapped", dtErr, "some detailed error wrapped")
	json, _ = elk.Json(dteErr, 500)
	fmt.Println(string(json))

}
Output:

{
  "Code": "unexpected-error"
}
{
  "Code": "unexpected-error",
  "Status": 400
}
{
  "Code": "some-error-code",
  "Message": "some message"
}
{
  "Code": "some-error-code",
  "Message": "some message",
  "Status": 400
}
{
  "Code": "unexpected-error",
  "Status": 500,
  "Details": {
    "Foo": "foo",
    "Bar": 123
  }
}
{
  "Code": "some-detailed-error-wrapped",
  "Message": "some detailed error wrapped",
  "Status": 500,
  "Details": {
    "Foo": "foo",
    "Bar": 123
  }
}

func JsonString

func JsonString(err error, statusCode int) (string, error)

JsonString behaves the same as Json() but returns the result as string instead of a slice of bytes.

func MustJson

func MustJson(err error, statusCode int) []byte

MustJson is an alias for Json but panics when the call to Json returns an error.

func MustJsonString

func MustJsonString(err error, statusCode int) string

MustJsonString is an alias for JsonString but panics when the call to Json returns an error.

func UnwrapFull

func UnwrapFull(err error) error

UnwrapFull takes an error and unwraps it until it can not be unwrapped anymore. Then, the last error is returned.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/studio-b12/elk"
)

func main() {
	type WrappedError struct {
		elk.InnerError
	}

	var err error
	originErr := errors.New("some error")
	err = elk.Wrap(elk.CodeUnexpected, originErr, "Some message")
	err = WrappedError{InnerError: elk.InnerError{Inner: err}}

	err = elk.UnwrapFull(err)
	fmt.Println(err == originErr)

}
Output:

true

Types

type CallFrame

type CallFrame runtime.Frame

CallFrame is a type alias for runtime.Frame with additional formatting functionailty used in downstream functions.

func (CallFrame) Format

func (t CallFrame) Format(s fmt.State, verb rune)

func (CallFrame) String

func (t CallFrame) String() string

type CallStack

type CallStack struct {
	// contains filtered or unexported fields
}

CallStack contains the list of called runtime.Frames in the call chain with an offset from which frames are reported.

func (*CallStack) At

func (t *CallStack) At(n int) (s string, ok bool)

At returns the formatted call frame at the given position n if existent.

func (*CallStack) First

func (t *CallStack) First() (s string, ok bool)

First is shorthand for At(0) and returns the first frame in the CallStack, if available.

func (*CallStack) Frames

func (t *CallStack) Frames() []CallFrame

Frames returns the offset slice of called runtime.Frame's in the recorded call stack.

func (*CallStack) String

func (t *CallStack) String() string

String returns the formatted output of the callstack as string.

func (*CallStack) Write

func (t *CallStack) Write(w io.Writer, max int)

Write formats the call stack into a table of called function and the file plus line number and writes the result into the writer w.

max defines the number of stack frames which are printed starting from the original caller.

func (*CallStack) WriteIndent

func (t *CallStack) WriteIndent(w io.Writer, max int, indent string)

WriteIndent is an alias for write with the given indent string attached before each line of output.

type Error

type Error struct {
	InnerError
	// contains filtered or unexported fields
}

Error contains a wrapped inner error, an optional message, optional details objects and a CallStack from where the error has been created.

func Cast

func Cast(err error, fallback ...ErrorCode) Error

Cast takes an arbitrary error and if it is not of type Error, it will be wrapped in a new Error which is then returned. If fallback is passed, it will be used as the ErrorCode of the new Error. Otherwise, CodeUnexpected is used.

If err is a joined error created with `errors.Join`, it will be inspected for contained elk Error elements. Depending on the contents of the join, it will be treated as following:

  • If the join contains exactly one elk Error, it will be returned.
  • If it contains no elk Error elements, the error will be wrapped using the passed fallback ErrorCode or CodeUnexpected.
  • If it contains more than one elk Error, a new wrapped Error is returned as well with the passed fallback ErrorCode or CodeUnexpected.

If err is of type Error, it is simply returned unchanged.

func NewError

func NewError(code ErrorCode, message ...string) Error

NewError creates a new Error with the given code and optional message.

func NewErrorf added in v0.2.0

func NewErrorf(code ErrorCode, format string, a ...any) Error

NewErrorf creates a new Error with the given code and message formatted according to the given format specification.

func Wrap

func Wrap(code ErrorCode, err error, message ...string) Error

Wrap takes an ErrorCode, error and an optional message and creates a new wrapped Error containing the passed error.

func WrapCopyCode added in v0.2.0

func WrapCopyCode(err error, message ...string) Error

WrapCopyCode wraps the error with an optional message keeping the error code of the wrapped error. If the wrapped error does not have a error code, CodeUnexpected is set insetad.

func WrapCopyCodef added in v0.2.0

func WrapCopyCodef(err error, format string, a ...any) Error

WrapCopyCodef wraps the error with a message formatted according to the given format specification keeping the error code of the wrapped error. If the wrapped error does not have a error code, CodeUnexpected is set instead.

func Wrapf added in v0.2.0

func Wrapf(code ErrorCode, err error, format string, a ...any) Error

Wrapf takes an ErrorCode, error and a message formatted according to the given format specification and creates a new wrapped Error containing the passed error.

func (Error) CallStack

func (t Error) CallStack() *CallStack

CallStack returns the errors CallStack starting from where the Error has been created.

func (Error) Code

func (t Error) Code() ErrorCode

Code returns the inner ErrorCode of the error.

func (Error) Error

func (t Error) Error() string

Error returns the error information as formatted string.

func (Error) Format

func (t Error) Format(s fmt.State, verb rune)

Format implements custom formatting rules used with the formatting functionalities in the fmt package.

%s, %q

Prints the message of the error, if available. Otherwise, the %s format of the inner error is represented. If the inner error is nil and no message is set, the error code is printed.

%v

Prints a more detailed representation of the error. Without any flags, the error is printed in the format `<{errorCode}> {message} ({innerError})`.

By passing the `+` flag, the inner error is represented in a seperate line. Also, by using the precision parameter, you can specify the depth of the represented callstack (i.E. `%+.5v` - prints a callstack of depth 5). Otherwise, no callstack will be printed.

Bypassing the `#` flag, an even more verbose representation of the error is printed. It shows the complete chain of errors wrapped in the Error with information about message, code, initiation origin and type of the error. With the precision parameter, you can define the depth of the unwrapping. The default value is 100, if not specified.

func (Error) Message

func (t Error) Message() string

Message returns the errors message text, if specified.

func (Error) ToResponseModel added in v0.4.0

func (t Error) ToResponseModel(statusCode int) (model ErrorResponseModel)

ToResponseModel transforms the

type ErrorCode

type ErrorCode string

type ErrorResponseModel added in v0.4.0

type ErrorResponseModel struct {
	Code    ErrorCode // The error code
	Message string    `json:",omitempty"` // An optional short message to further specify the error
	Status  int       `json:",omitempty"` // An optional platform- or protocol-specific status code; i.e. HTTP status code
	Details any       `json:",omitempty"` // Optional additional detailed context for the error
}

ErrorResponseModel is used to encode an Error into an API response.

type HasCallStack

type HasCallStack interface {
	error

	CallStack() *CallStack
}

HasCode describes an error which has a CallStack.

type HasCode

type HasCode interface {
	error

	// Code returns the inner ErrorCode of
	// the error.
	Code() ErrorCode
}

HasCode describes an error which has an ErrorCode.

type HasDetails added in v0.4.0

type HasDetails interface {
	error

	Details() any
}

type HasFormat

type HasFormat interface {
	error

	// Formatted returns the error details
	// as formatted string.
	Formatted() string
}

HasFormat describes an error with additional information which can be accessed as a formatted string.

type HasMessage

type HasMessage interface {
	error

	// Message returns the value for message.
	Message() string
}

HasMessage describes an error which has an additional message.

type InnerError

type InnerError struct {
	Inner error
}

InnerError wraps an inner error which's message is returned by calling Error() on it and which can be unwrapped using Unwrap().

InnerError is mostly used as anonymous field by other errors to "inherit" the unwrap functionality of contained errors.

func (InnerError) Error

func (t InnerError) Error() string

func (InnerError) Unwrap

func (t InnerError) Unwrap() error

Directories

Path Synopsis
examples
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL