blob: 97032cab6d6f01d5d2902d8d1b653a017a48abf0 [file] [log] [blame]
// skerr provides functions related to error reporting and stack traces.
package skerr
import (
"fmt"
"runtime"
"strings"
)
// StackTrace identifies a filename (base filename only) and line number.
type StackTrace struct {
File string
Line int
}
func (st *StackTrace) String() string {
return fmt.Sprintf("%s:%d", st.File, st.Line)
}
// CallStack returns a slice of StackTrace representing the current stack trace.
// The lines returned start at the depth specified by startAt: 0 means the call to CallStack,
// 1 means CallStack's caller, 2 means CallStack's caller's caller and so on. height means how
// many lines to include, counting deeper into the stack, with zero meaning to include all stack
// frames. If height is non-zero and there aren't enough stack frames, a dummy value is used
// instead.
//
// Suppose the stacktrace looks like:
// skerr.go:300 <- the call to runtime.Caller in skerr.CallStack
// alpha.go:123
// beta.go:456
// gamma.go:789
// delta.go:123
// main.go: 70
// A typical call may look like skerr.CallStack(6, 1), which returns
// [{File:alpha.go, Line:123}, {File:beta.go, Line:456},...,
//
// {File:main.go, Line:70}, {File:???, Line:1}], omitting the not-helpful reference to
//
// CallStack and padding the response with a dummy value, since the stack was not tall enough to
// show 6 items, starting at the second one.
func CallStack(height, startAt int) []StackTrace {
stack := []StackTrace{}
for i := 0; ; i++ {
_, file, line, ok := runtime.Caller(startAt + i)
if !ok {
if height <= 0 {
break
}
file = "???"
line = 1
} else {
slash := strings.LastIndex(file, "/")
if slash >= 0 {
file = file[slash+1:]
}
}
stack = append(stack, StackTrace{File: file, Line: line})
if height > 0 && len(stack) >= height {
break
}
}
return stack
}
// ErrorWithContext contains an original error with context info and a stack trace. It implements
// the error interface.
type ErrorWithContext struct {
// Wrapped is the original error. Never nil.
Wrapped error
// CallStack captures the caller's stack at the time the ErrorWithContext was created; most recent
// call first.
CallStack []StackTrace
// Context contains additional info from calls to Wrapf. The slice is in order of calls to Wrapf,
// which Error() prints in reverse.
Context []string
}
func (err *ErrorWithContext) Error() string {
var out strings.Builder
for i := len(err.Context) - 1; i >= 0; i-- {
_, _ = out.WriteString(err.Context[i])
_, _ = out.WriteString(": ")
}
_, _ = out.WriteString(err.Wrapped.Error())
if len(err.CallStack) > 0 {
_, _ = out.WriteString(". At")
for _, st := range err.CallStack {
_, _ = fmt.Fprintf(&out, " %s:%d", st.File, st.Line)
}
}
return out.String()
}
// Unwrap allows unwrapping an error as implemented in the `errors` standard
// library.
func (err *ErrorWithContext) Unwrap() error {
return err.Wrapped
}
func tryCast(err error) (*ErrorWithContext, bool) {
if wrapper, ok := err.(*ErrorWithContext); ok {
return wrapper, true
} else {
return nil, false
}
}
func wrap(err error, startAt int) error {
if _, ok := tryCast(err); ok {
return err
}
return &ErrorWithContext{
Wrapped: err,
CallStack: CallStack(0, startAt),
}
}
// Wrap adds stack trace info to err, if not already present. The return value will be of type
// ErrorWithContext. If err is nil, nil is returned instead.
func Wrap(err error) error {
if err == nil {
return nil
}
return wrap(err, 3)
}
// Unwrap returns the original error if err is ErrWithContext, otherwise just returns err.
func Unwrap(err error) error {
if wrapper, ok := tryCast(err); ok {
return wrapper.Wrapped
}
return err
}
// Fmt is equivalent to Wrap(fmt.Errorf(...)).
func Fmt(fmtStr string, args ...interface{}) error {
return wrap(fmt.Errorf(fmtStr, args...), 3)
}
// Wrapf adds context and stack trace info to err. Existing stack trace info will be preserved. The
// return value will be of type ErrorWithContext.
// Example: sklog.Wrapf(err, "When loading %d items from %s", count, url)
//
// If err is nil, nil is returned instead.
func Wrapf(err error, fmtStr string, args ...interface{}) error {
if err == nil {
return nil
}
newContext := fmt.Sprintf(fmtStr, args...)
if wrapper, ok := tryCast(err); ok {
callStack := wrapper.CallStack
if len(callStack) == 0 {
callStack = CallStack(0, 2)
}
return &ErrorWithContext{
Wrapped: wrapper.Wrapped,
CallStack: callStack,
Context: append(wrapper.Context, newContext),
}
} else {
return &ErrorWithContext{
Wrapped: err,
CallStack: CallStack(0, 2),
Context: []string{newContext},
}
}
}