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