From a5cd81ec2f933ebf93ca1e921e59851d80127999 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Mon, 2 Nov 2020 14:19:33 +0000 Subject: [PATCH] feat(errors): extract pkg/errors stacktraces --- CHANGELOG.md | 5 +++++ errors/error.go | 16 ++++++++++++++++ errors/error_test.go | 27 +++++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fab900b..9ed43704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## TBD +### Enhancements + +* Extract stacktrace contents on errors wrapped by + [`pkg/errors`](https://github.com/pkg/errors). + ### Bug fixes * Send web framework name with severity reason if set. Previously this value was diff --git a/errors/error.go b/errors/error.go index 4e197164..49dd03b2 100644 --- a/errors/error.go +++ b/errors/error.go @@ -4,6 +4,7 @@ package errors import ( "bytes" "fmt" + "github.com/pkg/errors" "reflect" "runtime" ) @@ -33,6 +34,11 @@ type ErrorWithStackFrames interface { StackFrames() []StackFrame } +type errorWithStack interface { + StackTrace() errors.StackTrace + Error() string +} + // New makes an Error from the given value. If that value is already an // error then it will be used directly, if not, it will be passed to // fmt.Errorf("%v"). The skip parameter indicates how far up the stack @@ -48,6 +54,16 @@ func New(e interface{}, skip int) *Error { Err: e, stack: e.Callers(), } + case errorWithStack: + trace := e.StackTrace() + stack := make([]uintptr, len(trace)) + for i, ptr := range trace { + stack[i] = uintptr(ptr) - 1 + } + return &Error{ + Err: e, + stack: stack, + } case ErrorWithStackFrames: stack := make([]uintptr, len(e.StackFrames())) for i, frame := range e.StackFrames() { diff --git a/errors/error_test.go b/errors/error_test.go index 419de944..99e4ab82 100644 --- a/errors/error_test.go +++ b/errors/error_test.go @@ -4,8 +4,11 @@ import ( "bytes" "fmt" "io" + "runtime" "strings" "testing" + + "github.com/pkg/errors" ) // fixture functions doing work to avoid inlining @@ -44,7 +47,7 @@ func TestParsePanicStack(t *testing.T) { } expected := []StackFrame{ StackFrame{Name: "TestParsePanicStack.func1", File: "errors/error_test.go"}, - StackFrame{Name: "a", File: "errors/error_test.go", LineNumber: 13}, + StackFrame{Name: "a", File: "errors/error_test.go", LineNumber: 16}, } assertStacksMatch(t, expected, err.StackFrames()) }() @@ -88,7 +91,7 @@ func TestSkipWorks(t *testing.T) { } expected := []StackFrame{ - StackFrame{Name: "a", File: "errors/error_test.go", LineNumber: 13}, + StackFrame{Name: "a", File: "errors/error_test.go", LineNumber: 16}, } assertStacksMatch(t, expected, err.StackFrames()) @@ -182,6 +185,26 @@ func TestNewError(t *testing.T) { } } +func TestUnwrapPkgError(t *testing.T) { + _, _, line, ok := runtime.Caller(0) // grab line immediately before error generator + top := func() error { + err := fmt.Errorf("OH NO") + return errors.Wrap(err, "failed") // the correct line for the top of the stack + } + unwrapped := New(top(), 0) // if errors.StackTrace detection fails, this line will be top of stack + if !ok { + t.Fatalf("Something has gone wrong with loading the current stack") + } + if unwrapped.Error() != "failed: OH NO" { + t.Errorf("Failed to unwrap error: %s", unwrapped.Error()) + } + expected := []StackFrame{ + StackFrame{Name: "TestUnwrapPkgError.func1", File: "errors/error_test.go", LineNumber: line + 3}, + StackFrame{Name: "TestUnwrapPkgError", File: "errors/error_test.go", LineNumber: line + 5}, + } + assertStacksMatch(t, expected, unwrapped.StackFrames()) +} + func ExampleErrorf() { for i := 1; i <= 2; i++ { if i%2 == 1 {