Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename WithStack to Stack and WithFields to Fields #4

Merged
merged 4 commits into from
Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 74 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,61 @@
# Errors
A modern error handling package to add additional structured fields to errors. This allows you to keep the
[only handle errors once rule](https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully) while not losing context where the error occurred.

* `errors.Wrap(err, "while reading")` includes a stack trace so logging can report the exact location where
the error occurred. *You can also call `Wrapf()`*
* `errors.WithStack(err)` for when you don't need a message, just a stack trace to where the error occurred.
* `errors.WithFields{"fileName": fileName}.Wrap(err, "while reading")` Attach additional fields to the error and a stack
trace to give structured logging as much context to the error as possible. *You can also call `Wrapf()`*
* `errors.WithFields{"fileName": fileName}.WithStack(err)` for when you don't need a message, just a stack
trace and some fields attached.
* `errors.WithFields{"fileName": fileName}.Error("while reading")` when you want to create a string error with
some fields attached. *You can also call `Errorf()`*

### Extract structured data from wrapped errors
Convenience functions to extract all stack and field information from the error.
* `errors.ToLogrus() logrus.Fields`
* `errors.ToMap() map[string]interface{}`
An error handling package to add additional structured fields to errors. This package helps you keep the
[only handle errors once rule](https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully)
while not losing context where the error occurred.

## Usage
#### errors.Wrap()
includes a stack trace so logging can report the exact location where the error occurred.
*Includes `Wrapf()` and `Wrap()` variants*
```go
return errors.Wrapf(err, "while reading '%s'", fileName)
return errors.Wrapf(err, "while reading '%s'", fileName)
```
#### errors.Stack()
Identical to `errors.Wrap()` but you don't need a message, just a stack trace to where the error occurred.
```go
return errors.Stack(err)
```
#### errors.Fields{}
Attach additional fields to the error and a stack trace to give structured logging as much context
to the error as possible. *Includes `Wrap()`, `Wrapf()`, `Stack()`, `Error()` and `Errorf()` variants*
```go
return errors.Fields{"fileName": fileName}.Wrapf(err, "while reading '%s'", fileName)
return errors.Fields{"fileName": fileName}.Stack(err)
return errors.Fields{"fileName": fileName}.Error("while reading")
```
#### errors.WrapFields()
Works just like `errors.Fields{}` but allows collecting and passing around fields independent of the point of error
creation. In functions with many exit points this can result in cleaner less cluttered looking code.
```go
fields := map[string]any{
"domain.id": domainId,
}
err, accountID := account.GetByDomain(domainID)
if err != nil {
// Error only includes `domain.id`
return errors.WrapFields(err, fields, "during call to account.GetByDomain()")
}
fields["account.id"] = accountID

### Example
err, disabled := domain.Disable(accountID, domainID)
if err != nil {
// Error now includes `account.id` and `domain.id`
return errors.WrapFields(err, fields, "during call to domain.Disable()")
}
```
#### errors.Last()
Works just like `errors.As()` except it returns the last error in the chain instead of the first. In
this way you can discover the target which is closest to where the error occurred.
```go
// Returns the last error in the chain that has a stack trace attached
var last callstack.HasStackTrace
if errors.Last(err, &last)) {
fmt.Printf("Error occurred here: %+v", last.StackTrace())
}
```
#### errors.ToMap()
A convenience function to extract all stack and field information from the error.
```go
err := io.EOF
err = errors.WithFields{"fileName": "file.txt"}.Wrap(err, "while reading")
Expand All @@ -33,6 +71,24 @@ fmt.Printf("%#v\n", m)
// "fileName":"file.txt"
// }
```
#### errors.ToLogrus()
A convenience function to extract all stack and field information from the error in a form
appropriate for logrus.
```go
err := io.EOF
err = errors.WithFields{"fileName": "file.txt"}.Wrap(err, "while reading")
f := errors.ToLogrus(err)
logrus.WithFields(f).Info("test logrus fields")
// OUTPUT
// time="2023-02-20T19:11:05-06:00"
// level=info
// msg="test logrus fields"
// excFileName=/path/to/wrap_test.go
// excFuncName=my_package.ReadAFile
// excLineNum=21
// excType="*errors.wrappedError"
// excValue="while reading: EOF"
```

## Convenience to std error library methods
Provides pass through access to the standard `errors.Is()`, `errors.As()`, `errors.Unwrap()` so you don't need to
Expand All @@ -47,23 +103,6 @@ searchable fields.
If you have custom http middleware for handling unhandled errors, this is an excellent way
to easily pass additional information about the request up to the error handling middleware.

## Adding structured fields to an error
Wraps the original error while providing structured field data
```go
_, err := ioutil.ReadFile(fileName)
if err != nil {
return errors.WithFields{"file": fileName}.Wrap(err, "while reading")
}
```

## Retrieving the structured fields
Using `errors.WithFields{}` stores the provided fields for later retrieval by upstream code or structured logging
systems
```go
// Pass to logrus as structured logging
logrus.WithFields(errors.ToLogrus(err)).Error("open file error")
```

