Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(56)

Unified Diff: common/errors/annotate.go

Issue 2963503003: [errors] Greatly simplify common/errors package. (Closed)
Patch Set: fix nits Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « common/data/text/stringtemplate/template.go ('k') | common/errors/annotate_example_test.go » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
« no previous file with comments | « common/data/text/stringtemplate/template.go ('k') | common/errors/annotate_example_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698