From 1dd308fbddce5f891a6747bb9c600d90d110f1b9 Mon Sep 17 00:00:00 2001 From: "Derrick J. Wippler" Date: Wed, 15 Feb 2023 18:32:01 -0600 Subject: [PATCH 1/4] Added WrapFields() and NoMsg --- errors.go | 4 ++++ fields.go | 16 ++++++++++++++-- fields_test.go | 12 ++++++++++++ wrap.go | 2 +- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/errors.go b/errors.go index ee3a2bc..1f2aae0 100644 --- a/errors.go +++ b/errors.go @@ -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. diff --git a/fields.go b/fields.go index 299c76e..ff3692a 100644 --- a/fields.go +++ b/fields.go @@ -40,6 +40,18 @@ func (f WithFields) Wrapf(err error, format string, args ...any) error { } } +func WrapFields(err error, msg string, f map[string]any) error { + if err == nil { + return nil + } + return &withFields{ + stack: callstack.New(1), + fields: f, + wrapped: err, + msg: msg, + } +} + // 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. @@ -104,7 +116,7 @@ func (c *withFields) Is(target error) bool { } func (c *withFields) Error() string { - if c.msg == "" { + if c.msg == NoMsg { return c.wrapped.Error() } return c.msg + ": " + c.wrapped.Error() @@ -141,7 +153,7 @@ func (c *withFields) 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 } diff --git a/fields_test.go b/fields_test.go index 92912ed..6997545 100644 --- a/fields_test.go +++ b/fields_test.go @@ -198,6 +198,18 @@ func TestHasFields(t *testing.T) { assert.Equal(t, "errors.go", m["file"]) } +func TestWrapFields(t *testing.T) { + err := errors.New("last") + err = errors.Wrap(err, "second") + err = errors.WrapFields(err, "fields", map[string]any{"key1": "value1"}) + err = errors.Wrap(err, "first") + + m := errors.ToMap(err) + require.NotNil(t, m) + assert.Equal(t, "value1", m["key1"]) + assert.Equal(t, "first: fields: second: last", err.Error()) +} + func TestWithFieldsError(t *testing.T) { t.Run("WithFields.Error() should create a new error", func(t *testing.T) { err := errors.WithFields{"key1": "value1"}.Error("error") diff --git a/wrap.go b/wrap.go index f5a48f6..7aa9793 100644 --- a/wrap.go +++ b/wrap.go @@ -47,7 +47,7 @@ func (e *wrappedError) Is(target error) bool { } func (e *wrappedError) Error() string { - if e.msg == "" { + if e.msg == NoMsg { return e.wrapped.Error() } return e.msg + ": " + e.wrapped.Error() From cfa129d57b0588d949ed55151f956c9f83793560 Mon Sep 17 00:00:00 2001 From: "Derrick J. Wippler" Date: Wed, 15 Feb 2023 18:35:53 -0600 Subject: [PATCH 2/4] Renamed WithFields to Fields --- fields.go | 48 ++++++++++++++++++++++++------------------------ fields_test.go | 44 ++++++++++++++++++++++---------------------- stack_test.go | 4 ++-- 3 files changed, 48 insertions(+), 48 deletions(-) diff --git a/fields.go b/fields.go index ff3692a..28eecda 100644 --- a/fields.go +++ b/fields.go @@ -22,17 +22,17 @@ 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, @@ -44,7 +44,7 @@ func WrapFields(err error, msg string, f map[string]any) error { if err == nil { return nil } - return &withFields{ + return &fields{ stack: callstack.New(1), fields: f, wrapped: err, @@ -55,11 +55,11 @@ func WrapFields(err error, msg string, f map[string]any) error { // 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, @@ -70,19 +70,19 @@ func (f WithFields) Wrap(err error, msg string) error { // 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 { +func (f Fields) WithStack(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), @@ -90,8 +90,8 @@ func (f WithFields) Error(msg string) error { } } -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...), @@ -99,37 +99,37 @@ func (f WithFields) Errorf(format string, args ...any) error { } } -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 { +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 @@ -149,7 +149,7 @@ 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('+') { @@ -167,7 +167,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 @@ -212,7 +212,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(), diff --git a/fields_test.go b/fields_test.go index 6997545..ab060af 100644 --- a/fields_test.go +++ b/fields_test.go @@ -41,9 +41,9 @@ func TestToMapToLogrusFindsLastStackTrace(t *testing.T) { }) } -func TestWithFields(t *testing.T) { +func TestFields(t *testing.T) { err := &ErrTest{Msg: "query error"} - wrap := errors.WithFields{"key1": "value1"}.Wrap(err, "message") + wrap := errors.Fields{"key1": "value1"}.Wrap(err, "message") assert.NotNil(t, wrap) t.Run("Unwrap should return ErrTest", func(t *testing.T) { @@ -60,7 +60,7 @@ func TestWithFields(t *testing.T) { assert.Regexp(t, ".*/fields_test.go", m["excFileName"]) assert.Regexp(t, "\\d*", m["excLineNum"]) assert.Equal(t, "message: query error", m["excValue"]) - assert.Equal(t, "errors_test.TestWithFields", m["excFuncName"]) + assert.Equal(t, "errors_test.TestFields", m["excFuncName"]) assert.Equal(t, "*errors_test.ErrTest", m["excType"]) assert.Len(t, m, 6) }) @@ -88,7 +88,7 @@ func TestWithFields(t *testing.T) { assert.Contains(t, b.String(), `excValue="message: query error"`) assert.Contains(t, b.String(), `excType="*errors_test.ErrTest"`) assert.Contains(t, b.String(), "key1=value1") - assert.Contains(t, b.String(), "excFuncName=errors_test.TestWithFields") + assert.Contains(t, b.String(), "excFuncName=errors_test.TestFields") assert.Regexp(t, "excFileName=.*/fields_test.go", b.String()) assert.Regexp(t, "excLineNum=\\d*", b.String()) @@ -112,8 +112,8 @@ func TestWithFields(t *testing.T) { assert.Regexp(t, ".*/fields_test.go", f["excFileName"]) assert.Regexp(t, "\\d*", f["excLineNum"]) assert.Equal(t, "I have no stack trace: message: query error", f["excValue"]) - assert.Equal(t, "errors_test.TestWithFields", f["excFuncName"]) - assert.Equal(t, "*errors.withFields", f["excType"]) + assert.Equal(t, "errors_test.TestFields", f["excFuncName"]) + assert.Equal(t, "*errors.fields", f["excType"]) assert.Equal(t, "value1", f["key1"]) assert.Len(t, f, 6) @@ -121,28 +121,28 @@ func TestWithFields(t *testing.T) { }) t.Run("Wrap() should return nil, if error is nil", func(t *testing.T) { - got := errors.WithFields{"some": "context"}.Wrap(nil, "no error") + got := errors.Fields{"some": "context"}.Wrap(nil, "no error") assert.Nil(t, got) }) t.Run("Wrapf() should return nil, if error is nil", func(t *testing.T) { - got := errors.WithFields{"some": "context"}.Wrapf(nil, "no '%d' error", 1) + got := errors.Fields{"some": "context"}.Wrapf(nil, "no '%d' error", 1) assert.Nil(t, got) }) } func TestErrorf(t *testing.T) { err := errors.New("this is an error") - wrap := errors.WithFields{"key1": "value1", "key2": "value2"}.Wrap(err, "message") + wrap := errors.Fields{"key1": "value1", "key2": "value2"}.Wrap(err, "message") err = fmt.Errorf("wrapped: %w", wrap) assert.Equal(t, fmt.Sprintf("%s", err), "wrapped: message: this is an error") } func TestNestedWithFields(t *testing.T) { err := errors.New("this is an error") - err = errors.WithFields{"key1": "value1"}.Wrap(err, "message") + err = errors.Fields{"key1": "value1"}.Wrap(err, "message") err = errors.Wrap(err, "second") - err = errors.WithFields{"key2": "value2"}.Wrap(err, "message") + err = errors.Fields{"key2": "value2"}.Wrap(err, "message") err = errors.Wrap(err, "first") t.Run("ToMap() collects all values from nested fields", func(t *testing.T) { @@ -167,31 +167,31 @@ func TestNestedWithFields(t *testing.T) { func TestWithFieldsFmtDirectives(t *testing.T) { t.Run("Wrap() with a message", func(t *testing.T) { - err := errors.WithFields{"key1": "value1"}.Wrap(errors.New("error"), "shit happened") + err := errors.Fields{"key1": "value1"}.Wrap(errors.New("error"), "shit happened") assert.Equal(t, "shit happened: error", fmt.Sprintf("%s", err)) assert.Equal(t, "shit happened: error", fmt.Sprintf("%v", err)) assert.Equal(t, "shit happened: error (key1=value1)", fmt.Sprintf("%+v", err)) - assert.Equal(t, "*errors.withFields", fmt.Sprintf("%T", err)) + assert.Equal(t, "*errors.fields", fmt.Sprintf("%T", err)) }) t.Run("Wrap() without a message", func(t *testing.T) { - err := errors.WithFields{"key1": "value1"}.Wrap(errors.New("error"), "") + err := errors.Fields{"key1": "value1"}.Wrap(errors.New("error"), "") assert.Equal(t, "error", fmt.Sprintf("%s", err)) assert.Equal(t, "error", fmt.Sprintf("%v", err)) assert.Equal(t, "error (key1=value1)", fmt.Sprintf("%+v", err)) - assert.Equal(t, "*errors.withFields", fmt.Sprintf("%T", err)) + assert.Equal(t, "*errors.fields", fmt.Sprintf("%T", err)) }) } func TestWithFieldsErrorValue(t *testing.T) { err := io.EOF - wrap := errors.WithFields{"key1": "value1"}.Wrap(err, "message") + wrap := errors.Fields{"key1": "value1"}.Wrap(err, "message") assert.True(t, errors.Is(wrap, io.EOF)) } func TestHasFields(t *testing.T) { hf := &ErrHasFields{M: "error", F: map[string]any{"file": "errors.go"}} - err := errors.WithFields{"key1": "value1"}.Wrap(hf, "") + err := errors.Fields{"key1": "value1"}.Wrap(hf, "") m := errors.ToMap(err) require.NotNil(t, m) assert.Equal(t, "value1", m["key1"]) @@ -211,16 +211,16 @@ func TestWrapFields(t *testing.T) { } func TestWithFieldsError(t *testing.T) { - t.Run("WithFields.Error() should create a new error", func(t *testing.T) { - err := errors.WithFields{"key1": "value1"}.Error("error") + t.Run("Fields.Error() should create a new error", func(t *testing.T) { + err := errors.Fields{"key1": "value1"}.Error("error") m := errors.ToMap(err) require.NotNil(t, m) assert.Equal(t, "value1", m["key1"]) assert.Equal(t, "error", err.Error()) }) - t.Run("WithFields.Errorf() should create a new error", func(t *testing.T) { - err := errors.WithFields{"key1": "value1"}.Errorf("error '%d'", 1) + t.Run("Fields.Errorf() should create a new error", func(t *testing.T) { + err := errors.Fields{"key1": "value1"}.Errorf("error '%d'", 1) m := errors.ToMap(err) require.NotNil(t, m) assert.Equal(t, "value1", m["key1"]) @@ -229,7 +229,7 @@ func TestWithFieldsError(t *testing.T) { } func TestWithFieldsWithStack(t *testing.T) { - err := errors.WithFields{"key1": "value1"}.WithStack(io.EOF) + err := errors.Fields{"key1": "value1"}.WithStack(io.EOF) m := errors.ToMap(err) require.NotNil(t, m) assert.Equal(t, "value1", m["key1"]) diff --git a/stack_test.go b/stack_test.go index eb5d112..f7b5380 100644 --- a/stack_test.go +++ b/stack_test.go @@ -14,10 +14,10 @@ import ( // NOTE: Line numbers matter to this test func TestWrapWithFieldsAndWithStack(t *testing.T) { // NOTE: The stack from StackTrace() should report this line - // not the WithFields line below + // not the Fields line below s := errors.WithStack(&ErrTest{Msg: "error"}) - err := errors.WithFields{"key1": "value1"}.Wrap(s, "context") + err := errors.Fields{"key1": "value1"}.Wrap(s, "context") myErr := &ErrTest{} assert.True(t, errors.Is(err, &ErrTest{})) From 01ecd1fd05acd23545532384b159ceb5a1641c67 Mon Sep 17 00:00:00 2001 From: "Derrick J. Wippler" Date: Wed, 15 Feb 2023 18:50:25 -0600 Subject: [PATCH 3/4] Changed the function signature of WrapFields() --- fields.go | 18 ++++++++++++++++-- fields_test.go | 26 +++++++++++++++++++------- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/fields.go b/fields.go index 28eecda..bfa28fb 100644 --- a/fields.go +++ b/fields.go @@ -40,15 +40,29 @@ func (f Fields) Wrapf(err error, format string, args ...any) error { } } -func WrapFields(err error, msg string, f map[string]any) error { +// 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), - fields: f, 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, } } diff --git a/fields_test.go b/fields_test.go index ab060af..9fa9530 100644 --- a/fields_test.go +++ b/fields_test.go @@ -93,7 +93,7 @@ func TestFields(t *testing.T) { assert.Regexp(t, "excLineNum=\\d*", b.String()) // OUTPUT: time="2023-01-26T10:37:48-05:00" level=info msg="test logrus fields" - // excFileName=errors/fields_test.go excFuncName=errors_test.TestWithFields + // excFileName=errors/fields_test.go excFuncName=errors_test.TestFields // excLineNum=18 excType="*errors_test.ErrTest" excValue="message: query error" key1=value1 // t.Log(b.String()) @@ -138,7 +138,7 @@ func TestErrorf(t *testing.T) { assert.Equal(t, fmt.Sprintf("%s", err), "wrapped: message: this is an error") } -func TestNestedWithFields(t *testing.T) { +func TestNestedFields(t *testing.T) { err := errors.New("this is an error") err = errors.Fields{"key1": "value1"}.Wrap(err, "message") err = errors.Wrap(err, "second") @@ -165,7 +165,7 @@ func TestNestedWithFields(t *testing.T) { }) } -func TestWithFieldsFmtDirectives(t *testing.T) { +func TestFieldsFmtDirectives(t *testing.T) { t.Run("Wrap() with a message", func(t *testing.T) { err := errors.Fields{"key1": "value1"}.Wrap(errors.New("error"), "shit happened") assert.Equal(t, "shit happened: error", fmt.Sprintf("%s", err)) @@ -183,7 +183,7 @@ func TestWithFieldsFmtDirectives(t *testing.T) { }) } -func TestWithFieldsErrorValue(t *testing.T) { +func TestFieldsErrorValue(t *testing.T) { err := io.EOF wrap := errors.Fields{"key1": "value1"}.Wrap(err, "message") assert.True(t, errors.Is(wrap, io.EOF)) @@ -201,7 +201,7 @@ func TestHasFields(t *testing.T) { func TestWrapFields(t *testing.T) { err := errors.New("last") err = errors.Wrap(err, "second") - err = errors.WrapFields(err, "fields", map[string]any{"key1": "value1"}) + err = errors.WrapFields(err, errors.Fields{"key1": "value1"}, "fields") err = errors.Wrap(err, "first") m := errors.ToMap(err) @@ -210,7 +210,19 @@ func TestWrapFields(t *testing.T) { assert.Equal(t, "first: fields: second: last", err.Error()) } -func TestWithFieldsError(t *testing.T) { +func TestWrapFieldsf(t *testing.T) { + err := errors.New("last") + err = errors.Wrap(err, "second") + err = errors.WrapFieldsf(err, errors.Fields{"key1": "value1"}, "fields '%d'", 42) + err = errors.Wrap(err, "first") + + m := errors.ToMap(err) + require.NotNil(t, m) + assert.Equal(t, "value1", m["key1"]) + assert.Equal(t, "first: fields '42': second: last", err.Error()) +} + +func TestFieldsError(t *testing.T) { t.Run("Fields.Error() should create a new error", func(t *testing.T) { err := errors.Fields{"key1": "value1"}.Error("error") m := errors.ToMap(err) @@ -228,7 +240,7 @@ func TestWithFieldsError(t *testing.T) { }) } -func TestWithFieldsWithStack(t *testing.T) { +func TestFieldsWithStack(t *testing.T) { err := errors.Fields{"key1": "value1"}.WithStack(io.EOF) m := errors.ToMap(err) require.NotNil(t, m) From 1f5351eebd6481bd33a996045a9e3aa76e925b0a Mon Sep 17 00:00:00 2001 From: "Derrick J. Wippler" Date: Mon, 20 Feb 2023 19:27:28 -0600 Subject: [PATCH 4/4] Rename WithStack to Stack and updated README --- README.md | 109 +++++++++++++++++++++++++++++++++---------------- fields.go | 7 ++-- fields_test.go | 5 ++- stack.go | 20 ++++----- stack_test.go | 64 ++++++++++++++--------------- wrap_test.go | 4 +- 6 files changed, 124 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 0f2ca83..9c013c4 100644 --- a/README.md +++ b/README.md @@ -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") @@ -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 @@ -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()` diff --git a/fields.go b/fields.go index bfa28fb..5d64911 100644 --- a/fields.go +++ b/fields.go @@ -81,10 +81,9 @@ func (f Fields) Wrap(err error, msg string) error { } } -// 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 Fields) 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 } diff --git a/fields_test.go b/fields_test.go index 9fa9530..069c02c 100644 --- a/fields_test.go +++ b/fields_test.go @@ -37,6 +37,7 @@ func TestToMapToLogrusFindsLastStackTrace(t *testing.T) { logrus.SetOutput(&b) logrus.WithFields(f).Info("test logrus fields") logrus.SetOutput(os.Stdout) + fmt.Printf("%s\n", b.String()) assert.Contains(t, b.String(), "excLineNum=21") }) } @@ -240,8 +241,8 @@ func TestFieldsError(t *testing.T) { }) } -func TestFieldsWithStack(t *testing.T) { - err := errors.Fields{"key1": "value1"}.WithStack(io.EOF) +func TestFieldsStack(t *testing.T) { + err := errors.Fields{"key1": "value1"}.Stack(io.EOF) m := errors.ToMap(err) require.NotNil(t, m) assert.Equal(t, "value1", m["key1"]) diff --git a/stack.go b/stack.go index 442054e..bb467c3 100644 --- a/stack.go +++ b/stack.go @@ -8,31 +8,31 @@ import ( "github.com/mailgun/errors/callstack" ) -// WithStack annotates err with a stack trace at the point WithStack was called. -// If err is nil, WithStack returns nil. -func WithStack(err error) error { +// Stack annotates err with a stack trace at the point Stack was called. +// If err is nil, Stack returns nil. +func Stack(err error) error { if err == nil { return nil } - return &withStack{ + return &stack{ err, callstack.New(1), } } -type withStack struct { +type stack struct { error *callstack.CallStack } -func (w *withStack) Unwrap() error { return w.error } +func (w *stack) Unwrap() error { return w.error } -func (w *withStack) Is(target error) bool { - _, ok := target.(*withStack) +func (w *stack) Is(target error) bool { + _, ok := target.(*stack) return ok } -func (w *withStack) HasFields() map[string]any { +func (w *stack) HasFields() map[string]any { if child, ok := w.error.(HasFields); ok { return child.HasFields() } @@ -45,7 +45,7 @@ func (w *withStack) HasFields() map[string]any { return nil } -func (w *withStack) Format(s fmt.State, verb rune) { +func (w *stack) Format(s fmt.State, verb rune) { switch verb { case 'v': if s.Flag('+') { diff --git a/stack_test.go b/stack_test.go index f7b5380..7a2daac 100644 --- a/stack_test.go +++ b/stack_test.go @@ -12,10 +12,10 @@ import ( ) // NOTE: Line numbers matter to this test -func TestWrapWithFieldsAndWithStack(t *testing.T) { +func TestWrapWithFieldsAndStack(t *testing.T) { // NOTE: The stack from StackTrace() should report this line // not the Fields line below - s := errors.WithStack(&ErrTest{Msg: "error"}) + s := errors.Stack(&ErrTest{Msg: "error"}) err := errors.Fields{"key1": "value1"}.Wrap(s, "context") @@ -33,12 +33,12 @@ func TestWrapWithFieldsAndWithStack(t *testing.T) { trace := stack.StackTrace() caller := callstack.GetLastFrame(trace) assert.Contains(t, fmt.Sprintf("%+v", stack), "errors/stack_test.go:18") - assert.Equal(t, "errors_test.TestWrapWithFieldsAndWithStack", caller.Func) + assert.Equal(t, "errors_test.TestWrapWithFieldsAndStack", caller.Func) assert.Equal(t, 18, caller.LineNo) } -func TestWithStack(t *testing.T) { - err := errors.WithStack(io.EOF) +func TestStack(t *testing.T) { + err := errors.Stack(io.EOF) var files []string var funcs []string @@ -49,11 +49,11 @@ func TestWithStack(t *testing.T) { } } assert.True(t, linq.From(files).Contains("stack_test.go")) - assert.True(t, linq.From(funcs).Contains("TestWithStack"), funcs) + assert.True(t, linq.From(funcs).Contains("TestStack"), funcs) } -func TestWithStackWrapped(t *testing.T) { - err := errors.WithStack(&ErrTest{Msg: "query error"}) +func TestStackWrapped(t *testing.T) { + err := errors.Stack(&ErrTest{Msg: "query error"}) err = fmt.Errorf("wrapped: %w", err) // Can use errors.Is() from std `errors` package @@ -65,71 +65,71 @@ func TestWithStackWrapped(t *testing.T) { assert.Equal(t, myErr.Msg, "query error") } -func TestFormatWithStack(t *testing.T) { +func TestFormatStack(t *testing.T) { tests := []struct { err error Name string format string want []string }{{ - Name: "withStack() string", - err: errors.WithStack(io.EOF), + Name: "stack() string", + err: errors.Stack(io.EOF), format: "%s", want: []string{"EOF"}, }, { - Name: "withStack() value", - err: errors.WithStack(io.EOF), + Name: "stack() value", + err: errors.Stack(io.EOF), format: "%v", want: []string{"EOF"}, }, { - Name: "withStack() value plus", - err: errors.WithStack(io.EOF), + Name: "stack() value plus", + err: errors.Stack(io.EOF), format: "%+v", want: []string{ "EOF", - "github.com/mailgun/errors_test.TestFormatWithStack", + "github.com/mailgun/errors_test.TestFormatStack", }, }, { - Name: "withStack(errors.New()) string", - err: errors.WithStack(errors.New("error")), + Name: "stack(errors.New()) string", + err: errors.Stack(errors.New("error")), format: "%s", want: []string{"error"}, }, { - Name: "withStack(errors.New()) value", - err: errors.WithStack(errors.New("error")), + Name: "stack(errors.New()) value", + err: errors.Stack(errors.New("error")), format: "%v", want: []string{"error"}, }, { - Name: "withStack(errors.New()) value plus", - err: errors.WithStack(errors.New("error")), + Name: "stack(errors.New()) value plus", + err: errors.Stack(errors.New("error")), format: "%+v", want: []string{ "error", - "github.com/mailgun/errors_test.TestFormatWithStack", + "github.com/mailgun/errors_test.TestFormatStack", "errors/stack_test.go", }, }, { - Name: "errors.WithStack(errors.WithStack(io.EOF)) value plus", - err: errors.WithStack(errors.WithStack(io.EOF)), + Name: "errors.Stack(errors.Stack(io.EOF)) value plus", + err: errors.Stack(errors.Stack(io.EOF)), format: "%+v", want: []string{"EOF", - "github.com/mailgun/errors_test.TestFormatWithStack", - "github.com/mailgun/errors_test.TestFormatWithStack", + "github.com/mailgun/errors_test.TestFormatStack", + "github.com/mailgun/errors_test.TestFormatStack", }, }, { Name: "deeply nested stack", - err: errors.WithStack(errors.WithStack(fmt.Errorf("message: %w", io.EOF))), + err: errors.Stack(errors.Stack(fmt.Errorf("message: %w", io.EOF))), format: "%+v", want: []string{"EOF", "message", - "github.com/mailgun/errors_test.TestFormatWithStack", + "github.com/mailgun/errors_test.TestFormatStack", }, }, { - Name: "WithStack with fmt.Errorf()", - err: errors.WithStack(fmt.Errorf("error%d", 1)), + Name: "Stack with fmt.Errorf()", + err: errors.Stack(fmt.Errorf("error%d", 1)), format: "%+v", want: []string{"error1", - "github.com/mailgun/errors_test.TestFormatWithStack", + "github.com/mailgun/errors_test.TestFormatStack", }, }} diff --git a/wrap_test.go b/wrap_test.go index be61cb6..77653eb 100644 --- a/wrap_test.go +++ b/wrap_test.go @@ -3,15 +3,15 @@ package errors_test import ( "bytes" "fmt" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/require" "io" "os" "strings" "testing" "github.com/mailgun/errors" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestWrap(t *testing.T) {