package errors import ( "fmt" "net/http" "path" "strconv" "strings" ) //CustomError implements the following interfaces: // error // github.com/pkg/errors: Cause type CustomError struct { code int 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 HTTPCode(err error) int { if errWithCode, ok := err.(ErrorWithCause); ok { return errWithCode.Code() } return 0 } func (err CustomError) Code() int { //find http error code - returning the smallest code in the stack of causes (excluding code==0) code := err.code if err.cause != nil { if causeWithCode, ok := err.cause.(ErrorWithCause); ok { causeCode := causeWithCode.Code() if code == 0 || (causeCode != 0 && causeCode < code) { code = causeCode } } } return code } 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 err.code != 0 { thisError += fmt.Sprintf(" HTTP(%d:%s)", err.code, http.StatusText(err.code)) } 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 }