Skip to content

Fix errors serialization #2

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

Merged
merged 3 commits into from
Jul 24, 2024
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ _testmain.go
*.exe
*.test
*.prof

cover.*
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
tests:
go test -v ./...
go test -v -coverprofile=cover.out ./...
go tool cover -html=cover.out -o=cover.html
100 changes: 51 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,69 +97,71 @@ func main() {
Here's the execution of the example:

```
$ go run examples/example1/example1.go
$ go run examples/example1/example1.go
Error logged as a JSON structure using the JSON.MarshalIndent:
{
"message": "failed to complete the transaction on bank_123456",
"data": {
"transactionId": "tx_123456",
"userId": "67890"
[
{
"data": {
"transactionId": "tx_123456",
"userId": "67890"
},
"message": "failed to complete the transaction on bank_123456",
"stack": [
"main.createTransaction @ /root/hack/errors/examples/example1/example1.go:13",
"main.main @ /root/hack/errors/examples/example1/example1.go:52",
"runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194",
"runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"
]
},
"stack": [
"main.createTransaction @ /root/hack/errors/examples/example1/example1.go:13",
"main.main @ /root/hack/errors/examples/example1/example1.go:52",
"runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194",
"runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"
],
"cause": {
"message": "failed to update the database",
{
"data": {
"operation": "update",
"tableName": "transactions"
},
"message": "failed to update the database",
"stack": [
"main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:24",
"main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12",
"main.main @ /root/hack/errors/examples/example1/example1.go:52",
"runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194",
"runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"
],
"cause": {
"message": "connection timeout",
"data": {
"server": "db-server-01",
"timeoutSeconds": 30
},
"stack": [
"main.createConnection @ /root/hack/errors/examples/example1/example1.go:35",
"main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23",
"main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12",
"main.main @ /root/hack/errors/examples/example1/example1.go:52",
"runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194",
"runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"
],
"cause": {
"message": "network instability detected",
"data": {
"network": "internal",
"severity": "high"
},
"stack": [
"main.open @ /root/hack/errors/examples/example1/example1.go:45",
"main.createConnection @ /root/hack/errors/examples/example1/example1.go:34",
"main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23",
"main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12",
"main.main @ /root/hack/errors/examples/example1/example1.go:52",
"runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194",
"runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"
]
}
}
]
},
{
"data": {
"server": "db-server-01",
"timeoutSeconds": 30
},
"message": "connection timeout",
"stack": [
"main.createConnection @ /root/hack/errors/examples/example1/example1.go:35",
"main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23",
"main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12",
"main.main @ /root/hack/errors/examples/example1/example1.go:52",
"runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194",
"runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"
]
},
{
"data": {
"network": "internal",
"severity": "high"
},
"message": "network instability detected",
"stack": [
"main.open @ /root/hack/errors/examples/example1/example1.go:45",
"main.createConnection @ /root/hack/errors/examples/example1/example1.go:34",
"main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23",
"main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12",
"main.main @ /root/hack/errors/examples/example1/example1.go:52",
"runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194",
"runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"
]
}
}
]

Error logged as a JSON structure using the JSON.Marshal:
{"message":"failed to complete the transaction on bank_123456","data":{"transactionId":"tx_123456","userId":"67890"},"stack":["main.createTransaction @ /root/hack/errors/examples/example1/example1.go:13","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"],"cause":{"message":"failed to update the database","data":{"operation":"update","tableName":"transactions"},"stack":["main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:24","main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"],"cause":{"message":"connection timeout","data":{"server":"db-server-01","timeoutSeconds":30},"stack":["main.createConnection @ /root/hack/errors/examples/example1/example1.go:35","main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23","main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"],"cause":{"message":"network instability detected","data":{"network":"internal","severity":"high"},"stack":["main.open @ /root/hack/errors/examples/example1/example1.go:45","main.createConnection @ /root/hack/errors/examples/example1/example1.go:34","main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23","main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"]}}}}
[{"data":{"transactionId":"tx_123456","userId":"67890"},"message":"failed to complete the transaction on bank_123456","stack":["main.createTransaction @ /root/hack/errors/examples/example1/example1.go:13","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"]},{"data":{"operation":"update","tableName":"transactions"},"message":"failed to update the database","stack":["main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:24","main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"]},{"data":{"server":"db-server-01","timeoutSeconds":30},"message":"connection timeout","stack":["main.createConnection @ /root/hack/errors/examples/example1/example1.go:35","main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23","main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"]},{"data":{"network":"internal","severity":"high"},"message":"network instability detected","stack":["main.open @ /root/hack/errors/examples/example1/example1.go:45","main.createConnection @ /root/hack/errors/examples/example1/example1.go:34","main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23","main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"]}]

Error logged using the s format specifier:
failed to complete the transaction on bank_123456: failed to update the database: connection timeout: network instability detected
Expand Down Expand Up @@ -204,8 +206,8 @@ cause:
message:
"network instability detected"
data:
network: internal
severity: high
network: internal
stack:
main.open @ /root/hack/errors/examples/example1/example1.go:45
main.createConnection @ /root/hack/errors/examples/example1/example1.go:34
Expand Down
42 changes: 42 additions & 0 deletions convertion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package errors

import "errors"

// toMapsSlice converts an error and its causes to flat slice of maps where each map represents an error.
func toMapsSlice(err error) []map[string]any {
errMaps := make([]map[string]any, 0)

if err == nil {
return errMaps
}

currentErr := err
for {
errMap, errCause := toMapAndCause(currentErr)
errMaps = append(errMaps, errMap)
if errCause == nil {
break
}
currentErr = errCause
}

return errMaps
}

// toMapAndCause converts an error to a map and extracts the cause.
func toMapAndCause(err error) (map[string]any, error) {
errMap := make(map[string]any)
var errCause error

if e, ok := err.(*Err); ok {
errMap["message"] = e.Message
errMap["data"] = e.Data
errMap["stack"] = e.Stack
errCause = e.Cause
} else {
errMap["message"] = err.Error()
errCause = errors.Unwrap(err)
}

return errMap, errCause
}
31 changes: 6 additions & 25 deletions error.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package errors

import "fmt"
import (
"encoding/json"
"fmt"
)

// Err is the error struct used internally by the package. This type should only be used for type assertions.
type Err struct {
Expand Down Expand Up @@ -31,28 +34,6 @@ func (e Err) Unwrap() error {
return e.Cause
}

// WithStack adds a stack trace to the provided error if it is an Err or *Err.
func WithStack(err error) error {
if e, ok := err.(Err); ok {
e.Stack = callers()
return e
} else if e, ok := err.(*Err); ok {
e.Stack = callers()
return e
} else {
return err
}
}

// WithCause adds a cause to the provided error if it is an Err or *Err.
func WithCause(err error, cause error) error {
if e, ok := err.(Err); ok {
e.Cause = cause
return e
} else if e, ok := err.(*Err); ok {
e.Cause = cause
return e
} else {
return err
}
func (e *Err) MarshalJSON() ([]byte, error) {
return json.Marshal(toMapsSlice(e))
}
75 changes: 75 additions & 0 deletions error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package errors

import (
"encoding/json"
"fmt"
"testing"
)

func TestJSONMarshaling(t *testing.T) {
t.Run("when marshaling a nested chain of errors.Err errors, should marshal the full chain", func(t *testing.T) {
err1 := New("context timeout")
err2 := Wrap(err1, "failed to connect to the database")
err3 := Wrap(err2, "failed to start the server")

b, err := json.MarshalIndent(err3, "", " ")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

var errs []map[string]any
err = json.Unmarshal(b, &errs)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if len(errs) != 3 {
t.Fatalf("unexpected number of errors, got %d, expected %d", len(errs), 3)
}

if fmt.Sprint(errs[0]["message"]) != err3.(*Err).Message {
t.Errorf("unexpected error message, got %q, expected %q", errs[0]["message"], err3.(*Err).Message)
}

if fmt.Sprint(errs[1]["message"]) != err2.(*Err).Message {
t.Errorf("unexpected error message, got %q, expected %q", errs[1]["message"], err2.(*Err).Message)
}

if fmt.Sprint(errs[2]["message"]) != err1.(*Err).Message {
t.Errorf("unexpected error message, got %q, expected %q", errs[2]["message"], err1.(*Err).Message)
}
})

t.Run("when marshaling a chain of errors.Err and standard errors, should marshal the full chain", func(t *testing.T) {
err1 := New("context timeout")
err2 := fmt.Errorf("failed to connect to the database: %w", err1)
err3 := Wrap(err2, "failed to start the server")

b, err := json.MarshalIndent(err3, "", " ")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

var errs []map[string]any
err = json.Unmarshal(b, &errs)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if len(errs) != 3 {
t.Fatalf("unexpected number of errors, got %d, expected %d", len(errs), 3)
}

if fmt.Sprint(errs[0]["message"]) != err3.(*Err).Message {
t.Errorf("unexpected error message, got %q, expected %q", errs[0]["message"], err3.(*Err).Message)
}

if fmt.Sprint(errs[1]["message"]) != err2.Error() {
t.Errorf("unexpected error message, got %q, expected %q", errs[1]["message"], err2.Error())
}

if fmt.Sprint(errs[2]["message"]) != err1.(*Err).Message {
t.Errorf("unexpected error message, got %q, expected %q", errs[2]["message"], err1.(*Err).Message)
}
})
}
26 changes: 26 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,29 @@ func Wrapdf(err error, data Data, format string, args ...any) error {
Cause: err,
}
}

// WithStack adds a stack trace to the provided error if it is an Err or *Err.
func WithStack(err error) error {
if e, ok := err.(Err); ok {
e.Stack = callers()
return e
} else if e, ok := err.(*Err); ok {
e.Stack = callers()
return e
} else {
return err
}
}

// WithCause adds a cause to the provided error if it is an Err or *Err.
func WithCause(err error, cause error) error {
if e, ok := err.(Err); ok {
e.Cause = cause
return e
} else if e, ok := err.(*Err); ok {
e.Cause = cause
return e
} else {
return err
}
}