Skip to content
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
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# xerrors

<!-- badges -->
[![Go Reference](https://pkg.go.dev/badge/github.com/wood-jp/xerrors.svg)](https://pkg.go.dev/github.com/wood-jp/xerrors)
[![CI](https://github.com/wood-jp/xerrors/actions/workflows/ci.yml/badge.svg)](https://github.com/wood-jp/xerrors/actions/workflows/ci.yml)
[![Coverage Status](https://coveralls.io/repos/github/wood-jp/xerrors/badge.svg?branch=main)](https://coveralls.io/github/wood-jp/xerrors?branch=main)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
Expand Down Expand Up @@ -168,11 +169,42 @@ github.com/wood-jp/xerrors/stacktrace

Captures a stack trace where `Wrap` is called and attaches it to the error. If the error already has a trace, `Wrap` is a no-op.

`StackTrace` implements `slog.LogValuer`, and appears as a `"stacktrace"` array in flat log output. For example:

```go
err = stacktrace.Wrap(err)
var errTest = errors.New("something went wrong")

func c() error {
return stacktrace.Wrap(errclass.WrapAs(errTest, errclass.Transient))
}

func b() error { return c() }
func a() error { return b() }

err := a()
logger.Error("request failed", xerrors.Log(err))
```

Most likely, stack traces are only used in logging. `StackTrace` implements `slog.LogValuer`, and appears as a `"stacktrace"` array in flat log output.
Outputs a log similar to:

```json
{
"level": "ERROR",
"msg": "request failed",
"error": {
"error": "something went wrong",
"error_detail": {
"class": "transient",
"stacktrace": [
{"func": "main.c", "line": 16, "source": "main.go"},
{"func": "main.b", "line": 20, "source": "main.go"},
{"func": "main.a", "line": 24, "source": "main.go"},
{"func": "main.main", "line": 31, "source": "main.go"}
]
}
}
}
```

However, if you wish to directly get at the stack trace data, you can pull the trace back out with `Extract`:

Expand All @@ -192,4 +224,4 @@ This results in all `Wrap` calls becoming no-ops.

## Attribution

*Originally written by [wood-jp](https://github.com/wood-jp) at [Zircuit](https://www.zircuit.com/). Based on [zkr-go-common](https://github.com/zircuit-labs/zkr-go-common-public), MIT license.*
*Originally written by [wood-jp](https://github.com/wood-jp) at [Zircuit](https://www.zircuit.com/). Based on [zkr-go-common-public](https://github.com/zircuit-labs/zkr-go-common-public), MIT license.*
63 changes: 63 additions & 0 deletions errclass/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package errclass_test

import (
"errors"
"fmt"
"log/slog"
"os"

"github.com/wood-jp/xerrors"
"github.com/wood-jp/xerrors/errclass"
)

func newLogger() *slog.Logger {
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && len(groups) == 0 {
return slog.Attr{}
}
return a
},
}))
}

func ExampleWrapAs() {
err := errclass.WrapAs(errors.New("connection timed out"), errclass.Transient)
newLogger().Error("operation failed", xerrors.Log(err))
// Output:
// {"level":"ERROR","msg":"operation failed","error":{"error":"connection timed out","error_detail":{"class":"transient"}}}
}

func ExampleGetClass() {
err := errors.New("disk full")
err = errclass.WrapAs(err, errclass.Persistent)
fmt.Println(errclass.GetClass(err))
// Output:
// persistent
}

func ExampleGetClass_nil() {
fmt.Println(errclass.GetClass(nil))
// Output:
// nil
}

func ExampleWithOnlyMoreSevere() {
err := errors.New("failure")
err = errclass.WrapAs(err, errclass.Persistent)
// Transient is less severe than Persistent, so the class is unchanged.
err = errclass.WrapAs(err, errclass.Transient, errclass.WithOnlyMoreSevere())
fmt.Println(errclass.GetClass(err))
// Output:
// persistent
}

func ExampleWithOnlyUnknown() {
err := errors.New("failure")
err = errclass.WrapAs(err, errclass.Transient)
// Error already has a class, so WithOnlyUnknown leaves it unchanged.
err = errclass.WrapAs(err, errclass.Persistent, errclass.WithOnlyUnknown())
fmt.Println(errclass.GetClass(err))
// Output:
// transient
}
32 changes: 32 additions & 0 deletions errcontext/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package errcontext_test

import (
"errors"
"log/slog"
"os"

"github.com/wood-jp/xerrors"
"github.com/wood-jp/xerrors/errcontext"
)

func newLogger() *slog.Logger {
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && len(groups) == 0 {
return slog.Attr{}
}
return a
},
}))
}

