diff --git a/.golangci.yml b/.golangci.yml index fbffc28..cdb7a37 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -43,4 +43,7 @@ issues: - funlen - dupl path: "_test.go" + - linters: + - errcheck + source: "_, _ = io.WriteString\\(s" # stack.go fmt functions diff --git a/error.go b/error.go index d8e849d..78a1663 100644 --- a/error.go +++ b/error.go @@ -73,14 +73,9 @@ func WrapError(ctx context.Context, err error, message string, keysAndValues ... } } - se, ok := newError(ctx, err, keysAndValues...) - if ok { - return wrappedStructuredError{ - structuredError: se, - } + return wrappedStructuredError{ + structuredError: newError(ctx, err, keysAndValues...), } - - return err } // NewError creates error with optional structured data. @@ -88,14 +83,7 @@ func WrapError(ctx context.Context, err error, message string, keysAndValues ... // LogError fields from context are also added to error structured data. func NewError(ctx context.Context, message string, keysAndValues ...interface{}) error { // nolint:goerr113 // Static errors can be used with WrapError. - err := errors.New(message) - - se, ok := newError(ctx, err, keysAndValues...) - if ok { - return se - } - - return err + return newError(ctx, errors.New(message), keysAndValues...) } // Tuples is a slice of keys and values, e.g. {"key1", 1, "key2", "val2"}. @@ -104,6 +92,7 @@ type Tuples []interface{} type structuredError struct { err error keysAndValues Tuples + *stack } type wrappedStructuredError struct { @@ -182,7 +171,7 @@ func (se structuredError) Tuples() []interface{} { return se.keysAndValues[0:len(se.keysAndValues):len(se.keysAndValues)] } -func newError(ctx context.Context, err error, keysAndValues ...interface{}) (structuredError, bool) { +func newError(ctx context.Context, err error, keysAndValues ...interface{}) structuredError { var ( se StructuredError kv = keysAndValues @@ -204,14 +193,11 @@ func newError(ctx context.Context, err error, keysAndValues ...interface{}) (str kv = append(kv, ctxFields...) } - if len(kv) > 1 { - return structuredError{ - err: err, - keysAndValues: kv, - }, true + return structuredError{ + err: err, + keysAndValues: kv, + stack: callers(), } - - return structuredError{}, false } var ( diff --git a/go.sum b/go.sum index c8008e5..fa948bd 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -github.com/bool64/dev v0.1.0/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg= github.com/bool64/dev v0.1.25/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU= github.com/bool64/dev v0.1.26 h1:9RppeANjTKsF0ZEROkgh0z8qKTvpNeVmKnz0uuCAkS4= github.com/bool64/dev v0.1.26/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU= @@ -9,8 +8,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/swaggest/usecase v0.0.0-20200928062416-27f47131b0f8 h1:XGJJai6ngqYpy/cTMvGreiO+jradIn1uq7Q5hY2OCF4= -github.com/swaggest/usecase v0.0.0-20200928062416-27f47131b0f8/go.mod h1:rcngDv7OaBXZyEXdEtimcDeNon7sq3iqLm9hxT06s3c= github.com/swaggest/usecase v0.1.5 h1:xMDWXnYGysVaF2f3ZnmDsn2FlZ8fd3FJD+O+8wl4aNQ= github.com/swaggest/usecase v0.1.5/go.mod h1:uubX4ZbjQK1Bnl0xX9hOYpb/IUiSoVKk/yQImawbNMU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/stack.go b/stack.go new file mode 100644 index 0000000..da00dcf --- /dev/null +++ b/stack.go @@ -0,0 +1,211 @@ +/* +Copyright (c) 2015, Dave Cheney +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// This file is derived from https://github.com/pkg/errors/blob/v0.9.1/stack.go. + +package ctxd + +import ( + "encoding" + "fmt" + "io" + "path" + "runtime" + "strconv" + "strings" +) + +// Frame represents a program counter inside a stack frame. +// For historical reasons if Frame is interpreted as a uintptr +// its value represents the program counter + 1. +type Frame uintptr + +// pc returns the program counter for this frame; +// multiple frames may have the same PC value. +func (f Frame) pc() uintptr { return uintptr(f) - 1 } + +const unknown = "unknown" + +// file returns the full path to the file that contains the +// function for this Frame's pc. +func (f Frame) file() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return unknown + } + + file, _ := fn.FileLine(f.pc()) + + return file +} + +// line returns the line number of source code of the +// function for this Frame's pc. +func (f Frame) line() int { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return 0 + } + + _, line := fn.FileLine(f.pc()) + + return line +} + +// name returns the name of this function, if known. +func (f Frame) name() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return unknown + } + + return fn.Name() +} + +// Format formats the frame according to the fmt.Formatter interface. +// +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+s function name and path of source file relative to the compile time +// GOPATH separated by \n\t (\n\t) +// %+v equivalent to %+s:%d +func (f Frame) Format(s fmt.State, verb rune) { + switch verb { + case 's': + switch { + case s.Flag('+'): + _, _ = io.WriteString(s, f.name()) + _, _ = io.WriteString(s, "\n\t") + _, _ = io.WriteString(s, f.file()) + default: + _, _ = io.WriteString(s, path.Base(f.file())) + } + case 'd': + _, _ = io.WriteString(s, strconv.Itoa(f.line())) + case 'n': + _, _ = io.WriteString(s, funcname(f.name())) + case 'v': + f.Format(s, 's') + _, _ = io.WriteString(s, ":") + f.Format(s, 'd') + } +} + +var _ encoding.TextMarshaler = Frame(0) + +// MarshalText formats a stacktrace Frame as a text string. The output is the +// same as that of fmt.Sprintf("%+v", f), but without newlines or tabs. +func (f Frame) MarshalText() ([]byte, error) { + name := f.name() + if name == unknown { + return []byte(name), nil + } + + return []byte(fmt.Sprintf("%s %s:%d", name, f.file(), f.line())), nil +} + +// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). +type StackTrace []Frame + +// Format formats the stack of Frames according to the fmt.Formatter interface. +// +// %s lists source files for each Frame in the stack +// %v lists the source file and line number for each Frame in the stack +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+v Prints filename, function, and line number for each Frame in the stack. +func (st StackTrace) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case s.Flag('+'): + for _, f := range st { + _, _ = io.WriteString(s, "\n") + f.Format(s, verb) + } + case s.Flag('#'): + _, _ = fmt.Fprintf(s, "%#v", []Frame(st)) + default: + st.formatSlice(s, verb) + } + case 's': + st.formatSlice(s, verb) + } +} + +// formatSlice will format this StackTrace into the given buffer as a slice of +// Frame, only valid when called with '%s' or '%v'. +func (st StackTrace) formatSlice(s fmt.State, verb rune) { + _, _ = io.WriteString(s, "[") + + for i, f := range st { + if i > 0 { + _, _ = io.WriteString(s, " ") + } + + f.Format(s, verb) + } + + _, _ = io.WriteString(s, "]") +} + +// stack represents a stack of program counters. +type stack []uintptr + +func (s *stack) StackTrace() StackTrace { + f := make([]Frame, len(*s)) + for i := 0; i < len(f); i++ { + f[i] = Frame((*s)[i]) + } + + return f +} + +func callers() *stack { + const depth = 32 + + var pcs [depth]uintptr + n := runtime.Callers(4, pcs[:]) + + var st stack = pcs[0:n] + + return &st +} + +// funcname removes the path prefix component of a function's name reported by func.Name(). +func funcname(name string) string { + i := strings.LastIndex(name, "/") + name = name[i+1:] + i = strings.Index(name, ".") + + return name[i+1:] +} diff --git a/stack_test.go b/stack_test.go new file mode 100644 index 0000000..403d258 --- /dev/null +++ b/stack_test.go @@ -0,0 +1,24 @@ +package ctxd_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/bool64/ctxd" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStack_StackTrace(t *testing.T) { + err := ctxd.NewError(context.Background(), "failed") + + var e interface { + StackTrace() ctxd.StackTrace + } + + assert.True(t, errors.As(err, &e)) + require.NotNil(t, e) + assert.Equal(t, "stack_test.go:15", fmt.Sprintf("%v", e.StackTrace()[0])) +}