diff --git a/errors/README.md b/errors/README.md new file mode 100644 index 0000000000000000000000000000000000000000..3d5830f33b519f19b31f41ddbf1eaa05cd0a64b7 --- /dev/null +++ b/errors/README.md @@ -0,0 +1,153 @@ +# errors + +This is our own implementation of the Golang `error` interface. + +## Why + +Packages tend to use one of the following to return errors: +* Standard go ```"errors"``` package with ```errors.New("...")``` +* Standard go ```"fmt"``` package with ```fmt.Errorf("...")``` +* ```github.com/pkg/errors``` package with ```errors.New("...")``` + +Of those three, the pkg/errors is by far the better option as it includes an error stack. + +One could use pkg/errors, but: +- it does not offer much flexibility in how it displays errors +- its display is cluttered, with the wrong info (e.g. runtime stack and not the error handling stack) +- it does not display the up passing of errors in the stack unless you add the stack each time +- if you do add the stack each time, it dumps many copies of a stack and becomes cluttered + +We write our own version of errors, to: +- always add the stack +- add the correct stack of where we raised/handled the error +- make error handling simpler (Wrapf(err) vs WithMessage(WithStack(err))) +- give us all the flexibility we need + - e.g. give us option to output JSON structured stack + +For lots of detail on this, see comments in error_formats_test.go and run that test with ```go test -v error_formats_test.go``` ... + +## Usage +Get the package into your project: +``` +go get github.com/uafrica/go-utils +``` + +## func New() +Fail with your own simple error message: + +``` +if !authenticated { + return errors.New("not authenticated") +} +``` + +## func Errorf() +Fail with your own error using Printf style, e.g. when you detect an invalid value: + +``` +if limit < 0 { + return errors.Errorf("invalid limit %d<0", limit) +} +``` + +## func Wrapf() +Fail when a called function returned an error: + +``` +if err := db.Select(query); err != nil { + return errors.Wrapf(err, "query failed on user=%s", username) +} +``` + +## Refactoring exiting code +Replace all other errors package imports with this package: +``` +import ( + "github.com/uafrica/go-utils/errors" +) +``` + +Refactor from standad go "errors" package: + +- No change: ```errors.New()``` is still supported in this package + +Refactor from "fmt" package: + +- Replace `errors.Errorf("my message: %v", err)` with `errors.Wrapf(err, "my message")` so that the layers are preserved and not merged into a single text string. + +Refactor from "github.com/pkg/errors": + +- Replace ```errors.WithStack(err)``` with ```errors.New(err)```. +- Replace ```return err``` with ```return errors.Wrapf(err, "some message")``` saying what failed as result of the err +- Replace ```errors.WithMessagef(err, "...", ...)``` with ```return errors.Wrapf(err, "...", ...)``` +- Replace ```errors.WithMessage(err, "...")``` with ```return errors.Wrap(err, "...")``` + +## Formatting + +Report an error with: + +``` +user,err := getUser(userName) +if err != nil { + log.Errorf("failed to get user %s: %+v", username, err) + ... +} +``` + +Select the appropriate format: + +- `%+c` in most cases to write the full compact error with file names and line numbers all on one line. +- `%+v` is the same when you write to console over multipole lines +- `%c` is full error on one line, without reference to source code, i.e. concatenate the error message of all levels e.g. ```A, because B, because C```, which you also get from ```err.Error()```. +- `%v` is full error over multipole lines, without references to source code, e.g. +``` + A, because + B, because + C +``` +- `%s` is just the last error message. + +## JSON error: + +Call ```err.Description()``` to get a struct that can marshal to JSON for a complete dump, e.g.: + +``` + { + "error": "login failed", + "source": { + "package": "github.com/uafrica/go-utils/errors/errors_test", + "file": "errors_test.go", + "line": 18, + "function": "TestErrorFormatting" + }, + "cause": { + "error": "failed to find account", + "source": { + "package": "github.com/uafrica/go-utils/errors/errors_test", + "file": "errors_test.go", + "line": 17, + "function": "TestErrorFormatting" + }, + "cause": { + "error": "query failed", + "source": { + "package": "github.com/uafrica/go-utils/errors/errors_test", + "file": "errors_test.go", + "line": 16, + "function": "TestErrorFormatting" + }, + "cause": { + "error": "you have problem in your SQL near xxx" + } + } + } + } +``` + +## func Caller() + +This function can be used also for logging to determine the caller from the runtime stack. It takes a skip value to skip a few levels of the stack, making it flexible to be called in various wrapper functions, without reporting the wrappers. + +## func Stack() + +This function is similar to Caller() but reports an array of callers, not only one. diff --git a/errors/caller.go b/errors/caller.go new file mode 100644 index 0000000000000000000000000000000000000000..3e29bf3d9d2ed1d3e7a8d25d91f42aee9b18d8ed --- /dev/null +++ b/errors/caller.go @@ -0,0 +1,186 @@ +package errors + +import ( + "fmt" + "io" + "path" + "runtime" + "strings" +) + +type ICaller interface { + fmt.Stringer + Package() string + Function() string + File() string + Line() int +} + +//e.g.: +type Caller struct { + file string + line int + pkgDotFunc string +} + +func (c Caller) Info() CallerInfo { + return CallerInfo{ + Package: c.Package(), + File: path.Base(c.File()), + Line: c.Line(), + Function: c.Function(), + } +} + +type CallerInfo struct { + Package string `json:"package"` + File string `json:"file"` + Line int `json:"line"` + Function string `json:"function"` +} + +func GetCaller(skip int) Caller { + c := Caller{ + file: "", + line: -1, + pkgDotFunc: "", + } + + var pc uintptr + var ok bool + if pc, c.file, c.line, ok = runtime.Caller(skip); !ok { + return c + } + + if fn := runtime.FuncForPC(pc); fn != nil { + c.pkgDotFunc = fn.Name() + } + return c +} //GetCaller() + +func Stack(skip int) []CallerInfo { + if skip < 0 { + skip = 0 + } + if skip >= 20 { + return nil + } + rpc := make([]uintptr, 20-skip) + n := runtime.Callers(skip, rpc[:]) + if n < 1 { + return nil + } + + callers := []CallerInfo{} + i := 0 + for { + frame, ok := runtime.CallersFrames(rpc[i:]).Next() + if !ok { + break + } + fn := runtime.FuncForPC(frame.PC) + c := Caller{ + pkgDotFunc: fn.Name(), + file: frame.File, //path on local file system has no significance over package name + line: frame.Line, + } + callers = append(callers, c.Info()) + i++ + } + + return callers +} + +func (c Caller) String() string { + return fmt.Sprintf("%s(%d)", path.Base(c.file), c.line) +} + +//with Function: "github.com/go-msvc/ms_test.TestCaller" +//return "github.com/go-msvc/ms_test" +func (c Caller) Package() string { + if i := strings.LastIndex(c.pkgDotFunc, "."); i >= 0 { + return c.pkgDotFunc[:i] + } + return "" +} + +//return "github.com/go-msvc/ms_test/my_test.go" +func (c Caller) PackageFile() string { + if i := strings.LastIndex(c.pkgDotFunc, "."); i >= 0 { + return c.pkgDotFunc[:i] + "/" + path.Base(c.file) + } + return "" +} + +//with Function: "github.com/go-msvc/ms_test.TestCaller" +//return "TestCaller" +func (c Caller) Function() string { + if i := strings.LastIndex(c.pkgDotFunc, "."); i >= 0 { + return c.pkgDotFunc[i+1:] + } + return "" +} + +//return full file name on system where code is built... +func (c Caller) File() string { + return c.file +} + +func (c Caller) Line() int { + return c.line +} + +func (caller Caller) Format(f fmt.State, c rune) { + var s string + switch c { + case 's', 'v': + if p, ok := f.Precision(); ok { + s = fmt.Sprintf("%s(%*d)", path.Base(caller.file), p, caller.line) + } else { + s = fmt.Sprintf("%s(%d)", path.Base(caller.file), caller.line) + } + case 'S', 'V': + if p, ok := f.Precision(); ok { + s = fmt.Sprintf("%s/%s(%*d)", caller.Package(), path.Base(caller.file), p, caller.line) + } else { + s = fmt.Sprintf("%s/%s(%d)", caller.Package(), path.Base(caller.file), caller.line) + } + case 'f': + if p, ok := f.Precision(); ok { + s = fmt.Sprintf("%s(%*d)", caller.Function(), p, caller.line) + } else { + s = fmt.Sprintf("%s(%d)", caller.Function(), caller.line) + } + case 'F': + if p, ok := f.Precision(); ok { + s = fmt.Sprintf("%s.%s(%*d)", caller.Package(), caller.Function(), p, caller.line) + } else { + s = fmt.Sprintf("%s.%s(%d)", caller.Package(), caller.Function(), caller.line) + } + } //switch(c) + + if w, ok := f.Width(); ok { + if w < 0 { + s = "" + } else if w <= len(s) { + //right/left truncate + if f.Flag('-') { + s = s[:w] + } else { + s = s[len(s)-w:] + } + } else if w > len(s) { + //pad to fill the space + s = fmt.Sprintf("%*s", w, s) + } + } + io.WriteString(f, s) +} // caller.Format() + +//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:] +// } // funcName() diff --git a/errors/error.go b/errors/error.go new file mode 100644 index 0000000000000000000000000000000000000000..38a9fad390d2b4730f7bc0188330368d2fa4699d --- /dev/null +++ b/errors/error.go @@ -0,0 +1,192 @@ +package errors + +import ( + "fmt" + "path" + "strconv" + "strings" +) + +//Error implements the following interfaces: +// error +// github.com/pkg/errors: Cause +type Error struct { + message string + caller Caller + //stack []CallerInfo + cause error +} + +//implement interface error: +func (err Error) Error() string { + return err.Formatted(FormattingOptions{Causes: true}) +} + +//implement github.com/pkg/errors: Cause +func (err Error) Cause() error { + return err.cause +} + +func (err Error) Description() Description { + info := err.caller.Info() + desc := &Description{ + Message: err.message, + Source: &info, + } + if err.cause != nil { + causeWithStack, ok := err.cause.(*Error) + 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 Error) 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 Error) 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.(*Error); 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 +} diff --git a/errors/error_formats_test.go b/errors/error_formats_test.go new file mode 100644 index 0000000000000000000000000000000000000000..46ca107bc5b8e24a2331513e90a2c5804ba45ded --- /dev/null +++ b/errors/error_formats_test.go @@ -0,0 +1,279 @@ +package errors_test + +import ( + "encoding/json" + "fmt" + "testing" + + go_errors "errors" + + pkg_errors "github.com/pkg/errors" + + uf_errors "github.com/uafrica/go-utils/errors" +) + +//use this struct to define various types of errors to experiment with +type Test struct { + title string + err error +} + +var tests []Test + +//define the errors for testing +//in each case, the main error message is "<this is broken>" +//in some cases that is passed up and wrapped with more messages +//the tests use different combinations of go errors, fmt.Errorf(), github.com/pkg/errors and our own go-utils/errors library +//NOTE: I put all the new calls on new lines to report different line numbers in the stack... +func init() { + tests = []Test{ + //a very simple go "errors.New()" error - does not have a stack + {title: "go.New(...)", err: go_errors.New("<this is broken>")}, + + //a fmt.Errorf() error - does not have a stack + {title: "fmt.Errorf(...)", err: fmt.Errorf("<this is broken>")}, + + //a better github.com/pkg/errors.New() error that includes the call stack + {title: "pkg.New(...)", err: pkg_errors.New("<this is broken>")}, + + //our own error type that always records the caller: + {title: "uafrica.New(...)", err: uf_errors.New("<this is broken>")}, + + //one can use pkg WithStack() to add a stack to an error that does not have a stack + {title: "pkg.WithStack(go.New(...))", err: pkg_errors.WithStack( + go_errors.New("<this is broken>"), + )}, + + //if you are ok with pkg's way to add a stack manually (I am not), + //then you could easily make this mistake to add a stack to an error that already has a stack + //by default pkg then prints two stacks and the rest of the error messages are after the last stack + //making it very hard to figure out what went wrong and where... + {title: "pkg.WithStack(pkg.New())", err: pkg_errors.WithStack( + pkg_errors.New("<this is broken>"), + )}, + + //one can also add a stack with our own errors, + //note that we always want an explanation which is just to enforce good practice: + {title: "uafrica.Wrapf(go.New(...))", err: uf_errors.Wrapf( + go_errors.New("<this is broken>"), + "<it is not working>", + )}, + + //and when we wrap an error that already has a stack, it will not duplicate, + //note that we always want an explanation which is just to enforce good practice: + {title: "uafrica.Wrapf(pkg.New(...))", err: uf_errors.Wrapf( + pkg_errors.New("<this is broken>"), + "<it is not working>", + )}, + + //pkg can also wrap an error with a text explanation, + //but the synctax is annoying as one has to add the stack and the message separately + //(compared to a simple Wrapf() call to imply a stack as well...) + + //we firstly wrap an error that has NO stack: + {title: "pkg.WithMessage(pkg.WithStack(go.New()))", err: pkg_errors.WithMessage( + pkg_errors.WithStack( + go_errors.New("<this is broken>"), + ), + "<it is not working>", + )}, + + //we will typically pass an error up several layers before it is reported + //and the original error may come without a stack, + //and another package might have added a stack using pkg + //then we still add our own explanations on top of that + //in both the following two examples, we have the following stack: + // + // db not open (without a stack) + // query failed (without a stack, using fmt.Errorf("query failed: %v", err)) + // failed to read list (stack added with pkg.WithStack(), then message added with pkg.WithMessage()) + // failed to read users from db (demonstrated with pkg.WithMessage() and our own Wrapf()) + // cannot display list of users (demonstrated with pkg.WithMessage() and our own Wrapf()) + // + //first using only pkg: + {title: "full stack with pkg", err: pkg_errors.WithMessage( + pkg_errors.WithMessage( + pkg_errors.WithStack( //assuming this comes from 3rd party lib e.g. sql or pg - we cannot change this or deeper errors + pkg_errors.WithMessage( + fmt.Errorf( + "query failed: %v", + go_errors.New("db not open"), + ), + "failed to read list", + ), + ), + "failed to read users from db", + ), + "cannot display list of users", + )}, + + //same thing but wrapping at the top with out own messages and stack + {title: "full stack with uafrica", err: uf_errors.Wrapf( + uf_errors.Wrapf( + pkg_errors.WithStack( //assuming this comes from 3rd party lib e.g. sql or pg - we cannot change this or deeper errors + pkg_errors.WithMessage( + fmt.Errorf( + "query failed: %v", + go_errors.New("db not open"), + ), + "failed to read list", + ), + ), + "failed to read users from db", + ), + "cannot display list of users", + )}, + + //============================================================================================================================================ + //in the final full stack case, here is the comparison: + //============================================================================================================================================ + // OUTPUT OF err.Errorf() + // ---------------------- + // + // pkg: cannot display list of users: failed to read users from db: failed to read list: query failed: db not open + // + // uafrica: cannot display list of users, because failed to read users from db, because failed to read list: query failed: db not open + // + // (at the end, uafrica could not insert ", because ..." because the deepest level was just text from fmt.Errorf(...)) + //============================================================================================================================================ + // OUTPUT OF Printf("%s") for simple error + // --------------------------------------- + // + // pkg: cannot display list of users: failed to read users from db: failed to read list: query failed: db not open + // + // uafrica: cannot display list of users + // + // Note that pkg has no simple way to just display the top level error, while we can do it with formatting directive "%s" + //============================================================================================================================================ + // OUTPUT OF Printf("%+v") for full error stack + // -------------------------------------------- + // + // pkg: + // query failed: db not open + // failed to read list + // command-line-arguments_test.init.0 + // /Users/jansemmelink/uafrica/go-utils/errors/other_test.go:96 + // runtime.doInit + // /usr/local/go/src/runtime/proc.go:6498 + // runtime.doInit + // /usr/local/go/src/runtime/proc.go:6475 + // runtime.main + // /usr/local/go/src/runtime/proc.go:238 + // runtime.goexit + // /usr/local/go/src/runtime/asm_arm64.s:1133 + // failed to read users from db + // cannot display list of users + // + // uafrica: + // command-line-arguments_test.init/other_test.go(111): 0() cannot display list of users, because + // command-line-arguments_test.init/other_test.go(112): 0() failed to read users from db, because + // command-line-arguments_test.init.0/other_test.go(113): init.0(): failed to read list: query failed: db not open + // + // Difference: + // - pkg starts with the lowest level then what effects it had (opposite order from how it prints err.Error()) + // uafrica maintains the order of effect then the reason why, which kind of makes more sense + // - pkg write multiple lines for each stack entry, and does not keep the messages to the stack lines + // uafrica put the error message next to the stack line that wrote it + // - pkg wrapping with text, does not include the stack again, so line 94 and 95 that passed the error on, is not mentioned + // uafrica mention each line that handled the error: 111, 112, 113 (called libs in line 114..117 did not include the stack) + // - pkg reports full package and FULL filename (host path where the code was compiled) which is not useful in production + // uafrica reports the package and BASE filename and function name instead. + //============================================================================================================================================ + // Compact Stack: + // -------------- + // In streaming log files we do not want a multi-line display of error messages, so uafrica also supports "%c" and "%+c" formatting to print + // on one line with ';' instead of new lines. The '+' adds the source package, file, line and function name. + // + // "%c" is same as err.Error(): + // + // cannot display list of users, because failed to read users from db, because failed to read list: query failed: db not open + // + // "%+c" is that plus the source references: + // + // command-line-arguments_test.init/error_formats_test.go(111): 0() cannot display list of users, because command-line-arguments_test.init/error_formats_test.go(112): 0() failed to read users from db, because command-line-arguments_test.init.0/error_formats_test.go(113): init.0(): failed to read list: query failed: db not open + // + // "%v" is same as "%c" over multiple lines: + // + // cannot display list of users, because + // failed to read users from db, because + // failed to read list: query failed: db not open + // + // "%+v" is same as "%+c" over multiple lines: (as in previous section where compared with pkg): + // + // command-line-arguments_test.init/error_formats_test.go(111): 0() cannot display list of users, because + // command-line-arguments_test.init/error_formats_test.go(112): 0() failed to read users from db, because + // command-line-arguments_test.init.0/error_formats_test.go(113): init.0(): failed to read list: query failed: db not open + // + //============================================================================================================================================ + // JSON: + // ----- + // Having our own errors package allows us also flexibility to do things like creating a JSON error description. + // This is illustrated in the JSON output: + // {"error":"cannot display list of users","source":{"package":"command-line-arguments_test.init","file":"other_test.go","line":111,"function":"0"}, + // "cause":{"error":"failed to read users from db","source":{"package":"command-line-arguments_test.init","file":"other_test.go","line":112, + // "function":"0"},"cause":{"error":"failed to read list: query failed: db not open","source":{"package":"command-line-arguments_test.init.0", + // "file":"other_test.go","line":113,"function":"init.0"},"cause":{"error":"failed to read list: query failed: db not open","cause":{ + // "error":"query failed: db not open"}}}}} + // Which we can pretty print with jq into a very readable stack: + // + // { + // "error": "cannot display list of users", + // "source": { + // "package": "command-line-arguments_test.init", + // "file": "other_test.go", + // "line": 111, + // "function": "0" + // }, + // "cause": { + // "error": "failed to read users from db", + // "source": { + // "package": "command-line-arguments_test.init", + // "file": "other_test.go", + // "line": 112, + // "function": "0" + // }, + // "cause": { + // "error": "failed to read list: query failed: db not open", + // "source": { + // "package": "command-line-arguments_test.init.0", + // "file": "other_test.go", + // "line": 113, + // "function": "init.0" + // }, + // "cause": { + // "error": "failed to read list: query failed: db not open", + // "cause": { + // "error": "query failed: db not open" + // } + // } + // } + // } + // } + //============================================================================================================================================ + } +} + +func TestErrorFormats(t *testing.T) { + for _, test := range tests { + show(t, test.title, test.err) + } +} + +func show(t *testing.T, title string, err error) { + t.Logf("------------------------------------------------------------------------------------") + t.Logf("-----[ %-70.70s ]-----", title) + t.Logf("------------------------------------------------------------------------------------") + t.Logf("err.Error(): %s", err.Error()) + t.Logf("Printf(%%s): %s", err) + if _, ok := err.(*uf_errors.Error); ok { //compact only supported on our own lib: + t.Logf("Printf(%%c): %c", err) + t.Logf("Printf(%%+c): %+c", err) + } + t.Logf("Printf(%%v): %v", err) + t.Logf("Printf(%%+v): %+v", err) + if e, ok := err.(*uf_errors.Error); ok { //description only supported on our own lib: + desc := e.Description() + jsonDesc, _ := json.Marshal(desc) + t.Logf("JSON: %s", string(jsonDesc)) + } +} diff --git a/errors/errors.go b/errors/errors.go new file mode 100644 index 0000000000000000000000000000000000000000..ca18e57beb4c5c6260a88079e0106f94a2444269 --- /dev/null +++ b/errors/errors.go @@ -0,0 +1,82 @@ +package errors + +import ( + "fmt" + + pkg_errors "github.com/pkg/errors" +) + +//extends default golang error interface +type ErrorWithCause interface { + error + Cause() error +} + +func New(message string) error { + err := &Error{ + message: message, + caller: GetCaller(2), + //stack: Stack(3), + cause: nil, + } + return err +} + +func NError(message string) error { + err := &Error{ + message: message, + caller: GetCaller(2), + //stack: Stack(3), + cause: nil, + } + return err +} + +func Errorf(format string, args ...interface{}) error { + err := &Error{ + message: fmt.Sprintf(format, args...), + caller: GetCaller(2), + //stack: Stack(3), + cause: nil, + } + return err +} + +func Wrapf(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + + wrappedErr := &Error{ + message: fmt.Sprintf(format, args...), + caller: GetCaller(2), + cause: err, + } + + return wrappedErr +} + +func Wrap(err error, msg string) error { + if err == nil { + return nil + } + + wrappedErr := &Error{ + message: msg, + caller: GetCaller(2), + cause: err, + } + + return wrappedErr +} + +type Description struct { + Message string `json:"error"` + Source *CallerInfo `json:"source,omitempty"` + Cause *Description `json:"cause,omitempty"` +} + +//from github.com/pkg/errors: +type stackTracer interface { + StackTrace() pkg_errors.StackTrace +} diff --git a/errors/errors_test.go b/errors/errors_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0c40721b7de024f5957b356ba5b97a0f210e3e4f --- /dev/null +++ b/errors/errors_test.go @@ -0,0 +1,92 @@ +package errors_test + +import ( + "encoding/json" + "testing" + + "github.com/uafrica/go-utils/errors" +) + +func TestErrorFormatting(t *testing.T) { + //pretend this is your error stack + //the original error is created by 3rd party library e.g. pg or mysql and may not have stack info + //but from that point on, we use our errors package which includes stack info... + e1 := errors.Errorf("you have problem in your SQL near xxx") + e2 := errors.Wrapf(e1, "query failed") + e3 := errors.Wrapf(e2, "failed to find account") + e4 := errors.Wrapf(e3, "login failed").(*errors.Error) + + //we can log the error in different ways: + + //use %s for a simple message + //-> "login failed" + t.Logf("%%s: %s", e4) + + //use either Error() or "%v" for concatenated message: + //-> "login failed, because failed to find account, because query failed, because you have problem in your SQL near xxx" + t.Logf("Error(): %s", e4.Error()) + t.Logf("%%v: %v", e4) + + //use %+v for complete error with stack: + //-> "github.com/uafrica/go-utils/errors_test/TestErrorFormatting():errors_test.go(17): login failed, because github.com/uafrica/go-utils/errors_test/TestErrorFormatting():errors_test.go(16): failed to find account, because github.com/uafrica/go-utils/errors_test/TestErrorFormatting():errors_test.go(15): query failed, because you have problem in your SQL near xxx + t.Logf("%%+v: %+v", e4) + + //you may also JSON marshal the error + //-> + jsonError, _ := json.Marshal(e4.Description()) + t.Logf("json: %s", string(jsonError)) + + //using the Description(), one can also verify that the correct + //source references are reported + desc := e4.Description() + if desc.Source == nil || desc.Source.Line != 18 { + t.Fatalf("failed not on line 18") + } + desc = *desc.Cause + if desc.Source == nil || desc.Source.Line != 17 { + t.Fatalf("failed not on line 17") + } + desc = *desc.Cause + if desc.Source == nil || desc.Source.Line != 16 { + t.Fatalf("failed not on line 16") + } +} + +func TestErrorStack(t *testing.T) { + //if some function (e.g. 3rd party) fail without stack, then store the calling stack and append as it is wrapped from there... + err := a(1) + t.Logf("err: %+v", err) +} + +func a(i int) error { + return errors.Wrapf(b(i), "b failed") +} + +func b(i int) error { + return errors.Wrapf(c(i), "c failed") +} + +func c(i int) error { + return errors.Wrapf(d(i), "d failed") +} + +func d(i int) error { + if err := e(i); err != nil { + return errors.Wrapf(err, "e(%d) failed", i) + } + return nil +} + +func e(i int) error { + if err := f(i); err != nil { + return errors.Wrapf(err, "f(%d) failed", i) + } + return nil +} + +func f(i int) error { + if i%2 == 0 { + return nil + } + return errors.Errorf("i=%d is odd", i) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..162ef1d2b3da63ecf6fad0b3fb364d61ef0cc34a --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module github.com/uafrica/go-utils + +go 1.17 + +require ( + github.com/go-pg/pg/v10 v10.10.5 // indirect + github.com/go-pg/zerochecker v0.2.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect + github.com/vmihailenco/bufpool v0.1.11 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.1 // indirect + github.com/vmihailenco/tagparser v0.1.2 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect + golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 // indirect + mellium.im/sasl v0.2.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..48695ebc4cddbe653045f18d849f3292f19839d5 --- /dev/null +++ b/go.sum @@ -0,0 +1,139 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-pg/pg/v10 v10.10.5 h1:RRW8NqxVu4vgzN9k05TT9rM5X+2VQHcIBRLeK9djMBE= +github.com/go-pg/pg/v10 v10.10.5/go.mod h1:EmoJGYErc+stNN/1Jf+o4csXuprjxcRztBnn6cHe38E= +github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= +github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= +github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= +github.com/vmihailenco/msgpack/v5 v5.3.1 h1:0i85a4dsZh8mC//wmyyTEzidDLPQfQAxZIOLtafGbFY= +github.com/vmihailenco/msgpack/v5 v5.3.1/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= +github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 h1:iGu644GcxtEcrInvDsQRCwJjtCIOlT2V7IRt6ah2Whw= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w= +mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ=