Index: common/errors/annotate.go |
diff --git a/common/errors/annotate.go b/common/errors/annotate.go |
index 6d7707c87b2391ed109c5bd64740f3e21d212daf..91dab743a84d59a5c5dfff394fdf9ef411fd7454 100644 |
--- a/common/errors/annotate.go |
+++ b/common/errors/annotate.go |
@@ -24,13 +24,18 @@ import ( |
"github.com/luci/luci-go/common/runtime/goroutine" |
) |
-// Datum is a single data entry value for StackContext.Data. |
+// Datum is a single data entry value for stackContext.Data. |
+// |
+// It's a tuple of Value (the actual data value you care about), and |
+// StackFormat, which is a fmt-style string for how this Datum should be |
+// rendered when using RenderStack. If StackFormat is left empty, "%#v" will be |
+// used. |
type Datum struct { |
Value interface{} |
StackFormat string |
} |
-// Data is used to add data to a StackContext. |
+// Data is used to add data when Annotate'ing an error. |
type Data map[string]Datum |
type stack struct { |
@@ -63,33 +68,33 @@ func (s *stack) findPointOfDivergence(other *stack) int { |
return myIdx |
} |
-// StackContexter is the interface that an error may implement if it has data |
+// stackContexter is the interface that an error may implement if it has data |
// associated with a specific stack frame. |
-type StackContexter interface { |
- StackContext() StackContext |
+type stackContexter interface { |
+ stackContext() stackContext |
} |
-// StackFrameInfo holds a stack and an index into that stack for association |
-// with StackContexts. |
-type StackFrameInfo struct { |
+// stackFrameInfo holds a stack and an index into that stack for association |
+// with stackContexts. |
+type stackFrameInfo struct { |
frameIdx int |
forStack *stack |
} |
-// StackContext represents the annotation data associated with an error, or an |
+// stackContext represents the annotation data associated with an error, or an |
// annotation of an error. |
-type StackContext struct { |
- FrameInfo StackFrameInfo |
- // Reason is the publicly-facing reason, and will show up in the Error() |
+type stackContext struct { |
+ frameInfo stackFrameInfo |
+ // reason is the publicly-facing reason, and will show up in the Error() |
// string. |
- Reason string |
+ reason string |
// InternalReason is used for printing tracebacks, but is otherwise formatted |
- // like Reason. |
- InternalReason string |
- Data Data |
+ // like reason. |
+ internalReason string |
+ data Data |
- Transient bool |
+ tags map[TagKey]interface{} |
} |
// We're looking for %(sometext) which is not preceded by a %. sometext may be |
@@ -136,17 +141,17 @@ func (d Data) Format(format string) string { |
return fmt.Sprintf(strings.Join(parts, ""), args...) |
} |
-// RenderPublic renders the public error.Error()-style string for this frame, |
+// renderPublic renders the public error.Error()-style string for this frame, |
// using the Reason and Data to produce a human readable string. |
-func (s *StackContext) RenderPublic(inner error) string { |
- if s.Reason == "" { |
+func (s *stackContext) renderPublic(inner error) string { |
+ if s.reason == "" { |
if inner != nil { |
return inner.Error() |
} |
return "" |
} |
- basis := s.Data.Format(s.Reason) |
+ basis := s.data.Format(s.reason) |
if inner != nil { |
return fmt.Sprintf("%s: %s", basis, inner) |
} |
@@ -156,15 +161,15 @@ func (s *StackContext) RenderPublic(inner error) string { |
// render renders the frame as a single entry in a stack trace. This looks like: |
// |
// I am an internal reson formatted with key1: value |
-// reason: "The literal content of the Reason field: %(key2)d" |
+// reason: "The literal content of the reason field: %(key2)d" |
// "key1" = "value" |
// "key2" = 10 |
-func (s *StackContext) render() Lines { |
- siz := len(s.Data) |
- if s.InternalReason != "" { |
+func (s *stackContext) render() Lines { |
+ siz := len(s.data) |
+ if s.internalReason != "" { |
siz++ |
} |
- if s.Reason != "" { |
+ if s.reason != "" { |
siz++ |
} |
if siz == 0 { |
@@ -173,76 +178,78 @@ func (s *StackContext) render() Lines { |
ret := make(Lines, 0, siz) |
- if s.InternalReason != "" { |
- ret = append(ret, s.Data.Format(s.InternalReason)) |
+ if s.internalReason != "" { |
+ ret = append(ret, s.data.Format(s.internalReason)) |
} |
- if s.Reason != "" { |
- ret = append(ret, fmt.Sprintf("reason: %q", s.Data.Format(s.Reason))) |
+ if s.reason != "" { |
+ ret = append(ret, fmt.Sprintf("reason: %q", s.data.Format(s.reason))) |
} |
- if s.Transient { |
- ret = append(ret, "transient: true") |
+ for key, val := range s.tags { |
+ ret = append(ret, fmt.Sprintf("tag[%q]: %#v", key.description, val)) |
} |
- if len(s.Data) > 0 { |
- for k, v := range s.Data { |
+ if len(s.data) > 0 { |
+ for k, v := range s.data { |
if v.StackFormat == "" || v.StackFormat == "%#v" { |
ret = append(ret, fmt.Sprintf("%q = %#v", k, v.Value)) |
} else { |
ret = append(ret, fmt.Sprintf("%q = "+v.StackFormat, k, v.Value)) |
} |
} |
- sort.Strings(ret[len(ret)-len(s.Data):]) |
+ sort.Strings(ret[len(ret)-len(s.data):]) |
} |
return ret |
} |
-// AddData does a 'dict.update' addition of the data. |
-func (s *StackContext) AddData(data Data) { |
- if s.Data == nil { |
- s.Data = make(Data, len(data)) |
+// addData does a 'dict.update' addition of the data. |
+func (s *stackContext) addData(data Data) { |
+ if s.data == nil { |
+ s.data = make(Data, len(data)) |
} |
for k, v := range data { |
- s.Data[k] = v |
+ s.data[k] = v |
} |
} |
-// AddDatum adds a single data item to the Data in this frame |
-func (s *StackContext) AddDatum(key string, value interface{}, format string) { |
- if s.Data == nil { |
- s.Data = Data{key: {value, format}} |
+// addDatum adds a single data item to the Data in this frame |
+func (s *stackContext) addDatum(key string, value interface{}, format string) { |
+ if s.data == nil { |
+ s.data = Data{key: {value, format}} |
} else { |
- s.Data[key] = Datum{value, format} |
+ s.data[key] = Datum{value, format} |
} |
} |
type terminalStackError struct { |
error |
- finfo StackFrameInfo |
+ finfo stackFrameInfo |
+ tags map[TagKey]interface{} |
} |
var _ interface { |
error |
- StackContexter |
+ stackContexter |
} = (*terminalStackError)(nil) |
-func (e *terminalStackError) StackContext() StackContext { return StackContext{FrameInfo: e.finfo} } |
+func (e *terminalStackError) stackContext() stackContext { |
+ return stackContext{frameInfo: e.finfo, tags: e.tags} |
+} |
type annotatedError struct { |
inner error |
- ctx StackContext |
+ ctx stackContext |
} |
var _ interface { |
error |
- StackContexter |
+ stackContexter |
Wrapped |
} = (*annotatedError)(nil) |
-func (e *annotatedError) Error() string { return e.ctx.RenderPublic(e.inner) } |
-func (e *annotatedError) StackContext() StackContext { return e.ctx } |
+func (e *annotatedError) Error() string { return e.ctx.renderPublic(e.inner) } |
+func (e *annotatedError) stackContext() stackContext { return e.ctx } |
func (e *annotatedError) InnerError() error { return e.inner } |
-func (e *annotatedError) IsTransient() bool { return e.ctx.Transient } |
// Annotator is a builder for annotating errors. Obtain one by calling Annotate |
// on an existing error or using Reason. |
@@ -250,7 +257,7 @@ func (e *annotatedError) IsTransient() bool { return e.ctx.Transient } |
// See the example test for Annotate to see how this is meant to be used. |
type Annotator struct { |
inner error |
- ctx StackContext |
+ ctx stackContext |
} |
// Reason adds a PUBLICLY READABLE reason string (for humans) to this error. |
@@ -273,7 +280,7 @@ func (a *Annotator) Reason(reason string) *Annotator { |
if a == nil { |
return a |
} |
- a.ctx.Reason = reason |
+ a.ctx.reason = reason |
return a |
} |
@@ -284,7 +291,7 @@ func (a *Annotator) InternalReason(reason string) *Annotator { |
if a == nil { |
return a |
} |
- a.ctx.InternalReason = reason |
+ a.ctx.internalReason = reason |
return a |
} |
@@ -302,7 +309,7 @@ func (a *Annotator) D(key string, value interface{}, format ...string) *Annotato |
default: |
panic(fmt.Errorf("len(format) > 1: %d", len(format))) |
} |
- a.ctx.AddDatum(key, value, formatVal) |
+ a.ctx.addDatum(key, value, formatVal) |
return a |
} |
@@ -311,18 +318,31 @@ func (a *Annotator) Data(data Data) *Annotator { |
if a == nil { |
return a |
} |
- a.ctx.AddData(data) |
+ a.ctx.addData(data) |
return a |
} |
-// Transient marks this error as transient. If the inner error is already |
-// transient, this has no effect. |
-func (a *Annotator) Transient() *Annotator { |
+// Tag adds a tag with an optional value to this error. |
+// |
+// `value` is a unary optional argument, and must be a simple type (i.e. has |
+// a reflect.Kind which is a base data type like bool, string, or int). |
+func (a *Annotator) Tag(tags ...TagValueGenerator) *Annotator { |
if a == nil { |
return a |
} |
- if !IsTransient(a.inner) { |
- a.ctx.Transient = true |
+ tagMap := make(map[TagKey]interface{}, len(tags)) |
+ for _, t := range tags { |
+ v := t.GenerateErrorTagValue() |
+ tagMap[v.Key] = v.Value |
+ } |
+ if len(tagMap) > 0 { |
+ if a.ctx.tags == nil { |
+ a.ctx.tags = tagMap |
+ } else { |
+ for k, v := range tagMap { |
+ a.ctx.tags[k] = v |
+ } |
+ } |
} |
return a |
} |
@@ -345,6 +365,9 @@ func Log(c context.Context, err error) { |
} |
// Lines is just a list of printable lines. |
+// |
+// It's a type because it's most frequently used as []Lines, and [][]string |
+// doesn't read well. |
type Lines []string |
// RenderedFrame represents a single, rendered stack frame. |
@@ -538,7 +561,7 @@ func RenderStack(err error) *RenderedError { |
lastAnnotatedFrame := 0 |
var wrappers = []Lines{} |
- getCurFrame := func(fi *StackFrameInfo) *RenderedFrame { |
+ getCurFrame := func(fi *stackFrameInfo) *RenderedFrame { |
if len(ret.Stacks) == 0 || ret.Stacks[len(ret.Stacks)-1].GoID != fi.forStack.id { |
lastAnnotatedFrame = len(fi.forStack.frames) - 1 |
toAdd := &RenderedStack{ |
@@ -565,10 +588,10 @@ func RenderStack(err error) *RenderedError { |
} |
for err != nil { |
- if sc, ok := err.(StackContexter); ok { |
- ctx := sc.StackContext() |
- if stk := ctx.FrameInfo.forStack; stk != nil { |
- frm := getCurFrame(&ctx.FrameInfo) |
+ if sc, ok := err.(stackContexter); ok { |
+ ctx := sc.stackContext() |
+ if stk := ctx.frameInfo.forStack; stk != nil { |
+ frm := getCurFrame(&ctx.frameInfo) |
if rendered := ctx.render(); len(rendered) > 0 { |
frm.Annotations = append(frm.Annotations, rendered) |
} |
@@ -581,7 +604,7 @@ func RenderStack(err error) *RenderedError { |
switch x := err.(type) { |
case MultiError: |
// TODO(riannucci): it's kinda dumb that we have to walk the MultiError |
- // twice (once in its StackContext method, and again here). |
+ // twice (once in its stackContext method, and again here). |
err = x.First() |
case Wrapped: |
err = x.InnerError() |
@@ -613,7 +636,7 @@ func Annotate(err error) *Annotator { |
if err == nil { |
return nil |
} |
- return &Annotator{err, StackContext{FrameInfo: StackFrameInfoForError(1, err)}} |
+ return &Annotator{err, stackContext{frameInfo: stackFrameInfoForError(1, err)}} |
} |
// Reason builds a new Annotator starting with reason. This allows you to use |
@@ -625,16 +648,24 @@ func Annotate(err error) *Annotator { |
// Prefer this form to errors.New(fmt.Sprintf("...")) |
func Reason(reason string) *Annotator { |
currentStack := captureStack(1) |
- frameInfo := StackFrameInfo{0, currentStack} |
- return (&Annotator{nil, StackContext{FrameInfo: frameInfo}}).Reason(reason) |
+ frameInfo := stackFrameInfo{0, currentStack} |
+ return (&Annotator{nil, stackContext{frameInfo: frameInfo}}).Reason(reason) |
} |
// New is an API-compatible version of the standard errors.New function. Unlike |
// the stdlib errors.New, this will capture the current stack information at the |
// place this error was created. |
-func New(msg string) error { |
- return &terminalStackError{errors.New(msg), |
- StackFrameInfo{forStack: captureStack(1)}} |
+func New(msg string, tags ...TagValueGenerator) error { |
+ tse := &terminalStackError{ |
+ errors.New(msg), stackFrameInfo{forStack: captureStack(1)}, nil} |
+ if len(tags) > 0 { |
+ tse.tags = make(map[TagKey]interface{}, len(tags)) |
+ for _, t := range tags { |
+ v := t.GenerateErrorTagValue() |
+ tse.tags[v.Key] = v.Value |
+ } |
+ } |
+ return tse |
} |
func captureStack(skip int) *stack { |
@@ -651,8 +682,8 @@ func captureStack(skip int) *stack { |
func getCapturedStack(err error) (ret *stack) { |
Walk(err, func(err error) bool { |
- if sc, ok := err.(StackContexter); ok { |
- ret = sc.StackContext().FrameInfo.forStack |
+ if sc, ok := err.(stackContexter); ok { |
+ ret = sc.stackContext().frameInfo.forStack |
return false |
} |
return true |
@@ -660,46 +691,26 @@ func getCapturedStack(err error) (ret *stack) { |
return |
} |
-// StackFrameInfoForError returns a StackFrameInfo suitable for use to implement |
-// the StackContexter interface. |
+// stackFrameInfoForError returns a stackFrameInfo suitable for use to implement |
+// the stackContexter interface. |
// |
// It skips the provided number of frames when collecting the current trace |
// (which should be equal to the number of functions between your error library |
// and the user's code). |
// |
-// The returned StackFrameInfo will find the appropriate frame in the error's |
+// The returned stackFrameInfo will find the appropriate frame in the error's |
// existing stack information (if the error was created with errors.New), or |
// include the current stack if it was not. |
-func StackFrameInfoForError(skip int, err error) StackFrameInfo { |
+func stackFrameInfoForError(skip int, err error) stackFrameInfo { |
currentStack := captureStack(skip + 1) |
currentlyCapturedStack := getCapturedStack(err) |
if currentlyCapturedStack == nil || currentStack.id != currentlyCapturedStack.id { |
// This is the very first annotation on this error OR |
// We switched goroutines. |
- return StackFrameInfo{forStack: currentStack} |
+ return stackFrameInfo{forStack: currentStack} |
} |
- return StackFrameInfo{ |
+ return stackFrameInfo{ |
frameIdx: currentlyCapturedStack.findPointOfDivergence(currentStack), |
forStack: currentlyCapturedStack, |
} |
} |
- |
-// ExtractData walks the error and extracts the given key's data from |
-// Annotations (e.g. data added with D(key, <value>) will return <value>). |
-// |
-// The first matching key encountered (e.g. highest up the callstack) will be |
-// returned. |
-// |
-// If the error does not contain key at all, this returns nil. |
-func ExtractData(err error, key string) (ret interface{}) { |
- Walk(err, func(err error) bool { |
- if sc, ok := err.(StackContexter); ok { |
- if d, ok := sc.StackContext().Data[key]; ok { |
- ret = d.Value |
- return false |
- } |
- } |
- return true |
- }) |
- return |
-} |