func ExampleAdd() {
err := errors.New("request failed")
err = errcontext.Add(err,
slog.String("user_id", "u123"),
slog.Int("status", 503),
)
newLogger().Error("handler error", xerrors.Log(err))
// Output:
// {"level":"ERROR","msg":"handler error","error":{"error":"request failed","error_detail":{"context":{"status":503,"user_id":"u123"}}}}
}
43 changes: 43 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package xerrors_test

import (
"errors"
"fmt"
"log/slog"
"os"

"github.com/wood-jp/xerrors"
)

func newLogger() *slog.Logger {
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && len(groups) == 0 {
return slog.Attr{}
}
return a
},
}))
}

func ExampleLog() {
type RequestInfo struct {
UserID string
Path string
}

err := xerrors.Extend(RequestInfo{UserID: "u123", Path: "/api/items"}, errors.New("unauthorized"))
newLogger().Error("request failed", xerrors.Log(err))
// Output:
// {"level":"ERROR","msg":"request failed","error":{"error":"unauthorized","error_detail":{"data":{"UserID":"u123","Path":"/api/items"}}}}
}

func ExampleExtract() {
err := errors.New("database error")
err = xerrors.Extend(503, err)

code, ok := xerrors.Extract[int](err)
fmt.Println(ok, code)
// Output:
// true 503
}
50 changes: 50 additions & 0 deletions stacktrace/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package stacktrace_test

import (
"bytes"
"errors"
"fmt"
"log/slog"
"regexp"

"github.com/wood-jp/xerrors"
"github.com/wood-jp/xerrors/stacktrace"
)

func newLogger(buf *bytes.Buffer) *slog.Logger {
return slog.New(slog.NewJSONHandler(buf, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && len(groups) == 0 {
return slog.Attr{}
}
return a
},
}))
}

var (
reSource = regexp.MustCompile(`"source":"[^"]*"`)
reLine = regexp.MustCompile(`"line":\d+`)
)

func normalizeStack(s string) string {
s = reSource.ReplaceAllString(s, `"source":"..."`)
s = reLine.ReplaceAllString(s, `"line":0`)
return s
}

func ExampleWrap() {
var buf bytes.Buffer
err := stacktrace.Wrap(errors.New("something failed"))
newLogger(&buf).Error("operation failed", xerrors.Log(err))
fmt.Print(normalizeStack(buf.String()))
// Output:
// {"level":"ERROR","msg":"operation failed","error":{"error":"something failed","error_detail":{"stacktrace":[{"func":"github.com/wood-jp/xerrors/stacktrace_test.ExampleWrap","line":0,"source":"..."},{"func":"main.main","line":0,"source":"..."}]}}}
}

func ExampleExtract_noStack() {
err := errors.New("plain error")
fmt.Println(stacktrace.Extract(err) == nil)
// Output:
// true
}
5 changes: 4 additions & 1 deletion stacktrace/stacktrace.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// Package stacktrace uses the go runtime to capture stack trace data.
// Package stacktrace captures and formats call stack information using the Go runtime.
// It provides [GetStack] to capture the current program stack, and [Wrap] / [Extract]
// to attach a [StackTrace] to an error. [StackTrace] implements [slog.LogValuer] for
// structured logging integration.
package stacktrace

import (
Expand Down
Loading