From 7fed68b36f2791ef38b14de11b8ca9ad06ef05a4 Mon Sep 17 00:00:00 2001 From: Igor Lazarev Date: Sat, 5 Aug 2023 10:27:33 +0300 Subject: [PATCH] feature: joining errors --- errors.go | 8 ++++++- errors_test.go | 40 +++++++++++++++---------------- joining.go | 17 ++++++++++++- json_test.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++-- logging_test.go | 2 +- 5 files changed, 106 insertions(+), 25 deletions(-) diff --git a/errors.go b/errors.go index 1a70136..7b1de8c 100644 --- a/errors.go +++ b/errors.go @@ -248,7 +248,13 @@ func (e *stacked) Format(s fmt.State, verb rune) { func (e *stacked) MarshalJSON() ([]byte, error) { data := mapWriter{"error": e.Error()} data.SetStackTrace(e.StackTrace()) - e.LogFields(data) + + var err error + for err = e; err != nil; err = Unwrap(err) { + if loggable, ok := err.(LoggableError); ok { + loggable.LogFields(data) + } + } return json.Marshal(data) } diff --git a/errors_test.go b/errors_test.go index 42163b4..d5f9ac9 100644 --- a/errors_test.go +++ b/errors_test.go @@ -24,7 +24,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("ooh"), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:23", + "\t.+/errors/errors_test.go:24", }, }, { @@ -32,7 +32,7 @@ func TestStackTrace(t *testing.T) { err: errors.Wrap(errors.Errorf("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:31", + "\t.+/errors/errors_test.go:32", }, }, { @@ -40,7 +40,7 @@ func TestStackTrace(t *testing.T) { err: errors.Wrap(errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:39", + "\t.+/errors/errors_test.go:40", }, }, { @@ -48,7 +48,7 @@ func TestStackTrace(t *testing.T) { err: errors.Wrap(errors.Wrap(errors.New("ooh"))), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:47", + "\t.+/errors/errors_test.go:48", }, }, { @@ -56,7 +56,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("ooh"), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:55", + "\t.+/errors/errors_test.go:56", }, }, { @@ -64,7 +64,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("%v", errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:63", + "\t.+/errors/errors_test.go:64", }, }, { @@ -72,7 +72,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("%w", errors.Wrap(errors.New("ooh"))), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:71", + "\t.+/errors/errors_test.go:72", }, }, { @@ -80,7 +80,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("%%w %v", errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:79", + "\t.+/errors/errors_test.go:80", }, }, { @@ -88,7 +88,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("%s: %w", "prefix", errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:87", + "\t.+/errors/errors_test.go:88", }, }, { @@ -96,7 +96,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("%w", errors.Errorf("%w", errors.New("ooh"))), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:95", + "\t.+/errors/errors_test.go:96", }, }, { @@ -104,7 +104,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("%w", fmt.Errorf("%w", errors.New("ooh"))), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:103", + "\t.+/errors/errors_test.go:104", }, }, { @@ -112,9 +112,9 @@ func TestStackTrace(t *testing.T) { err: wrap(errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.wrap\n" + - "\t.+/errors/errors_test.go:200", + "\t.+/errors/errors_test.go:201", "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:111", + "\t.+/errors/errors_test.go:112", }, }, { @@ -122,7 +122,7 @@ func TestStackTrace(t *testing.T) { err: wrapSkipCaller(errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:121", + "\t.+/errors/errors_test.go:122", }, }, { @@ -130,7 +130,7 @@ func TestStackTrace(t *testing.T) { err: wrapSkipCallers(errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:129", + "\t.+/errors/errors_test.go:130", }, }, { @@ -138,7 +138,7 @@ func TestStackTrace(t *testing.T) { err: errorfSkipCaller("ooh"), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:137", + "\t.+/errors/errors_test.go:138", }, }, { @@ -146,7 +146,7 @@ func TestStackTrace(t *testing.T) { err: errors.Join(fmt.Errorf("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:145", + "\t.+/errors/errors_test.go:146", }, }, { @@ -157,7 +157,7 @@ func TestStackTrace(t *testing.T) { ), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:153", + "\t.+/errors/errors_test.go:154", }, }, { @@ -167,7 +167,7 @@ func TestStackTrace(t *testing.T) { ), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:165", + "\t.+/errors/errors_test.go:166", }, }, { @@ -178,7 +178,7 @@ func TestStackTrace(t *testing.T) { ), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:174", + "\t.+/errors/errors_test.go:175", }, }, } diff --git a/joining.go b/joining.go index 17cd604..53af66b 100644 --- a/joining.go +++ b/joining.go @@ -51,7 +51,9 @@ type joinError struct { errs []error } -// todo: add marshal json? +func (e *joinError) LogFields(logger FieldLogger) { + logFieldsFromErrors(logger, e.errs) +} func (e *joinError) Error() string { var b []byte @@ -69,3 +71,16 @@ func (e *joinError) Error() string { func (e *joinError) Unwrap() []error { return e.errs } + +func logFieldsFromErrors(logger FieldLogger, errs []error) { + for _, err := range errs { + for w := err; w != nil; w = Unwrap(w) { + if j, ok := w.(interface{ Unwrap() []error }); ok { + logFieldsFromErrors(logger, j.Unwrap()) + } + if loggable, ok := w.(LoggableError); ok { + loggable.LogFields(logger) + } + } + } +} diff --git a/json_test.go b/json_test.go index 7246db8..269a3c0 100644 --- a/json_test.go +++ b/json_test.go @@ -2,6 +2,7 @@ package errors_test import ( "encoding/json" + stderrors "errors" "testing" "github.com/muonsoft/errors" @@ -27,7 +28,7 @@ func TestStackedError_MarshalJSON(t *testing.T) { { Function: "github.com/muonsoft/errors_test.TestStackedError_MarshalJSON", File: ".+/errors/json_test.go", - Line: 12, + Line: 13, }, }) if jsonError.Key != "value" { @@ -57,7 +58,7 @@ func TestWrappedError_MarshalJSON(t *testing.T) { { Function: "github.com/muonsoft/errors_test.TestWrappedError_MarshalJSON", File: ".+/errors/json_test.go", - Line: 40, + Line: 41, }, }) if jsonError.Key != "value" { @@ -68,6 +69,65 @@ func TestWrappedError_MarshalJSON(t *testing.T) { } } +func TestJoinedError_MarshalJSON(t *testing.T) { + err := errors.Join( + errors.Join( + errors.Wrap( + errors.Errorf("error 1", errors.String("key1", "value1")), + errors.String("key2", "value2"), + ), + errors.Errorf("error 2", errors.String("key3", "value3")), + stderrors.Join( + errors.Errorf("error 3", errors.String("key4", "value4")), + errors.Errorf("error 4", errors.String("key5", "value5")), + ), + ), + ) + jsonData, e := json.Marshal(err) + if e != nil { + t.Fatalf("expected %#v to be marshalable into json: %v", err, e) + } + var jsonError struct { + Error string `json:"error"` + StackTrace errorstest.StackTrace `json:"stackTrace"` + Key1 string `json:"key1"` + Key2 string `json:"key2"` + Key3 string `json:"key3"` + Key4 string `json:"key4"` + Key5 string `json:"key5"` + } + e = json.Unmarshal(jsonData, &jsonError) + if e != nil { + t.Fatalf("failed to unmarshal json: %v", e) + } + + if jsonError.Error != "error 1\nerror 2\nerror 3\nerror 4" { + t.Errorf("expected %#v to have error key", err) + } + assertStackRegexp(t, jsonError.StackTrace, errorstest.StackTrace{ + { + Function: "github.com/muonsoft/errors_test.TestJoinedError_MarshalJSON", + File: ".+/errors/json_test.go", + Line: 74, + }, + }) + if jsonError.Key1 != "value1" { + t.Errorf(`expected %#v to have key "key1"`, err) + } + if jsonError.Key2 != "value2" { + t.Errorf(`expected %#v to have key "key2"`, err) + } + if jsonError.Key3 != "value3" { + t.Errorf(`expected %#v to have key "key3"`, err) + } + if jsonError.Key4 != "value4" { + t.Errorf(`expected %#v to have key "key4"`, err) + } + if jsonError.Key5 != "value5" { + t.Errorf(`expected %#v to have key "key5"`, err) + } +} + type JSONError struct { Error string `json:"error"` StackTrace errorstest.StackTrace `json:"stackTrace"` diff --git a/logging_test.go b/logging_test.go index 03f3cb0..f486f84 100644 --- a/logging_test.go +++ b/logging_test.go @@ -39,7 +39,7 @@ func TestLog_errorWithStack(t *testing.T) { { Function: "github.com/muonsoft/errors_test.TestLog_errorWithStack", File: ".+errors/logging_test.go", - Line: 29, + Line: 30, }, }) logger.AssertField(t, "key", "value")