Index: common/errors/annotate.go |
diff --git a/common/errors/annotate.go b/common/errors/annotate.go |
index 91dab743a84d59a5c5dfff394fdf9ef411fd7454..4a0964a41cf392afddc2200e0b3f0d734ad72fe2 100644 |
--- a/common/errors/annotate.go |
+++ b/common/errors/annotate.go |
@@ -10,7 +10,6 @@ import ( |
"fmt" |
"io" |
"path/filepath" |
- "regexp" |
"runtime" |
"sort" |
"strings" |
@@ -24,20 +23,6 @@ import ( |
"github.com/luci/luci-go/common/runtime/goroutine" |
) |
-// 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 when Annotate'ing an error. |
-type Data map[string]Datum |
- |
type stack struct { |
id goroutine.ID |
frames []uintptr |
@@ -85,87 +70,34 @@ type stackFrameInfo struct { |
// annotation of an error. |
type stackContext struct { |
frameInfo stackFrameInfo |
- // reason is the publicly-facing reason, and will show up in the Error() |
- // string. |
+ // publicly-facing reason, and will show up in the Error() string. |
reason string |
- // InternalReason is used for printing tracebacks, but is otherwise formatted |
- // like reason. |
+ // used for printing tracebacks, but will not show up in the Error() string. |
internalReason string |
- data Data |
+ // tags are any data associated with this frame. |
tags map[TagKey]interface{} |
} |
-// We're looking for %(sometext) which is not preceded by a %. sometext may be |
-// any characters except for a close paren. |
-// |
-// Submatch indices: |
-// [0:1] Full match |
-// [2:3] Text before the (...) pair (including the '%'). |
-// [4:5] (key) |
-var namedFormatMatcher = regexp.MustCompile(`((?:^|[^%])%)\(([^)]+)\)`) |
- |
-// Format uses the data contained in this Data map to format the provided |
-// string. Items from the map are looked up in python dict-format style, e.g. |
-// |
-// %(key)d |
-// |
-// Will look up the item "key", and then format as a decimal number it using the |
-// value for "key" in this Data map. Like python, a given item may appear |
-// multiple times in the format string. |
-// |
-// All formatting directives are identical to the ones used by fmt.Sprintf. |
-func (d Data) Format(format string) string { |
- smi := namedFormatMatcher.FindAllStringSubmatchIndex(format, -1) |
- |
- var ( |
- parts = make([]string, 0, len(smi)+1) |
- args = make([]interface{}, 0, len(smi)) |
- pos = 0 |
- ) |
- for _, match := range smi { |
- // %(key)s => %s |
- parts = append(parts, format[pos:match[3]]) |
- pos = match[1] |
- |
- // Add key to args. |
- key := format[match[4]:match[5]] |
- if v, ok := d[key]; ok { |
- args = append(args, v.Value) |
- } else { |
- args = append(args, fmt.Sprintf("MISSING(key=%q)", key)) |
- } |
- } |
- parts = append(parts, format[pos:]) |
- return fmt.Sprintf(strings.Join(parts, ""), args...) |
-} |
- |
// renderPublic renders the public error.Error()-style string for this frame, |
-// using the Reason and Data to produce a human readable string. |
+// combining this frame's Reason with the inner error. |
func (s *stackContext) renderPublic(inner error) string { |
- if s.reason == "" { |
- if inner != nil { |
- return inner.Error() |
- } |
- return "" |
- } |
- |
- basis := s.data.Format(s.reason) |
- if inner != nil { |
- return fmt.Sprintf("%s: %s", basis, inner) |
+ switch { |
+ case inner == nil: |
+ return s.reason |
+ case s.reason == "": |
+ return inner.Error() |
} |
- return basis |
+ return fmt.Sprintf("%s: %s", s.reason, inner.Error()) |
} |
// 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" |
-// "key1" = "value" |
-// "key2" = 10 |
-func (s *stackContext) render() Lines { |
- siz := len(s.data) |
+// internal reason: I am an internal reason formatted with key1: value |
+func (s *stackContext) render() lines { |
+ siz := len(s.tags) |
if s.internalReason != "" { |
siz++ |
} |
@@ -176,49 +108,23 @@ func (s *stackContext) render() Lines { |
return nil |
} |
- ret := make(Lines, 0, siz) |
- |
- if s.internalReason != "" { |
- ret = append(ret, s.data.Format(s.internalReason)) |
- } |
+ ret := make(lines, 0, siz) |
if s.reason != "" { |
- ret = append(ret, fmt.Sprintf("reason: %q", s.data.Format(s.reason))) |
- } |
- for key, val := range s.tags { |
- ret = append(ret, fmt.Sprintf("tag[%q]: %#v", key.description, val)) |
+ ret = append(ret, fmt.Sprintf("reason: %s", s.reason)) |
} |
- |
- 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):]) |
+ if s.internalReason != "" { |
+ ret = append(ret, fmt.Sprintf("internal reason: %s", s.internalReason)) |
} |
- |
- 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)) |
+ keys := make(tagKeySlice, 0, len(s.tags)) |
+ for key := range s.tags { |
+ keys = append(keys, key) |
} |
- for k, v := range data { |
- s.data[k] = v |
+ sort.Sort(keys) |
+ for _, key := range keys { |
+ ret = append(ret, fmt.Sprintf("tag[%q]: %#v", key.description, s.tags[key])) |
} |
-} |
-// 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} |
- } |
+ return ret |
} |
type terminalStackError struct { |
@@ -260,65 +166,20 @@ type Annotator struct { |
ctx stackContext |
} |
-// Reason adds a PUBLICLY READABLE reason string (for humans) to this error. |
-// |
-// You should assume that end-users (including unauthenticated end users) may |
-// see the text in here. |
-// |
-// These reasons will be used to compose the result of the final Error() when |
-// rendering this error, and will also be used to decorate the error |
-// annotation stack when logging the error using the Log function. |
+// InternalReason adds a stack-trace-only internal reason string (for humans) to |
+// this error. |
// |
-// In a webserver context, if you don't want users to see some information about |
-// this error, don't put it in the Reason. |
+// The text here will only be visible when using `errors.Log` or |
+// `errors.RenderStack`, not when calling the .Error() method of the resulting |
+// error. |
// |
-// This explanation may have formatting instructions in the form of: |
-// %(key)... |
-// where key is the name of one of the entries submitted to either D or Data. |
-// The `...` may be any Printf-compatible formatting directive. |
-func (a *Annotator) Reason(reason string) *Annotator { |
- if a == nil { |
- return a |
- } |
- a.ctx.reason = reason |
- return a |
-} |
- |
-// InternalReason adds a stack-trace-only internal reason string (for humans) to |
-// this error. This is formatted like Reason, but will not be visible in the |
-// Error() string. |
-func (a *Annotator) InternalReason(reason string) *Annotator { |
+// The `reason` string is formatted with `args` and may contain Sprintf-style |
+// formatting directives. |
+func (a *Annotator) InternalReason(reason string, args ...interface{}) *Annotator { |
if a == nil { |
return a |
} |
- a.ctx.internalReason = reason |
- return a |
-} |
- |
-// D adds a single datum to this error. Only one format may be specified. If |
-// format is omitted or the empty string, the format "%#v" will be used. |
-func (a *Annotator) D(key string, value interface{}, format ...string) *Annotator { |
- if a == nil { |
- return a |
- } |
- formatVal := "" |
- switch len(format) { |
- case 0: |
- case 1: |
- formatVal = format[0] |
- default: |
- panic(fmt.Errorf("len(format) > 1: %d", len(format))) |
- } |
- a.ctx.addDatum(key, value, formatVal) |
- return a |
-} |
- |
-// Data adds data to this error. |
-func (a *Annotator) Data(data Data) *Annotator { |
- if a == nil { |
- return a |
- } |
- a.ctx.addData(data) |
+ a.ctx.internalReason = fmt.Sprintf(reason, args...) |
return a |
} |
@@ -357,43 +218,45 @@ func (a *Annotator) Err() error { |
// Log logs the full error. If this is an Annotated error, it will log the full |
// stack information as well. |
-func Log(c context.Context, err error) { |
+// |
+// This is a shortcut for logging the output of RenderStack(err). |
+func Log(c context.Context, err error, excludePkgs ...string) { |
log := logging.Get(c) |
- for _, l := range RenderStack(err).ToLines() { |
+ for _, l := range RenderStack(err, excludePkgs...) { |
log.Errorf("%s", l) |
} |
} |
-// Lines is just a list of printable lines. |
+// lines is just a list of printable lines. |
// |
-// It's a type because it's most frequently used as []Lines, and [][]string |
+// It's a type because it's most frequently used as []lines, and [][]string |
// doesn't read well. |
-type Lines []string |
+type lines []string |
-// RenderedFrame represents a single, rendered stack frame. |
-type RenderedFrame struct { |
- Pkg string |
- File string |
- LineNum int |
- FuncName string |
+// renderedFrame represents a single, rendered stack frame. |
+type renderedFrame struct { |
+ pkg string |
+ file string |
+ lineNum int |
+ funcName string |
- // Wrappers is any frame-info-less errors.Wrapped that were encountered when |
+ // wrappers is any frame-info-less errors.Wrapped that were encountered when |
// rendering that didn't have any associated frame info: this is the closest |
// frame to where they were added to the error. |
- Wrappers []Lines |
+ wrappers []lines |
- // Annotations is any Annotate context associated directly with this Frame. |
- Annotations []Lines |
+ // annotations is any Annotate context associated directly with this Frame. |
+ annotations []lines |
} |
var nlSlice = []byte{'\n'} |
-// DumpWrappersTo formats the Wrappers portion of this RenderedFrame. |
-func (r *RenderedFrame) DumpWrappersTo(w io.Writer, from, to int) (n int, err error) { |
+// dumpWrappersTo formats the wrappers portion of this renderedFrame. |
+func (r *renderedFrame) dumpWrappersTo(w io.Writer, from, to int) (n int, err error) { |
return iotools.WriteTracker(w, func(rawWriter io.Writer) error { |
w := &indented.Writer{Writer: rawWriter, UseSpaces: true} |
fmt.Fprintf(w, "From frame %d to %d, the following wrappers were found:\n", from, to) |
- for i, wrp := range r.Wrappers { |
+ for i, wrp := range r.wrappers { |
if i != 0 { |
w.Write(nlSlice) |
} |
@@ -411,23 +274,23 @@ func (r *RenderedFrame) DumpWrappersTo(w io.Writer, from, to int) (n int, err er |
}) |
} |
-// DumpTo formats the Header and Annotations for this RenderedFrame. |
-func (r *RenderedFrame) DumpTo(w io.Writer, idx int) (n int, err error) { |
+// dumpTo formats the Header and annotations for this renderedFrame. |
+func (r *renderedFrame) dumpTo(w io.Writer, idx int) (n int, err error) { |
return iotools.WriteTracker(w, func(rawWriter io.Writer) error { |
w := &indented.Writer{Writer: rawWriter, UseSpaces: true} |
- fmt.Fprintf(w, "#%d %s/%s:%d - %s()\n", idx, r.Pkg, r.File, |
- r.LineNum, r.FuncName) |
+ fmt.Fprintf(w, "#%d %s/%s:%d - %s()\n", idx, r.pkg, r.file, |
+ r.lineNum, r.funcName) |
w.Level += 2 |
- switch len(r.Annotations) { |
+ switch len(r.annotations) { |
case 0: |
// pass |
case 1: |
- for _, line := range r.Annotations[0] { |
+ for _, line := range r.annotations[0] { |
fmt.Fprintf(w, "%s\n", line) |
} |
default: |
- for i, ann := range r.Annotations { |
+ for i, ann := range r.annotations { |
fmt.Fprintf(w, "annotation #%d:\n", i) |
w.Level += 2 |
for _, line := range ann { |
@@ -440,18 +303,18 @@ func (r *RenderedFrame) DumpTo(w io.Writer, idx int) (n int, err error) { |
}) |
} |
-// RenderedStack is a single rendered stack from one goroutine. |
-type RenderedStack struct { |
- GoID goroutine.ID |
- Frames []*RenderedFrame |
+// renderedStack is a single rendered stack from one goroutine. |
+type renderedStack struct { |
+ goID goroutine.ID |
+ frames []*renderedFrame |
} |
-// DumpTo formats the full stack. |
-func (r *RenderedStack) DumpTo(w io.Writer, excludePkgs ...string) (n int, err error) { |
+// dumpTo formats the full stack. |
+func (r *renderedStack) dumpTo(w io.Writer, excludePkgs ...string) (n int, err error) { |
excludeSet := stringset.NewFromSlice(excludePkgs...) |
return iotools.WriteTracker(w, func(w io.Writer) error { |
- fmt.Fprintf(w, "goroutine %d:\n", r.GoID) |
+ fmt.Fprintf(w, "goroutine %d:\n", r.goID) |
lastIdx := 0 |
needNL := false |
@@ -468,31 +331,31 @@ func (r *RenderedStack) DumpTo(w io.Writer, excludePkgs ...string) (n int, err e |
skipPkg = "" |
} |
} |
- for i, f := range r.Frames { |
+ for i, f := range r.frames { |
if needNL { |
w.Write(nlSlice) |
needNL = false |
} |
- if excludeSet.Has(f.Pkg) { |
- if skipPkg == f.Pkg { |
+ if excludeSet.Has(f.pkg) { |
+ if skipPkg == f.pkg { |
skipCount++ |
} else { |
flushSkips("") |
skipCount++ |
- skipPkg = f.Pkg |
+ skipPkg = f.pkg |
} |
continue |
} |
flushSkips("\n") |
- if len(f.Wrappers) > 0 { |
- f.DumpWrappersTo(w, lastIdx, i) |
+ if len(f.wrappers) > 0 { |
+ f.dumpWrappersTo(w, lastIdx, i) |
w.Write(nlSlice) |
} |
- if len(f.Annotations) > 0 { |
+ if len(f.annotations) > 0 { |
lastIdx = i |
needNL = true |
} |
- f.DumpTo(w, i) |
+ f.dumpTo(w, i) |
} |
flushSkips("") |
@@ -500,38 +363,38 @@ func (r *RenderedStack) DumpTo(w io.Writer, excludePkgs ...string) (n int, err e |
}) |
} |
-// RenderedError is a series of RenderedStacks, one for each goroutine that the |
+// renderedError is a series of RenderedStacks, one for each goroutine that the |
// error was annotated on. |
-type RenderedError struct { |
- OriginalError string |
- Stacks []*RenderedStack |
+type renderedError struct { |
+ originalError string |
+ stacks []*renderedStack |
} |
-// ToLines renders a full-information stack trace as a series of lines. |
-func (r *RenderedError) ToLines(excludePkgs ...string) Lines { |
+// toLines renders a full-information stack trace as a series of lines. |
+func (r *renderedError) toLines(excludePkgs ...string) lines { |
buf := bytes.Buffer{} |
- r.DumpTo(&buf, excludePkgs...) |
+ r.dumpTo(&buf, excludePkgs...) |
return strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n") |
} |
-// DumpTo writes the full-information stack trace to the writer. |
-func (r *RenderedError) DumpTo(w io.Writer, excludePkgs ...string) (n int, err error) { |
+// dumpTo writes the full-information stack trace to the writer. |
+func (r *renderedError) dumpTo(w io.Writer, excludePkgs ...string) (n int, err error) { |
return iotools.WriteTracker(w, func(w io.Writer) error { |
- if r.OriginalError != "" { |
- fmt.Fprintf(w, "original error: %s\n\n", r.OriginalError) |
+ if r.originalError != "" { |
+ fmt.Fprintf(w, "original error: %s\n\n", r.originalError) |
} |
- for i := len(r.Stacks) - 1; i >= 0; i-- { |
- if i != len(r.Stacks)-1 { |
+ for i := len(r.stacks) - 1; i >= 0; i-- { |
+ if i != len(r.stacks)-1 { |
w.Write(nlSlice) |
} |
- r.Stacks[i].DumpTo(w, excludePkgs...) |
+ r.stacks[i].dumpTo(w, excludePkgs...) |
} |
return nil |
}) |
} |
-func frameHeaderDetails(frm uintptr) (pkg, filename, funcname string, lineno int) { |
+func frameHeaderDetails(frm uintptr) (pkg, filename, funcName string, lineno int) { |
// this `frm--` is to get the correct line/function information, since the |
// Frame is actually the `return` pc. See runtime.Callers. |
frm-- |
@@ -546,45 +409,49 @@ func frameHeaderDetails(frm uintptr) (pkg, filename, funcname string, lineno int |
fnName := fn.Name() |
lastSlash := strings.LastIndex(fnName, "/") |
if lastSlash == -1 { |
- funcname = fnName |
+ funcName = fnName |
pkg = pkgTopLevelName |
} else { |
- funcname = fnName[lastSlash+1:] |
+ funcName = fnName[lastSlash+1:] |
pkg = fmt.Sprintf("%s/%s", fnName[:lastSlash], pkgTopLevelName) |
} |
return |
} |
-// RenderStack renders the error to a RenderedError. |
-func RenderStack(err error) *RenderedError { |
- ret := &RenderedError{} |
+// RenderStack renders the error to a list of lines. |
+func RenderStack(err error, excludePkgs ...string) []string { |
+ return renderStack(err).toLines(excludePkgs...) |
+} |
+ |
+func renderStack(err error) *renderedError { |
+ ret := &renderedError{} |
lastAnnotatedFrame := 0 |
- var wrappers = []Lines{} |
- getCurFrame := func(fi *stackFrameInfo) *RenderedFrame { |
- if len(ret.Stacks) == 0 || ret.Stacks[len(ret.Stacks)-1].GoID != fi.forStack.id { |
+ var wrappers = []lines{} |
+ 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{ |
- GoID: fi.forStack.id, |
- Frames: make([]*RenderedFrame, len(fi.forStack.frames)), |
+ toAdd := &renderedStack{ |
+ goID: fi.forStack.id, |
+ frames: make([]*renderedFrame, len(fi.forStack.frames)), |
} |
for i, frm := range fi.forStack.frames { |
pkgPath, filename, functionName, line := frameHeaderDetails(frm) |
- toAdd.Frames[i] = &RenderedFrame{ |
- Pkg: pkgPath, File: filename, LineNum: line, FuncName: functionName} |
+ toAdd.frames[i] = &renderedFrame{ |
+ pkg: pkgPath, file: filename, lineNum: line, funcName: functionName} |
} |
- ret.Stacks = append(ret.Stacks, toAdd) |
+ ret.stacks = append(ret.stacks, toAdd) |
} |
- curStack := ret.Stacks[len(ret.Stacks)-1] |
+ curStack := ret.stacks[len(ret.stacks)-1] |
if fi.frameIdx < lastAnnotatedFrame { |
lastAnnotatedFrame = fi.frameIdx |
- frm := curStack.Frames[lastAnnotatedFrame] |
- frm.Wrappers = wrappers |
+ frm := curStack.frames[lastAnnotatedFrame] |
+ frm.wrappers = wrappers |
wrappers = nil |
return frm |
} |
- return curStack.Frames[lastAnnotatedFrame] |
+ return curStack.frames[lastAnnotatedFrame] |
} |
for err != nil { |
@@ -593,13 +460,13 @@ func RenderStack(err error) *RenderedError { |
if stk := ctx.frameInfo.forStack; stk != nil { |
frm := getCurFrame(&ctx.frameInfo) |
if rendered := ctx.render(); len(rendered) > 0 { |
- frm.Annotations = append(frm.Annotations, rendered) |
+ frm.annotations = append(frm.annotations, rendered) |
} |
} else { |
wrappers = append(wrappers, ctx.render()) |
} |
} else { |
- wrappers = append(wrappers, Lines{fmt.Sprintf("unknown wrapper %T", err)}) |
+ wrappers = append(wrappers, lines{fmt.Sprintf("unknown wrapper %T", err)}) |
} |
switch x := err.(type) { |
case MultiError: |
@@ -609,7 +476,7 @@ func RenderStack(err error) *RenderedError { |
case Wrapped: |
err = x.InnerError() |
default: |
- ret.OriginalError = err.Error() |
+ ret.originalError = err.Error() |
err = nil |
} |
} |
@@ -618,8 +485,9 @@ func RenderStack(err error) *RenderedError { |
} |
// Annotate captures the current stack frame and returns a new annotatable |
-// error. You can add additional metadata to this error with its methods and |
-// then get the new derived error with the Err() function. |
+// error, attaching the publically readable `reason` format string to the error. |
+// You can add additional metadata to this error with the 'InternalReason' and |
+// 'Tag' methods, and then obtain a real `error` with the Err() function. |
// |
// If this is passed nil, it will return a no-op Annotator whose .Err() function |
// will also return nil. |
@@ -628,28 +496,42 @@ func RenderStack(err error) *RenderedError { |
// returned error. |
// |
// Rendering the derived error with Error() will render a summary version of all |
-// the Reasons as well as the initial underlying errors Error() text. It is |
-// intended that the initial underlying error and all annotated Reasons only |
-// contain user-visible information, so that the accumulated error may be |
+// the public `reason`s as well as the initial underlying error's Error() text. |
+// It is intended that the initial underlying error and all annotated reasons |
+// only contain user-visible information, so that the accumulated error may be |
// returned to the user without worrying about leakage. |
-func Annotate(err error) *Annotator { |
+// |
+// You should assume that end-users (including unauthenticated end users) may |
+// see the text in the `reason` field here. To only attach an internal reason, |
+// leave the `reason` argument blank and don't pass any additional formatting |
+// arguments. |
+// |
+// The `reason` string is formatted with `args` and may contain Sprintf-style |
+// formatting directives. |
+func Annotate(err error, reason string, args ...interface{}) *Annotator { |
if err == nil { |
return nil |
} |
- return &Annotator{err, stackContext{frameInfo: stackFrameInfoForError(1, err)}} |
+ return &Annotator{err, stackContext{ |
+ frameInfo: stackFrameInfoForError(1, err), |
+ reason: fmt.Sprintf(reason, args...), |
+ }} |
} |
// Reason builds a new Annotator starting with reason. This allows you to use |
// all the formatting directives you would normally use with Annotate, in case |
-// your originating error needs formatting directives: |
+// your originating error needs tags or an internal reason. |
// |
-// errors.Reason("something bad: %(value)d").D("value", 100)).Err() |
+// errors.Reason("something bad: %d", value).Tag(transient.Tag).Err() |
// |
// Prefer this form to errors.New(fmt.Sprintf("...")) |
-func Reason(reason string) *Annotator { |
+func Reason(reason string, args ...interface{}) *Annotator { |
currentStack := captureStack(1) |
frameInfo := stackFrameInfo{0, currentStack} |
- return (&Annotator{nil, stackContext{frameInfo: frameInfo}}).Reason(reason) |
+ return (&Annotator{nil, stackContext{ |
+ frameInfo: frameInfo, |
+ reason: fmt.Sprintf(reason, args...), |
+ }}) |
} |
// New is an API-compatible version of the standard errors.New function. Unlike |