## Support for standard golang introspection functions
Errors wrapped with `errors.WithFields{}` are compatible with standard library introspection functions `errors.Unwrap()`,
`errors.Is()` and `errors.As()`
Expand Down
4 changes: 4 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import (
"reflect"
)

// NoMsg is a small indicator in the code that "" is intentional and there
// is no message include with the Wrap()
const NoMsg = ""

// Import all the standard errors functions as a convenience.

// Is reports whether any error in err's chain matches target.
Expand Down
81 changes: 53 additions & 28 deletions fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,102 +22,127 @@ type HasFormat interface {
Format(st fmt.State, verb rune)
}

// WithFields Creates errors that conform to the `HasFields` interface
type WithFields map[string]any
// Fields Creates errors that conform to the `HasFields` interface
type Fields map[string]any

// Wrapf returns an error annotating err with a stack trace
// at the point Wrapf is call, and the format specifier.
// If err is nil, Wrapf returns nil.
func (f WithFields) Wrapf(err error, format string, args ...any) error {
func (f Fields) Wrapf(err error, format string, args ...any) error {
if err == nil {
return nil
}
return &withFields{
return &fields{
stack: callstack.New(1),
fields: f,
wrapped: err,
msg: fmt.Sprintf(format, args...),
}
}

// WrapFields returns a new error wrapping the provided error with fields and a message.
func WrapFields(err error, f Fields, msg string) error {
if err == nil {
return nil
}
return &fields{
stack: callstack.New(1),
wrapped: err,
msg: msg,
fields: f,
}
}

// WrapFieldsf is identical to WrapFields but with optional formatting
func WrapFieldsf(err error, f Fields, format string, args ...any) error {
if err == nil {
return nil
}
return &fields{
msg: fmt.Sprintf(format, args...),
stack: callstack.New(1),
wrapped: err,
fields: f,
}
}

// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns nil.
func (f WithFields) Wrap(err error, msg string) error {
func (f Fields) Wrap(err error, msg string) error {
if err == nil {
return nil
}
return &withFields{
return &fields{
stack: callstack.New(1),
fields: f,
wrapped: err,
msg: msg,
}
}

// WithStack returns an error annotating err with a stack trace
// at the point WithStack is called
// If err is nil, WithStack returns nil.
func (f WithFields) WithStack(err error) error {
// Stack returns an error annotating err with a stack trace
// at the point Stack is called. If err is nil, Stack returns nil.
func (f Fields) Stack(err error) error {
if err == nil {
return nil
}
return &withFields{
return &fields{
stack: callstack.New(1),
fields: f,
wrapped: err,
}
}

func (f WithFields) Error(msg string) error {
return &withFields{
func (f Fields) Error(msg string) error {
return &fields{
stack: callstack.New(1),
fields: f,
wrapped: errors.New(msg),
msg: "",
}
}

func (f WithFields) Errorf(format string, args ...any) error {
return &withFields{
func (f Fields) Errorf(format string, args ...any) error {
return &fields{
stack: callstack.New(1),
fields: f,
wrapped: fmt.Errorf(format, args...),
msg: "",
}
}

type withFields struct {
fields WithFields
type fields struct {
fields Fields
msg string
wrapped error
stack *callstack.CallStack
}

func (c *withFields) Unwrap() error {
func (c *fields) Unwrap() error {
return c.wrapped
}

func (c *withFields) Is(target error) bool {
_, ok := target.(*withFields)
func (c *fields) Is(target error) bool {
_, ok := target.(*fields)
return ok
}

func (c *withFields) Error() string {
if c.msg == "" {
func (c *fields) Error() string {
if c.msg == NoMsg {
return c.wrapped.Error()
}
return c.msg + ": " + c.wrapped.Error()
}

func (c *withFields) StackTrace() callstack.StackTrace {
func (c *fields) StackTrace() callstack.StackTrace {
if child, ok := c.wrapped.(callstack.HasStackTrace); ok {
return child.StackTrace()
}
return c.stack.StackTrace()
}

func (c *withFields) HasFields() map[string]any {
func (c *fields) HasFields() map[string]any {
result := make(map[string]any, len(c.fields))
for key, value := range c.fields {
result[key] = value
Expand All @@ -137,11 +162,11 @@ func (c *withFields) HasFields() map[string]any {
return result
}

func (c *withFields) Format(s fmt.State, verb rune) {
func (c *fields) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
if c.msg == "" {
if c.msg == NoMsg {
_, _ = fmt.Fprintf(s, "%+v (%s)", c.Unwrap(), c.FormatFields())
return
}
Expand All @@ -155,7 +180,7 @@ func (c *withFields) Format(s fmt.State, verb rune) {
}
}

func (c *withFields) FormatFields() string {
func (c *fields) FormatFields() string {
var buf bytes.Buffer
var count int

Expand Down Expand Up @@ -200,7 +225,7 @@ func ToMap(err error) map[string]any {
// ToLogrus 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)
// logrus.Fields(errors.ToLogrus(err)).WithField("tid", 1).Error(err)
func ToLogrus(err error) logrus.Fields {
result := logrus.Fields{
"excValue": err.Error(),
Expand Down
Loading