merr: initial implementation, designed to replace merry and work nicely with mlog, while allowing support for multi-errors and other niceties in the future
This commit is contained in:
parent
33a10a4ac7
commit
56f3e71acd
73
merr/kv.go
Normal file
73
merr/kv.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package merr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// not really used for attributes, but w/e
|
||||||
|
const attrKeyErr attrKey = "err"
|
||||||
|
const attrKeyErrSrc attrKey = "errSrc"
|
||||||
|
|
||||||
|
// KVer implements the mlog.KVer interface. This is defined here to avoid this
|
||||||
|
// package needing to actually import mlog.
|
||||||
|
type KVer struct {
|
||||||
|
kv map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KV implements the mlog.KVer interface.
|
||||||
|
func (kv KVer) KV() map[string]interface{} {
|
||||||
|
return kv.kv
|
||||||
|
}
|
||||||
|
|
||||||
|
// KV returns a KVer which contains all visible values embedded in the error, as
|
||||||
|
// well as the original error string itself. Keys will be turned into strings
|
||||||
|
// using the fmt.Sprint function.
|
||||||
|
//
|
||||||
|
// If any keys conflict then their type information will be included as part of
|
||||||
|
// the key.
|
||||||
|
func KV(e error) KVer {
|
||||||
|
er := wrap(e, false, 1)
|
||||||
|
kvm := make(map[string]interface{}, len(er.attr)+1)
|
||||||
|
|
||||||
|
keys := map[string]interface{}{} // in this case the value is the raw key
|
||||||
|
setKey := func(k, v interface{}) {
|
||||||
|
kStr := fmt.Sprint(k)
|
||||||
|
oldKey := keys[kStr]
|
||||||
|
if oldKey == nil {
|
||||||
|
keys[kStr] = k
|
||||||
|
kvm[kStr] = v
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if oldKey is in kvm, if so it needs to be moved to account for
|
||||||
|
// its type info
|
||||||
|
if oldV, ok := kvm[kStr]; ok {
|
||||||
|
delete(kvm, kStr)
|
||||||
|
kvm[fmt.Sprintf("%T(%s)", oldKey, kStr)] = oldV
|
||||||
|
}
|
||||||
|
|
||||||
|
kvm[fmt.Sprintf("%T(%s)", k, kStr)] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
setKey(attrKeyErr, er.err.Error())
|
||||||
|
for k, v := range er.attr {
|
||||||
|
if !v.visible {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stack, ok := v.val.(Stack)
|
||||||
|
if !ok {
|
||||||
|
setKey(k, v.val)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// compress the stack trace to just be the top-most frame
|
||||||
|
frame := stack.Frame()
|
||||||
|
file, dir := filepath.Base(frame.File), filepath.Dir(frame.File)
|
||||||
|
dir = filepath.Base(dir) // only want the first dirname, ie the pkg name
|
||||||
|
setKey(attrKeyErrSrc, fmt.Sprintf("%s/%s:%d", dir, file, frame.Line))
|
||||||
|
}
|
||||||
|
|
||||||
|
return KVer{kvm}
|
||||||
|
}
|
69
merr/kv_test.go
Normal file
69
merr/kv_test.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package merr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
. "testing"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKV(t *T) {
|
||||||
|
er := New("foo")
|
||||||
|
kv := KV(er).KV()
|
||||||
|
massert.Fatal(t, massert.Comment(
|
||||||
|
massert.All(
|
||||||
|
massert.Len(kv, 2),
|
||||||
|
massert.Equal("foo", kv["err"]),
|
||||||
|
massert.Equal(true,
|
||||||
|
strings.HasPrefix(kv["errSrc"].(string), "merr/kv_test.go:")),
|
||||||
|
),
|
||||||
|
"kv: %#v", kv,
|
||||||
|
))
|
||||||
|
|
||||||
|
type A string
|
||||||
|
type B string
|
||||||
|
type C string
|
||||||
|
|
||||||
|
er = WithValue(er, "invisible", "you can't see me", false)
|
||||||
|
er = WithValue(er, A("k"), "1", true)
|
||||||
|
kv = KV(er).KV()
|
||||||
|
massert.Fatal(t, massert.Comment(
|
||||||
|
massert.All(
|
||||||
|
massert.Len(kv, 3),
|
||||||
|
massert.Equal("foo", kv["err"]),
|
||||||
|
massert.Equal(true,
|
||||||
|
strings.HasPrefix(kv["errSrc"].(string), "merr/kv_test.go:")),
|
||||||
|
massert.Equal("1", kv["k"]),
|
||||||
|
),
|
||||||
|
"kv: %#v", kv,
|
||||||
|
))
|
||||||
|
|
||||||
|
er = WithValue(er, B("k"), "2", true)
|
||||||
|
kv = KV(er).KV()
|
||||||
|
massert.Fatal(t, massert.Comment(
|
||||||
|
massert.All(
|
||||||
|
massert.Len(kv, 4),
|
||||||
|
massert.Equal("foo", kv["err"]),
|
||||||
|
massert.Equal(true,
|
||||||
|
strings.HasPrefix(kv["errSrc"].(string), "merr/kv_test.go:")),
|
||||||
|
massert.Equal("1", kv["merr.A(k)"]),
|
||||||
|
massert.Equal("2", kv["merr.B(k)"]),
|
||||||
|
),
|
||||||
|
"kv: %#v", kv,
|
||||||
|
))
|
||||||
|
|
||||||
|
er = WithValue(er, C("k"), "3", true)
|
||||||
|
kv = KV(er).KV()
|
||||||
|
massert.Fatal(t, massert.Comment(
|
||||||
|
massert.All(
|
||||||
|
massert.Len(kv, 5),
|
||||||
|
massert.Equal("foo", kv["err"]),
|
||||||
|
massert.Equal(true,
|
||||||
|
strings.HasPrefix(kv["errSrc"].(string), "merr/kv_test.go:")),
|
||||||
|
massert.Equal("1", kv["merr.A(k)"]),
|
||||||
|
massert.Equal("2", kv["merr.B(k)"]),
|
||||||
|
massert.Equal("3", kv["merr.C(k)"]),
|
||||||
|
),
|
||||||
|
"kv: %#v", kv,
|
||||||
|
))
|
||||||
|
}
|
166
merr/merr.go
Normal file
166
merr/merr.go
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
// Package merr extends the errors package with features like key-value
|
||||||
|
// attributes for errors, embedded stacktraces, and multi-errors.
|
||||||
|
package merr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var strBuilderPool = sync.Pool{
|
||||||
|
New: func() interface{} { return new(strings.Builder) },
|
||||||
|
}
|
||||||
|
|
||||||
|
func putStrBuilder(sb *strings.Builder) {
|
||||||
|
sb.Reset()
|
||||||
|
strBuilderPool.Put(sb)
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
type val struct {
|
||||||
|
visible bool
|
||||||
|
val interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v val) String() string {
|
||||||
|
return fmt.Sprint(v.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
type err struct {
|
||||||
|
err error
|
||||||
|
attr map[interface{}]val
|
||||||
|
}
|
||||||
|
|
||||||
|
// attr keys internal to this package
|
||||||
|
type attrKey string
|
||||||
|
|
||||||
|
func wrap(e error, cp bool, skip int) *err {
|
||||||
|
er, ok := e.(*err)
|
||||||
|
if !ok {
|
||||||
|
er := &err{err: e, attr: map[interface{}]val{}}
|
||||||
|
if skip >= 0 {
|
||||||
|
setStack(er, skip+1)
|
||||||
|
}
|
||||||
|
return er
|
||||||
|
} else if !cp {
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
er2 := &err{
|
||||||
|
err: er.err,
|
||||||
|
attr: make(map[interface{}]val, len(er.attr)),
|
||||||
|
}
|
||||||
|
for k, v := range er.attr {
|
||||||
|
er2.attr[k] = v
|
||||||
|
}
|
||||||
|
if _, ok := er2.attr[attrKeyStack]; !ok && skip >= 0 {
|
||||||
|
setStack(er, skip+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return er2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap takes in an error and returns one wrapping it in merr's inner type,
|
||||||
|
// which embeds information like the stack trace.
|
||||||
|
func Wrap(e error) error {
|
||||||
|
return wrap(e, false, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new error with the given string as its error string. New
|
||||||
|
// automatically wraps the error in merr's inner type, which embeds information
|
||||||
|
// like the stack trace.
|
||||||
|
func New(str string) error {
|
||||||
|
return wrap(errors.New(str), false, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf is like New, but allows for formatting of the string.
|
||||||
|
func Errorf(str string, args ...interface{}) error {
|
||||||
|
return wrap(fmt.Errorf(str, args...), false, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (er *err) visibleAttrs() [][2]string {
|
||||||
|
out := make([][2]string, 0, len(er.attr))
|
||||||
|
for k, v := range er.attr {
|
||||||
|
if !v.visible {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, [2]string{
|
||||||
|
strings.Trim(fmt.Sprintf("%q", k), `"`),
|
||||||
|
fmt.Sprint(v.val),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(out, func(i, j int) bool {
|
||||||
|
return out[i][0] < out[j][0]
|
||||||
|
})
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (er *err) Error() string {
|
||||||
|
visAttrs := er.visibleAttrs()
|
||||||
|
if len(visAttrs) == 0 {
|
||||||
|
return er.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
sb := strBuilderPool.Get().(*strings.Builder)
|
||||||
|
defer putStrBuilder(sb)
|
||||||
|
|
||||||
|
sb.WriteString(strings.TrimSpace(er.err.Error()))
|
||||||
|
for _, attr := range visAttrs {
|
||||||
|
k, v := strings.TrimSpace(attr[0]), strings.TrimSpace(attr[1])
|
||||||
|
sb.WriteString("\n\t* ")
|
||||||
|
sb.WriteString(k)
|
||||||
|
sb.WriteString(": ")
|
||||||
|
|
||||||
|
// if there's no newlines then print v inline with k
|
||||||
|
if strings.Index(v, "\n") < 0 {
|
||||||
|
sb.WriteString(v)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, vLine := range strings.Split(v, "\n") {
|
||||||
|
sb.WriteString("\n\t\t")
|
||||||
|
sb.WriteString(strings.TrimSpace(vLine))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithValue returns a copy of the original error, automatically wrapping it if
|
||||||
|
// the error is not from merr (see Wrap). The returned error has a value set on
|
||||||
|
// with for the given key.
|
||||||
|
//
|
||||||
|
// visible determines whether or not the value is visible in the output of
|
||||||
|
// Error.
|
||||||
|
func WithValue(e error, k, v interface{}, visible bool) error {
|
||||||
|
er := wrap(e, true, 1)
|
||||||
|
er.attr[k] = val{val: v, visible: visible}
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue returns the value embedded in the error for the given key, or nil if
|
||||||
|
// the error isn't from this package or doesn't have that key embedded.
|
||||||
|
func GetValue(e error, k interface{}) interface{} {
|
||||||
|
return wrap(e, false, -1).attr[k].val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base takes in an error and checks if it is merr's internal error type. If it
|
||||||
|
// is then the underlying error which is being wrapped is returned. If it's not
|
||||||
|
// then the passed in error is returned as-is.
|
||||||
|
func Base(e error) error {
|
||||||
|
if er, ok := e.(*err); ok {
|
||||||
|
return er.err
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal is a shortcut for Base(e1) == Base(e2).
|
||||||
|
func Equal(e1, e2 error) bool {
|
||||||
|
return Base(e1) == Base(e2)
|
||||||
|
}
|
41
merr/merr_test.go
Normal file
41
merr/merr_test.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package merr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
. "testing"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestError(t *T) {
|
||||||
|
er := &err{
|
||||||
|
err: errors.New("foo"),
|
||||||
|
attr: map[interface{}]val{
|
||||||
|
"a": val{val: "aaa aaa\n", visible: true},
|
||||||
|
"b": val{val: "invisible"},
|
||||||
|
"c": val{val: "ccc\nccc\n", visible: true},
|
||||||
|
"d\t": val{val: "weird key but ok", visible: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
str := er.Error()
|
||||||
|
exp := `foo
|
||||||
|
* a: aaa aaa
|
||||||
|
* c:
|
||||||
|
ccc
|
||||||
|
ccc
|
||||||
|
* d\t: weird key but ok`
|
||||||
|
massert.Fatal(t, massert.Equal(exp, str))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBase(t *T) {
|
||||||
|
errFoo, errBar := errors.New("foo"), errors.New("bar")
|
||||||
|
erFoo := Wrap(errFoo)
|
||||||
|
massert.Fatal(t, massert.All(
|
||||||
|
massert.Equal(errFoo, Base(erFoo)),
|
||||||
|
massert.Equal(errBar, Base(errBar)),
|
||||||
|
massert.Not(massert.Equal(errFoo, erFoo)),
|
||||||
|
massert.Not(massert.Equal(errBar, Base(erFoo))),
|
||||||
|
massert.Equal(true, Equal(errFoo, erFoo)),
|
||||||
|
massert.Equal(false, Equal(errBar, erFoo)),
|
||||||
|
))
|
||||||
|
}
|
85
merr/stack.go
Normal file
85
merr/stack.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package merr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaxStackSize indicates the maximum number of stack frames which will be
|
||||||
|
// stored when embedding stack traces in errors.
|
||||||
|
var MaxStackSize = 50
|
||||||
|
|
||||||
|
const attrKeyStack attrKey = "stack"
|
||||||
|
|
||||||
|
// Stack represents a stack trace at a particular point in execution.
|
||||||
|
type Stack []uintptr
|
||||||
|
|
||||||
|
// Frame returns the first frame in the stack.
|
||||||
|
func (s Stack) Frame() runtime.Frame {
|
||||||
|
if len(s) == 0 {
|
||||||
|
panic("cannot call Frame on empty stack")
|
||||||
|
}
|
||||||
|
|
||||||
|
frame, _ := runtime.CallersFrames([]uintptr(s)).Next()
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frames returns all runtime.Frame instances for this stack.
|
||||||
|
func (s Stack) Frames() []runtime.Frame {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]runtime.Frame, 0, len(s))
|
||||||
|
frames := runtime.CallersFrames([]uintptr(s))
|
||||||
|
for {
|
||||||
|
frame, more := frames.Next()
|
||||||
|
out = append(out, frame)
|
||||||
|
if !more {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the full stack trace.
|
||||||
|
func (s Stack) String() string {
|
||||||
|
sb := strBuilderPool.Get().(*strings.Builder)
|
||||||
|
defer putStrBuilder(sb)
|
||||||
|
tw := tabwriter.NewWriter(sb, 0, 4, 4, ' ', 0)
|
||||||
|
for _, frame := range s.Frames() {
|
||||||
|
file := fmt.Sprintf("%s:%d", frame.File, frame.Line)
|
||||||
|
fmt.Fprintf(tw, "%s\t%s\n", file, frame.Function)
|
||||||
|
}
|
||||||
|
if err := tw.Flush(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setStack(er *err, skip int) {
|
||||||
|
stackSlice := make([]uintptr, MaxStackSize)
|
||||||
|
// incr skip once for setStack, and once for runtime.Callers
|
||||||
|
l := runtime.Callers(skip+2, stackSlice)
|
||||||
|
er.attr[attrKeyStack] = val{val: Stack(stackSlice[:l]), visible: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStack returns a copy of the original error, automatically wrapping it if
|
||||||
|
// the error is not from merr (see Wrap). The returned error has the embedded
|
||||||
|
// stacktrace set to the frame calling this function.
|
||||||
|
//
|
||||||
|
// skip can be used to exclude that many frames from the top of the stack.
|
||||||
|
func WithStack(e error, skip int) error {
|
||||||
|
er := wrap(e, true, -1)
|
||||||
|
setStack(er, skip+1)
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStack returns the Stack which was embedded in the error, if the error is
|
||||||
|
// from this package. If not then nil is returned.
|
||||||
|
func GetStack(e error) Stack {
|
||||||
|
stack, _ := wrap(e, false, -1).attr[attrKeyStack].val.(Stack)
|
||||||
|
return stack
|
||||||
|
}
|
47
merr/stack_test.go
Normal file
47
merr/stack_test.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package merr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
. "testing"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStack(t *T) {
|
||||||
|
foo := New("foo")
|
||||||
|
fooStack := GetStack(foo)
|
||||||
|
|
||||||
|
// test Frame
|
||||||
|
frame := fooStack.Frame()
|
||||||
|
massert.Fatal(t, massert.All(
|
||||||
|
massert.Equal(true, strings.Contains(frame.File, "stack_test.go")),
|
||||||
|
massert.Equal(true, strings.Contains(frame.Function, "TestStack")),
|
||||||
|
))
|
||||||
|
|
||||||
|
frames := fooStack.Frames()
|
||||||
|
massert.Fatal(t, massert.Comment(
|
||||||
|
massert.All(
|
||||||
|
massert.Equal(true, len(frames) >= 2),
|
||||||
|
massert.Equal(true, strings.Contains(frames[0].File, "stack_test.go")),
|
||||||
|
massert.Equal(true, strings.Contains(frames[0].Function, "TestStack")),
|
||||||
|
),
|
||||||
|
"fooStack.String():\n%s", fooStack.String(),
|
||||||
|
))
|
||||||
|
|
||||||
|
// test that WithStack works and can be used to skip frames
|
||||||
|
inner := func() {
|
||||||
|
bar := WithStack(foo, 1)
|
||||||
|
barStack := GetStack(bar)
|
||||||
|
frames := barStack.Frames()
|
||||||
|
massert.Fatal(t, massert.Comment(
|
||||||
|
massert.All(
|
||||||
|
massert.Equal(true, len(frames) >= 2),
|
||||||
|
massert.Equal(true, strings.Contains(frames[0].File, "stack_test.go")),
|
||||||
|
massert.Equal(true, strings.Contains(frames[0].Function, "TestStack")),
|
||||||
|
),
|
||||||
|
"barStack.String():\n%s", barStack.String(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
inner()
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user