Skip to content

Commit

Permalink
Merge pull request #2 from zignd/fix-errors-serialization
Browse files Browse the repository at this point in the history
Fix errors serialization
  • Loading branch information
zignd authored Jul 24, 2024
2 parents 07b2b57 + d00bbcb commit 144a742
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 75 deletions.
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
}
}

0 comments on commit 144a742

Please sign in to comment.