Select Git revision
-
Christel Loftus authoredChristel Loftus authored
error.go 4.69 KiB
package errors
import (
"fmt"
"path"
"strconv"
"strings"
)
//CustomError implements the following interfaces:
// error
// github.com/pkg/errors: Cause
type CustomError struct {
message string
caller Caller
cause error
}
//implement interface error:
func (err CustomError) Error() string {
return err.Formatted(FormattingOptions{Causes: true})
}
//implement github.com/pkg/errors: Cause
func (err CustomError) Cause() error {
return err.cause
}
func (err CustomError) Description() Description {
info := err.caller.Info()
desc := &Description{
Message: err.message,
Source: &info,
}
if err.cause != nil {
causeWithStack, ok := err.cause.(*CustomError)
if !ok {
//external cause without our stack
//if github.com/pkg/errors, we can still get caller reference
desc.Cause = pkgDescription(0, err.cause)
} else {
//cause has our stack
causeDesription := causeWithStack.Description()
desc.Cause = &causeDesription
}
}
return *desc
}
func (err CustomError) Format(s fmt.State, v rune) {
s.Write([]byte(
err.Formatted(
FormattingOptions{
Causes: (v == 'v' || v == 'c'), //include causes for %c and %v, s is only this error
NewLines: v == 'v', //use newlines only on v, c is more compact on single line
Source: s.Flag('+'), //include source references when %+v or %+c
},
),
))
}
type FormattingOptions struct {
Causes bool
NewLines bool
Source bool
}
func (err CustomError) Formatted(opts FormattingOptions) string {
//start with this error
thisError := ""
if opts.Source {
thisError += fmt.Sprintf("%s/%s(%d): %s() ",
err.caller.Package(),
path.Base(err.caller.File()),
err.caller.Line(),
err.caller.Function(),
)
}
thisError += err.message
if !opts.Causes {
return thisError
}
if err.cause == nil {
return thisError
}
sep := ", because"
if opts.NewLines {
sep += "\n\t"
} else {
sep += " "
}
if causeWithStack, ok := err.cause.(*CustomError); ok {
return thisError + sep + causeWithStack.Formatted(opts)
}
//this level does not have our own stack, but we can detect github.com/pkg/errors stack, then:
//note: do not use fmt.Sprintf("%+v", err.cause) because it will repeat the stack if multiple were captured
//note: do not use err.cause.Error() because it does not include any stack
//instead, get first layer that implements it and log it
return thisError + pkgStack(err.cause, opts)
}
func pkgStack(err error, opts FormattingOptions) string {
s := ""
e := err
for e != nil {
if errWithStackTracer, ok := e.(stackTracer); ok {
st := errWithStackTracer.StackTrace()
for _, f := range st {
//source := fmt.Sprintf("%n %s %d", f, f, f) - this shows only package name, not fully qualified package name :-(
sources := strings.SplitN(fmt.Sprintf("%+s(%d)\n%n", f, f, f), "\n", 3)
//package <-- sources[0]
//full filename:line <-- source[1]
//function name <-- source[2]
//skip runtime packages
if strings.HasPrefix(sources[0], "runtime") {
break
}
if opts.NewLines {
s += ", because \n\t"
} else {
s += ", because "
}
if opts.Source {
s += sources[0] + "/" + path.Base(sources[1]) + ": " + sources[2] + "(): "
}
s += fmt.Sprintf("%s", e)
}
return s
}
errWithCause, ok := e.(ErrorWithCause)
if !ok {
break
}
e = errWithCause.Cause()
}
return s
}
func pkgDescription(level int, err error) *Description {
desc := &Description{
Message: fmt.Sprintf("%s", err),
Source: nil,
Cause: nil,
}
//recursively fill causes first
if errWithCause, ok := err.(ErrorWithCause); ok {
causeErr := errWithCause.Cause()
if causeErr != nil {
desc.Cause = pkgDescription(level+1, causeErr)
}
}
if errWithStackTracer, ok := err.(stackTracer); ok {
tempDesc := desc
st := errWithStackTracer.StackTrace()
for _, f := range st {
if tempDesc == nil {
break //stop if no more causes populated
}
//source := fmt.Sprintf("%n %s %d", f, f, f) - this shows only package name, not fully qualified package name :-(
sources := strings.SplitN(fmt.Sprintf("%+s\n%d\n%n", f, f, f), "\n", 4)
//package <-- sources[0]
//full filename <-- source[1]
//line <-- source[2]
//function name <-- source[3]
//stop if entering runtime part of the stack
if strings.HasPrefix(sources[0], "runtime") {
break
}
tempDesc.Source = &CallerInfo{
Package: sources[0],
File: path.Base(sources[1]),
//Line: sources[2],
Function: sources[3],
}
if i64, err := strconv.ParseInt(sources[2], 10, 64); err == nil {
tempDesc.Source.Line = int(i64)
}
tempDesc = tempDesc.Cause
} //for each stack level
} //if has stack
return desc
}