From 053df9447f73cdaa0cf6cff4591b3545d3c379da Mon Sep 17 00:00:00 2001 From: Masayoshi Mizutani Date: Sat, 6 Apr 2024 11:37:35 +0900 Subject: [PATCH] Add examples and update README --- README.md | 207 +++++++++++++++++++--------- examples/errors_is/main.go | 27 ++++ examples/logging/main.go | 12 +- examples/stacktrace/go.mod | 15 -- examples/stacktrace/go.sum | 49 ------- examples/stacktrace/main.go | 23 ---- examples/stacktrace_extract/main.go | 27 ++++ examples/stacktrace_print/main.go | 21 +++ examples/variables/main.go | 37 +++++ 9 files changed, 263 insertions(+), 155 deletions(-) create mode 100644 examples/errors_is/main.go delete mode 100644 examples/stacktrace/go.mod delete mode 100644 examples/stacktrace/go.sum delete mode 100644 examples/stacktrace/main.go create mode 100644 examples/stacktrace_extract/main.go create mode 100644 examples/stacktrace_print/main.go create mode 100644 examples/variables/main.go diff --git a/README.md b/README.md index 4714dc5..6744d63 100644 --- a/README.md +++ b/README.md @@ -4,85 +4,158 @@ Package `goerr` provides more contextual error handling in Go. ## Features -- Adding contextual variables to error by `With(key, value)` -- Records stacktrace (Compatible with `github.com/pkg/errors`) -- Supports `errors.Is` to identify error and `errors.As` to unwrap error -- Provides structured stacktrace and contextual variables +`goerr` provides the following features: + +- Stack traces + - Compatible with `github.com/pkg/errors`. + - Structured stack traces with `goerr.Stack` is available. +- Contextual variables to errors using `With(key, value)`. +- `errors.Is` to identify errors and `errors.As` to unwrap errors. +- `slog.LogValuer` interface to output structured logs with `slog`. ## Usage -### Extract values +### Stack trace + +`goerr` records stack trace when creating an error. The format is compatible with `github.com/pkg/errors` and it can be used for [sentry.io](https://sentry.io), etc. -Example code is [here](examples/basic/main.go) ```go -package main +func someAction(fname string) error { + if _, err := os.Open(fname); err != nil { + return goerr.Wrap(err, "failed to open file") + } + return nil +} -import ( - "errors" - "log" +func main() { + if err := someAction("no_such_file.txt"); err != nil { + log.Fatalf("%+v", err) + } +} +``` - "github.com/m-mizutani/goerr" -) +Output: +``` +2024/04/06 10:30:27 failed to open file: open no_such_file.txt: no such file or directory +main.someAction + /Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/stacktrace_print/main.go:12 +main.main + /Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/stacktrace_print/main.go:18 +runtime.main + /usr/local/go/src/runtime/proc.go:271 +runtime.goexit + /usr/local/go/src/runtime/asm_arm64.s:1222 +exit status 1 +``` -func someAction(input string) error { - if input != "OK" { - return goerr.New("input is not OK").With("input", input).With("time", time.Now()) +You can not only print the stack trace, but also extract the stack trace by `goerr.Unwrap(err).Stacks()`. + +```go +if err := someAction("no_such_file.txt"); err != nil { + // NOTE: `errors.Unwrap` also works + if goErr := goerr.Unwrap(err); goErr != nil { + for i, st := range goErr.Stacks() { + log.Printf("%d: %v\n", i, st) + } + } + log.Fatal(err) +} +``` + +`Stacks()` returns a slice of `goerr.Stack` struct, which contains `Func`, `File`, and `Line`. + +``` +2024/04/06 10:35:30 0: &{main.someAction /Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/stacktrace_extract/main.go 12} +2024/04/06 10:35:30 1: &{main.main /Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/stacktrace_extract/main.go 18} +2024/04/06 10:35:30 2: &{runtime.main /usr/local/go/src/runtime/proc.go 271} +2024/04/06 10:35:30 3: &{runtime.goexit /usr/local/go/src/runtime/asm_arm64.s 1222} +2024/04/06 10:35:30 failed to open file: open no_such_file.txt: no such file or directory +exit status 1 +``` + +### Add/Extract contextual variables + +`goerr` provides `With(key, value)` to add contextual variables to errors. + +```go +func firstFunc(label string) error { + _, err := secondFunc(label+".txt", os.O_RDONLY, 0644) + if err != nil { + return goerr.Wrap(err, "failed to call secondFunc").With("label", label) } + // ..... return nil } +func secondFunc(fname string, flag int, perm fs.FileMode) ([]byte, error) { + if _, err := os.OpenFile(fname, flag, perm); err != nil { + return nil, goerr.Wrap(err).With("fname", fname).With("flag", flag) + } + // ..... + return nil, nil +} + func main() { - if err := someAction("ng"); err != nil { - var goErr *goerr.Error - if errors.As(err, &goErr) { + if err := firstFunc("no_such_file"); err != nil { + if goErr := goerr.Unwrap(err); goErr != nil { for k, v := range goErr.Values() { - log.Printf("%s = %v\n", k, v) + log.Printf("var: %s => %v\n", k, v) } } - log.Fatalf("Error: %+v\n", err) + log.Fatalf("msg: %s", err) } } ``` Output: ``` -2022/05/14 10:28:08 input = ng -2022/05/14 10:28:08 time = 2022-05-14 10:28:08.452831 +0900 JST m=+0.000483668 -2022/05/14 10:28:08 Error: input is not OK -main.someAction - /xxx/goerr/examples/basic/main.go:13 -main.main - /xxx/goerr/examples/basic/main.go:19 -runtime.main - /usr/local/go/src/runtime/proc.go:250 -runtime.goexit - /usr/local/go/src/runtime/asm_arm64.s:1259 +2024/04/06 11:10:14 var: fname => no_such_file.txt +2024/04/06 11:10:14 var: flag => 0 +2024/04/06 11:10:14 var: label => no_such_file +2024/04/06 11:10:14 msg: failed to call secondFunc: : open no_such_file.txt: no such file or directory exit status 1 ``` -### Extract stack trace +If you want to send the error to sentry.io, you can extract the contextual variables by `goErr.Values()` and set them to the scope. ```go -import ( - "github.com/m-mizutani/goerr" + // Sending error to Sentry + hub := sentry.CurrentHub().Clone() + hub.ConfigureScope(func(scope *sentry.Scope) { + if goErr := goerr.Unwrap(err); goErr != nil { + for k, v := range goErr.Values() { + scope.SetExtra(k, v) + } + } + }) + evID := hub.CaptureException(err) +``` - "github.com/rs/zerolog/log" -) +### Structured logging + +`goerr` provides `slog.LogValuer` interface to output structured logs with `slog`. It can be used to output not only the error message but also the stack trace and contextual variables. Additionally, unwrapped errors can be output recursively. + +```go +var errRuntime = errors.New("runtime error") func someAction(input string) error { + if err := validate(input); err != nil { + return goerr.Wrap(err, "failed validation") + } + return nil +} + +func validate(input string) error { if input != "OK" { - return goerr.New("input is not OK").With("input", input) + return goerr.Wrap(errRuntime, "invalid input").With("input", input) } return nil } func main() { + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) if err := someAction("ng"); err != nil { - // Same with errors.As extraction - if goErr := goerr.Unwrap(err); goErr != nil { - stacks := goErr.Stacks() - log.Info().Interface("stackTrace", stacks).Msg("Show stacktrace") - } + logger.Error("fail someAction", slog.Any("error", err)) } } ``` @@ -90,35 +163,37 @@ func main() { Output: ```json { - "level": "info", - "stackTrace": [ - { - "func": "main.someAction", - "file": "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/stacktrace/main.go", - "line": 11 - }, - { - "func": "main.main", - "file": "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/stacktrace/main.go", - "line": 17 - }, - { - "func": "runtime.main", - "file": "/usr/local/go/src/runtime/proc.go", - "line": 250 - }, - { - "func": "runtime.goexit", - "file": "/usr/local/go/src/runtime/asm_arm64.s", - "line": 1259 + "time": "2024-04-06T11:32:40.350873+09:00", + "level": "ERROR", + "msg": "fail someAction", + "error": { + "message": "failed validation", + "stacktrace": [ + "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/logging/main.go:16 main.someAction", + "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/logging/main.go:30 main.main", + "/usr/local/go/src/runtime/proc.go:271 runtime.main", + "/usr/local/go/src/runtime/asm_arm64.s:1222 runtime.goexit" + ], + "cause": { + "message": "invalid input", + "values": { + "input": "ng" + }, + "stacktrace": [ + "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/logging/main.go:23 main.validate", + "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/logging/main.go:15 main.someAction", + "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/logging/main.go:30 main.main", + "/usr/local/go/src/runtime/proc.go:271 runtime.main", + "/usr/local/go/src/runtime/asm_arm64.s:1222 runtime.goexit" + ], + "cause": "runtime error" } - ], - "time": "2022-05-14T10:50:42+09:00", - "message": "Show stacktrace" + } } ``` + ## License The 2-Clause BSD License. See [LICENSE](LICENSE) for more detail. diff --git a/examples/errors_is/main.go b/examples/errors_is/main.go new file mode 100644 index 0000000..91111e0 --- /dev/null +++ b/examples/errors_is/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "errors" + "log" + + "github.com/m-mizutani/goerr" +) + +var errInvalidInput = errors.New("invalid input") + +func someAction(input string) error { + if input != "OK" { + return goerr.Wrap(errInvalidInput, "input is not OK").With("input", input) + } + // ..... + return nil +} + +func main() { + if err := someAction("ng"); err != nil { + switch { + case errors.Is(err, errInvalidInput): + log.Printf("It's user's bad: %v\n", err) + } + } +} diff --git a/examples/logging/main.go b/examples/logging/main.go index 9470c3c..9a0d6ca 100644 --- a/examples/logging/main.go +++ b/examples/logging/main.go @@ -1,6 +1,7 @@ package main import ( + "errors" "os" "log/slog" @@ -8,11 +9,18 @@ import ( "github.com/m-mizutani/goerr" ) -var runtimeError = goerr.New("runtime error") +var errRuntime = errors.New("runtime error") func someAction(input string) error { + if err := validate(input); err != nil { + return goerr.Wrap(err, "failed validation") + } + return nil +} + +func validate(input string) error { if input != "OK" { - return goerr.Wrap(runtimeError, "input is not OK").With("input", input) + return goerr.Wrap(errRuntime, "invalid input").With("input", input) } return nil } diff --git a/examples/stacktrace/go.mod b/examples/stacktrace/go.mod deleted file mode 100644 index 3cf17cf..0000000 --- a/examples/stacktrace/go.mod +++ /dev/null @@ -1,15 +0,0 @@ -module github.com/m-mizutani/goerr/examples/stacktrace - -go 1.18 - -require ( - github.com/m-mizutani/goerr v0.1.7 - github.com/rs/zerolog v1.27.0 -) - -require ( - github.com/google/uuid v1.3.0 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect -) diff --git a/examples/stacktrace/go.sum b/examples/stacktrace/go.sum deleted file mode 100644 index 7b7df1e..0000000 --- a/examples/stacktrace/go.sum +++ /dev/null @@ -1,49 +0,0 @@ -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/m-mizutani/goerr v0.1.6 h1:D+BN21CeuER4Y3oo/0ZnLUDautlKZ7gZVZ2FglE73E4= -github.com/m-mizutani/goerr v0.1.6/go.mod h1:fQkXuu06q+oLlp4FkbiTFzI/N/+WAK/Mz1W5kPZ6yzs= -github.com/m-mizutani/goerr v0.1.7 h1:T0k3nUVQPBXkLrhE+ZmzJP87KVa9Eb6PAWPVSO6bRYU= -github.com/m-mizutani/goerr v0.1.7/go.mod h1:fQkXuu06q+oLlp4FkbiTFzI/N/+WAK/Mz1W5kPZ6yzs= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= -github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= -github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= -github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/examples/stacktrace/main.go b/examples/stacktrace/main.go deleted file mode 100644 index f9a7b58..0000000 --- a/examples/stacktrace/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "github.com/m-mizutani/goerr" - - "github.com/rs/zerolog/log" -) - -func someAction(input string) error { - if input != "OK" { - return goerr.New("input is not OK").With("input", input) - } - return nil -} - -func main() { - if err := someAction("ng"); err != nil { - if goErr := goerr.Unwrap(err); goErr != nil { - stacks := goErr.Stacks() - log.Info().Interface("stackTrace", stacks).Msg("Show stacktrace") - } - } -} diff --git a/examples/stacktrace_extract/main.go b/examples/stacktrace_extract/main.go new file mode 100644 index 0000000..9bb459c --- /dev/null +++ b/examples/stacktrace_extract/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "log" + "os" + + "github.com/m-mizutani/goerr" +) + +func someAction(fname string) error { + if _, err := os.Open(fname); err != nil { + return goerr.Wrap(err, "failed to open file") + } + return nil +} + +func main() { + if err := someAction("no_such_file.txt"); err != nil { + // NOTE: `errors.Unwrap` also works + if goErr := goerr.Unwrap(err); goErr != nil { + for i, st := range goErr.Stacks() { + log.Printf("%d: %v\n", i, st) + } + } + log.Fatal(err) + } +} diff --git a/examples/stacktrace_print/main.go b/examples/stacktrace_print/main.go new file mode 100644 index 0000000..3ffb419 --- /dev/null +++ b/examples/stacktrace_print/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "log" + "os" + + "github.com/m-mizutani/goerr" +) + +func someAction(fname string) error { + if _, err := os.Open(fname); err != nil { + return goerr.Wrap(err, "failed to open file") + } + return nil +} + +func main() { + if err := someAction("no_such_file.txt"); err != nil { + log.Fatalf("%+v", err) + } +} diff --git a/examples/variables/main.go b/examples/variables/main.go new file mode 100644 index 0000000..5b50037 --- /dev/null +++ b/examples/variables/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "io/fs" + "log" + "os" + + "github.com/m-mizutani/goerr" +) + +func firstFunc(label string) error { + _, err := secondFunc(label+".txt", os.O_RDONLY, 0644) + if err != nil { + return goerr.Wrap(err, "failed to call secondFunc").With("label", label) + } + // ..... + return nil +} + +func secondFunc(fname string, flag int, perm fs.FileMode) ([]byte, error) { + if _, err := os.OpenFile(fname, flag, perm); err != nil { + return nil, goerr.Wrap(err).With("fname", fname).With("flag", flag) + } + // ..... + return nil, nil +} + +func main() { + if err := firstFunc("no_such_file"); err != nil { + if goErr := goerr.Unwrap(err); goErr != nil { + for k, v := range goErr.Values() { + log.Printf("var: %s => %v\n", k, v) + } + } + log.Fatalf("msg: %s", err) + } +}