OLD | NEW |
---|---|
1 // Copyright 2016 The LUCI Authors. All rights reserved. | 1 // Copyright 2016 The LUCI Authors. All rights reserved. |
2 // Use of this source code is governed under the Apache License, Version 2.0 | 2 // Use of this source code is governed under the Apache License, Version 2.0 |
3 // that can be found in the LICENSE file. | 3 // that can be found in the LICENSE file. |
4 | 4 |
5 package errors | 5 package errors |
6 | 6 |
7 import ( | 7 import ( |
8 "bytes" | 8 "bytes" |
9 "errors" | 9 "errors" |
10 "fmt" | 10 "fmt" |
11 "io" | 11 "io" |
12 "path/filepath" | 12 "path/filepath" |
13 "regexp" | |
14 "runtime" | 13 "runtime" |
15 "sort" | |
16 "strings" | 14 "strings" |
17 | 15 |
18 "golang.org/x/net/context" | 16 "golang.org/x/net/context" |
19 | 17 |
20 "github.com/luci/luci-go/common/data/stringset" | 18 "github.com/luci/luci-go/common/data/stringset" |
21 "github.com/luci/luci-go/common/data/text/indented" | 19 "github.com/luci/luci-go/common/data/text/indented" |
22 "github.com/luci/luci-go/common/iotools" | 20 "github.com/luci/luci-go/common/iotools" |
23 "github.com/luci/luci-go/common/logging" | 21 "github.com/luci/luci-go/common/logging" |
24 "github.com/luci/luci-go/common/runtime/goroutine" | 22 "github.com/luci/luci-go/common/runtime/goroutine" |
25 ) | 23 ) |
26 | 24 |
27 // Datum is a single data entry value for stackContext.Data. | |
28 // | |
29 // It's a tuple of Value (the actual data value you care about), and | |
30 // StackFormat, which is a fmt-style string for how this Datum should be | |
31 // rendered when using RenderStack. If StackFormat is left empty, "%#v" will be | |
32 // used. | |
33 type Datum struct { | |
34 Value interface{} | |
35 StackFormat string | |
36 } | |
37 | |
38 // Data is used to add data when Annotate'ing an error. | |
39 type Data map[string]Datum | |
40 | |
41 type stack struct { | 25 type stack struct { |
42 id goroutine.ID | 26 id goroutine.ID |
43 frames []uintptr | 27 frames []uintptr |
44 } | 28 } |
45 | 29 |
46 func (s *stack) findPointOfDivergence(other *stack) int { | 30 func (s *stack) findPointOfDivergence(other *stack) int { |
47 // TODO(iannucci): can we optimize this search routine to not overly pen alize | 31 // TODO(iannucci): can we optimize this search routine to not overly pen alize |
48 // tail-recursive functions? Searching 'up' from both stacks doesn't wor k in | 32 // tail-recursive functions? Searching 'up' from both stacks doesn't wor k in |
49 // the face of recursion because there will be multiple ambiguous stack | 33 // the face of recursion because there will be multiple ambiguous stack |
50 // frames. The algorithm below is correct, but it potentially collects a nd | 34 // frames. The algorithm below is correct, but it potentially collects a nd |
(...skipping 27 matching lines...) Expand all Loading... | |
78 // with stackContexts. | 62 // with stackContexts. |
79 type stackFrameInfo struct { | 63 type stackFrameInfo struct { |
80 frameIdx int | 64 frameIdx int |
81 forStack *stack | 65 forStack *stack |
82 } | 66 } |
83 | 67 |
84 // stackContext represents the annotation data associated with an error, or an | 68 // stackContext represents the annotation data associated with an error, or an |
85 // annotation of an error. | 69 // annotation of an error. |
86 type stackContext struct { | 70 type stackContext struct { |
87 frameInfo stackFrameInfo | 71 frameInfo stackFrameInfo |
88 » // reason is the publicly-facing reason, and will show up in the Error() | 72 » // publicly-facing reason, and will show up in the Error() string. |
89 » // string. | |
90 reason string | 73 reason string |
91 | 74 |
92 » // InternalReason is used for printing tracebacks, but is otherwise form atted | 75 » // used for printing tracebacks, but will not show up in the Error() str ing. |
93 » // like reason. | |
94 internalReason string | 76 internalReason string |
95 data Data | |
96 | 77 |
78 // tags are any data associated with this frame. | |
97 tags map[TagKey]interface{} | 79 tags map[TagKey]interface{} |
98 } | 80 } |
99 | 81 |
100 // We're looking for %(sometext) which is not preceded by a %. sometext may be | 82 // renderPublic renders the public error.Error()-style string for this frame, |
101 // any characters except for a close paren. | 83 // combining this frame's Reason with the inner error. |
102 // | 84 func (s *stackContext) renderPublic(inner error) string { |
103 // Submatch indices: | 85 » switch { |
104 // [0:1] Full match | 86 » case inner == nil: |
105 // [2:3] Text before the (...) pair (including the '%'). | 87 » » return s.reason |
106 // [4:5] (key) | 88 » case s.reason == "": |
107 var namedFormatMatcher = regexp.MustCompile(`((?:^|[^%])%)\(([^)]+)\)`) | 89 » » return inner.Error() |
108 | |
109 // Format uses the data contained in this Data map to format the provided | |
110 // string. Items from the map are looked up in python dict-format style, e.g. | |
111 // | |
112 // %(key)d | |
113 // | |
114 // Will look up the item "key", and then format as a decimal number it using the | |
115 // value for "key" in this Data map. Like python, a given item may appear | |
116 // multiple times in the format string. | |
117 // | |
118 // All formatting directives are identical to the ones used by fmt.Sprintf. | |
119 func (d Data) Format(format string) string { | |
120 » smi := namedFormatMatcher.FindAllStringSubmatchIndex(format, -1) | |
121 | |
122 » var ( | |
123 » » parts = make([]string, 0, len(smi)+1) | |
124 » » args = make([]interface{}, 0, len(smi)) | |
125 » » pos = 0 | |
126 » ) | |
127 » for _, match := range smi { | |
128 » » // %(key)s => %s | |
129 » » parts = append(parts, format[pos:match[3]]) | |
130 » » pos = match[1] | |
131 | |
132 » » // Add key to args. | |
133 » » key := format[match[4]:match[5]] | |
134 » » if v, ok := d[key]; ok { | |
135 » » » args = append(args, v.Value) | |
136 » » } else { | |
137 » » » args = append(args, fmt.Sprintf("MISSING(key=%q)", key)) | |
138 » » } | |
139 } | 90 } |
140 » parts = append(parts, format[pos:]) | 91 » return fmt.Sprintf("%s: %s", s.reason, inner.Error()) |
141 » return fmt.Sprintf(strings.Join(parts, ""), args...) | |
142 } | |
143 | |
144 // renderPublic renders the public error.Error()-style string for this frame, | |
145 // using the Reason and Data to produce a human readable string. | |
146 func (s *stackContext) renderPublic(inner error) string { | |
147 » if s.reason == "" { | |
148 » » if inner != nil { | |
149 » » » return inner.Error() | |
150 » » } | |
151 » » return "" | |
152 » } | |
153 | |
154 » basis := s.data.Format(s.reason) | |
155 » if inner != nil { | |
156 » » return fmt.Sprintf("%s: %s", basis, inner) | |
157 » } | |
158 » return basis | |
159 } | 92 } |
160 | 93 |
161 // render renders the frame as a single entry in a stack trace. This looks like: | 94 // render renders the frame as a single entry in a stack trace. This looks like: |
162 // | 95 // |
163 // I am an internal reson formatted with key1: value | 96 // I am an internal reson formatted with key1: value |
Vadim Sh.
2017/06/29 02:08:31
typo: 'reason'
also, is this comment still up-to-
iannucci
2017/06/29 23:22:51
not really, going to make a followup CL which furt
| |
164 // reason: "The literal content of the reason field: %(key2)d" | 97 // reason: "The literal content of the reason field: %(key2)d" |
165 // "key1" = "value" | 98 // "key1" = "value" |
166 // "key2" = 10 | 99 // "key2" = 10 |
167 func (s *stackContext) render() Lines { | 100 func (s *stackContext) render() lines { |
168 » siz := len(s.data) | 101 » siz := len(s.tags) |
169 if s.internalReason != "" { | 102 if s.internalReason != "" { |
170 siz++ | 103 siz++ |
171 } | 104 } |
172 if s.reason != "" { | 105 if s.reason != "" { |
173 siz++ | 106 siz++ |
174 } | 107 } |
175 if siz == 0 { | 108 if siz == 0 { |
176 return nil | 109 return nil |
177 } | 110 } |
178 | 111 |
179 » ret := make(Lines, 0, siz) | 112 » ret := make(lines, 0, siz) |
180 | 113 » if s.reason != "" { |
114 » » ret = append(ret, fmt.Sprintf("reason: %s", s.reason)) | |
115 » } | |
181 if s.internalReason != "" { | 116 if s.internalReason != "" { |
182 » » ret = append(ret, s.data.Format(s.internalReason)) | 117 » » ret = append(ret, fmt.Sprintf("internal reason: %s", s.internalR eason)) |
183 » } | |
184 » if s.reason != "" { | |
185 » » ret = append(ret, fmt.Sprintf("reason: %q", s.data.Format(s.reas on))) | |
186 } | 118 } |
187 for key, val := range s.tags { | 119 for key, val := range s.tags { |
Vadim Sh.
2017/06/29 02:08:31
we probably want to sort tags for consistent outpu
iannucci
2017/06/29 23:22:51
Done.
| |
188 ret = append(ret, fmt.Sprintf("tag[%q]: %#v", key.description, v al)) | 120 ret = append(ret, fmt.Sprintf("tag[%q]: %#v", key.description, v al)) |
189 } | 121 } |
190 | 122 |
191 if len(s.data) > 0 { | |
192 for k, v := range s.data { | |
193 if v.StackFormat == "" || v.StackFormat == "%#v" { | |
194 ret = append(ret, fmt.Sprintf("%q = %#v", k, v.V alue)) | |
195 } else { | |
196 ret = append(ret, fmt.Sprintf("%q = "+v.StackFor mat, k, v.Value)) | |
197 } | |
198 } | |
199 sort.Strings(ret[len(ret)-len(s.data):]) | |
200 } | |
201 | |
202 return ret | 123 return ret |
203 } | 124 } |
204 | 125 |
205 // addData does a 'dict.update' addition of the data. | |
206 func (s *stackContext) addData(data Data) { | |
207 if s.data == nil { | |
208 s.data = make(Data, len(data)) | |
209 } | |
210 for k, v := range data { | |
211 s.data[k] = v | |
212 } | |
213 } | |
214 | |
215 // addDatum adds a single data item to the Data in this frame | |
216 func (s *stackContext) addDatum(key string, value interface{}, format string) { | |
217 if s.data == nil { | |
218 s.data = Data{key: {value, format}} | |
219 } else { | |
220 s.data[key] = Datum{value, format} | |
221 } | |
222 } | |
223 | |
224 type terminalStackError struct { | 126 type terminalStackError struct { |
225 error | 127 error |
226 finfo stackFrameInfo | 128 finfo stackFrameInfo |
227 tags map[TagKey]interface{} | 129 tags map[TagKey]interface{} |
228 } | 130 } |
229 | 131 |
230 var _ interface { | 132 var _ interface { |
231 error | 133 error |
232 stackContexter | 134 stackContexter |
233 } = (*terminalStackError)(nil) | 135 } = (*terminalStackError)(nil) |
(...skipping 19 matching lines...) Expand all Loading... | |
253 | 155 |
254 // Annotator is a builder for annotating errors. Obtain one by calling Annotate | 156 // Annotator is a builder for annotating errors. Obtain one by calling Annotate |
255 // on an existing error or using Reason. | 157 // on an existing error or using Reason. |
256 // | 158 // |
257 // See the example test for Annotate to see how this is meant to be used. | 159 // See the example test for Annotate to see how this is meant to be used. |
258 type Annotator struct { | 160 type Annotator struct { |
259 inner error | 161 inner error |
260 ctx stackContext | 162 ctx stackContext |
261 } | 163 } |
262 | 164 |
263 // Reason adds a PUBLICLY READABLE reason string (for humans) to this error. | 165 // InternalReason adds a stack-trace-only internal reason string (for humans) to |
166 // this error. | |
264 // | 167 // |
265 // You should assume that end-users (including unauthenticated end users) may | 168 // The text here will only be visible when using `errors.Log` or |
266 // see the text in here. | 169 // `errors.RenderStack`, not when calling the .Error() method of the resulting |
170 // error. | |
267 // | 171 // |
268 // These reasons will be used to compose the result of the final Error() when | 172 // The `reason` string is formatted with `args` and may contain Sprintf-style |
269 // rendering this error, and will also be used to decorate the error | 173 // formatting directives. |
270 // annotation stack when logging the error using the Log function. | 174 func (a *Annotator) InternalReason(reason string, args ...interface{}) *Annotato r { |
271 // | |
272 // In a webserver context, if you don't want users to see some information about | |
273 // this error, don't put it in the Reason. | |
274 // | |
275 // This explanation may have formatting instructions in the form of: | |
276 // %(key)... | |
277 // where key is the name of one of the entries submitted to either D or Data. | |
278 // The `...` may be any Printf-compatible formatting directive. | |
279 func (a *Annotator) Reason(reason string) *Annotator { | |
280 if a == nil { | 175 if a == nil { |
281 return a | 176 return a |
282 } | 177 } |
283 » a.ctx.reason = reason | 178 » a.ctx.internalReason = fmt.Sprintf(reason, args...) |
Vadim Sh.
2017/06/29 02:08:31
woot!
| |
284 return a | 179 return a |
285 } | 180 } |
286 | 181 |
287 // InternalReason adds a stack-trace-only internal reason string (for humans) to | |
288 // this error. This is formatted like Reason, but will not be visible in the | |
289 // Error() string. | |
290 func (a *Annotator) InternalReason(reason string) *Annotator { | |
291 if a == nil { | |
292 return a | |
293 } | |
294 a.ctx.internalReason = reason | |
295 return a | |
296 } | |
297 | |
298 // D adds a single datum to this error. Only one format may be specified. If | |
299 // format is omitted or the empty string, the format "%#v" will be used. | |
300 func (a *Annotator) D(key string, value interface{}, format ...string) *Annotato r { | |
301 if a == nil { | |
302 return a | |
303 } | |
304 formatVal := "" | |
305 switch len(format) { | |
306 case 0: | |
307 case 1: | |
308 formatVal = format[0] | |
309 default: | |
310 panic(fmt.Errorf("len(format) > 1: %d", len(format))) | |
311 } | |
312 a.ctx.addDatum(key, value, formatVal) | |
313 return a | |
314 } | |
315 | |
316 // Data adds data to this error. | |
317 func (a *Annotator) Data(data Data) *Annotator { | |
318 if a == nil { | |
319 return a | |
320 } | |
321 a.ctx.addData(data) | |
322 return a | |
323 } | |
324 | |
325 // Tag adds a tag with an optional value to this error. | 182 // Tag adds a tag with an optional value to this error. |
326 // | 183 // |
327 // `value` is a unary optional argument, and must be a simple type (i.e. has | 184 // `value` is a unary optional argument, and must be a simple type (i.e. has |
328 // a reflect.Kind which is a base data type like bool, string, or int). | 185 // a reflect.Kind which is a base data type like bool, string, or int). |
329 func (a *Annotator) Tag(tags ...TagValueGenerator) *Annotator { | 186 func (a *Annotator) Tag(tags ...TagValueGenerator) *Annotator { |
330 if a == nil { | 187 if a == nil { |
331 return a | 188 return a |
332 } | 189 } |
333 tagMap := make(map[TagKey]interface{}, len(tags)) | 190 tagMap := make(map[TagKey]interface{}, len(tags)) |
334 for _, t := range tags { | 191 for _, t := range tags { |
(...skipping 15 matching lines...) Expand all Loading... | |
350 // Err returns the finalized annotated error. | 207 // Err returns the finalized annotated error. |
351 func (a *Annotator) Err() error { | 208 func (a *Annotator) Err() error { |
352 if a == nil { | 209 if a == nil { |
353 return nil | 210 return nil |
354 } | 211 } |
355 return (*annotatedError)(a) | 212 return (*annotatedError)(a) |
356 } | 213 } |
357 | 214 |
358 // Log logs the full error. If this is an Annotated error, it will log the full | 215 // Log logs the full error. If this is an Annotated error, it will log the full |
359 // stack information as well. | 216 // stack information as well. |
360 func Log(c context.Context, err error) { | 217 // |
218 // This is a shortcut for logging the output of RenderStack(err). | |
219 func Log(c context.Context, err error, excludePkgs ...string) { | |
Vadim Sh.
2017/06/29 02:08:31
is 'excludePkgs' feature used outside of unit test
iannucci
2017/06/29 23:22:51
followup, but yes, it can still be used.
| |
361 log := logging.Get(c) | 220 log := logging.Get(c) |
362 » for _, l := range RenderStack(err).ToLines() { | 221 » for _, l := range RenderStack(err, excludePkgs...) { |
363 log.Errorf("%s", l) | 222 log.Errorf("%s", l) |
364 } | 223 } |
365 } | 224 } |
366 | 225 |
367 // Lines is just a list of printable lines. | 226 // lines is just a list of printable lines. |
368 // | 227 // |
369 // It's a type because it's most frequently used as []Lines, and [][]string | 228 // It's a type because it's most frequently used as []lines, and [][]string |
370 // doesn't read well. | 229 // doesn't read well. |
371 type Lines []string | 230 type lines []string |
372 | 231 |
373 // RenderedFrame represents a single, rendered stack frame. | 232 // renderedFrame represents a single, rendered stack frame. |
374 type RenderedFrame struct { | 233 type renderedFrame struct { |
375 » Pkg string | 234 » pkg string |
376 » File string | 235 » file string |
377 » LineNum int | 236 » lineNum int |
378 » FuncName string | 237 » funcName string |
379 | 238 |
380 » // Wrappers is any frame-info-less errors.Wrapped that were encountered when | 239 » // wrappers is any frame-info-less errors.Wrapped that were encountered when |
381 // rendering that didn't have any associated frame info: this is the clo sest | 240 // rendering that didn't have any associated frame info: this is the clo sest |
382 // frame to where they were added to the error. | 241 // frame to where they were added to the error. |
383 » Wrappers []Lines | 242 » wrappers []lines |
384 | 243 |
385 » // Annotations is any Annotate context associated directly with this Fra me. | 244 » // annotations is any Annotate context associated directly with this Fra me. |
386 » Annotations []Lines | 245 » annotations []lines |
387 } | 246 } |
388 | 247 |
389 var nlSlice = []byte{'\n'} | 248 var nlSlice = []byte{'\n'} |
390 | 249 |
391 // DumpWrappersTo formats the Wrappers portion of this RenderedFrame. | 250 // DumpWrappersTo formats the wrappers portion of this renderedFrame. |
392 func (r *RenderedFrame) DumpWrappersTo(w io.Writer, from, to int) (n int, err er ror) { | 251 func (r *renderedFrame) DumpWrappersTo(w io.Writer, from, to int) (n int, err er ror) { |
Vadim Sh.
2017/06/29 02:08:31
is DumpWrappersTo part of some interface?
If not,
iannucci
2017/06/29 23:22:51
done
| |
393 return iotools.WriteTracker(w, func(rawWriter io.Writer) error { | 252 return iotools.WriteTracker(w, func(rawWriter io.Writer) error { |
394 w := &indented.Writer{Writer: rawWriter, UseSpaces: true} | 253 w := &indented.Writer{Writer: rawWriter, UseSpaces: true} |
395 fmt.Fprintf(w, "From frame %d to %d, the following wrappers were found:\n", from, to) | 254 fmt.Fprintf(w, "From frame %d to %d, the following wrappers were found:\n", from, to) |
396 » » for i, wrp := range r.Wrappers { | 255 » » for i, wrp := range r.wrappers { |
397 if i != 0 { | 256 if i != 0 { |
398 w.Write(nlSlice) | 257 w.Write(nlSlice) |
399 } | 258 } |
400 w.Level = 2 | 259 w.Level = 2 |
401 for i, line := range wrp { | 260 for i, line := range wrp { |
402 if i == 0 { | 261 if i == 0 { |
403 fmt.Fprintf(w, "%s\n", line) | 262 fmt.Fprintf(w, "%s\n", line) |
404 w.Level += 2 | 263 w.Level += 2 |
405 } else { | 264 } else { |
406 fmt.Fprintf(w, "%s\n", line) | 265 fmt.Fprintf(w, "%s\n", line) |
407 } | 266 } |
408 } | 267 } |
409 } | 268 } |
410 return nil | 269 return nil |
411 }) | 270 }) |
412 } | 271 } |
413 | 272 |
414 // DumpTo formats the Header and Annotations for this RenderedFrame. | 273 // dumpTo formats the Header and annotations for this renderedFrame. |
415 func (r *RenderedFrame) DumpTo(w io.Writer, idx int) (n int, err error) { | 274 func (r *renderedFrame) dumpTo(w io.Writer, idx int) (n int, err error) { |
416 return iotools.WriteTracker(w, func(rawWriter io.Writer) error { | 275 return iotools.WriteTracker(w, func(rawWriter io.Writer) error { |
417 w := &indented.Writer{Writer: rawWriter, UseSpaces: true} | 276 w := &indented.Writer{Writer: rawWriter, UseSpaces: true} |
418 | 277 |
419 » » fmt.Fprintf(w, "#%d %s/%s:%d - %s()\n", idx, r.Pkg, r.File, | 278 » » fmt.Fprintf(w, "#%d %s/%s:%d - %s()\n", idx, r.pkg, r.file, |
420 » » » r.LineNum, r.FuncName) | 279 » » » r.lineNum, r.funcName) |
421 w.Level += 2 | 280 w.Level += 2 |
422 » » switch len(r.Annotations) { | 281 » » switch len(r.annotations) { |
423 case 0: | 282 case 0: |
424 // pass | 283 // pass |
425 case 1: | 284 case 1: |
426 » » » for _, line := range r.Annotations[0] { | 285 » » » for _, line := range r.annotations[0] { |
427 fmt.Fprintf(w, "%s\n", line) | 286 fmt.Fprintf(w, "%s\n", line) |
428 } | 287 } |
429 default: | 288 default: |
430 » » » for i, ann := range r.Annotations { | 289 » » » for i, ann := range r.annotations { |
431 fmt.Fprintf(w, "annotation #%d:\n", i) | 290 fmt.Fprintf(w, "annotation #%d:\n", i) |
432 w.Level += 2 | 291 w.Level += 2 |
433 for _, line := range ann { | 292 for _, line := range ann { |
434 fmt.Fprintf(w, "%s\n", line) | 293 fmt.Fprintf(w, "%s\n", line) |
435 } | 294 } |
436 w.Level -= 2 | 295 w.Level -= 2 |
437 } | 296 } |
438 } | 297 } |
439 return nil | 298 return nil |
440 }) | 299 }) |
441 } | 300 } |
442 | 301 |
443 // RenderedStack is a single rendered stack from one goroutine. | 302 // renderedStack is a single rendered stack from one goroutine. |
444 type RenderedStack struct { | 303 type renderedStack struct { |
445 » GoID goroutine.ID | 304 » goID goroutine.ID |
Vadim Sh.
2017/06/29 02:08:31
:)
| |
446 » Frames []*RenderedFrame | 305 » frames []*renderedFrame |
447 } | 306 } |
448 | 307 |
449 // DumpTo formats the full stack. | 308 // dumpTo formats the full stack. |
450 func (r *RenderedStack) DumpTo(w io.Writer, excludePkgs ...string) (n int, err e rror) { | 309 func (r *renderedStack) dumpTo(w io.Writer, excludePkgs ...string) (n int, err e rror) { |
451 excludeSet := stringset.NewFromSlice(excludePkgs...) | 310 excludeSet := stringset.NewFromSlice(excludePkgs...) |
452 | 311 |
453 return iotools.WriteTracker(w, func(w io.Writer) error { | 312 return iotools.WriteTracker(w, func(w io.Writer) error { |
454 » » fmt.Fprintf(w, "goroutine %d:\n", r.GoID) | 313 » » fmt.Fprintf(w, "goroutine %d:\n", r.goID) |
455 | 314 |
456 lastIdx := 0 | 315 lastIdx := 0 |
457 needNL := false | 316 needNL := false |
458 skipCount := 0 | 317 skipCount := 0 |
459 skipPkg := "" | 318 skipPkg := "" |
460 flushSkips := func(extra string) { | 319 flushSkips := func(extra string) { |
461 if skipCount != 0 { | 320 if skipCount != 0 { |
462 if needNL { | 321 if needNL { |
463 w.Write(nlSlice) | 322 w.Write(nlSlice) |
464 needNL = false | 323 needNL = false |
465 } | 324 } |
466 fmt.Fprintf(w, "... skipped %d frames in pkg %q. ..\n%s", skipCount, skipPkg, extra) | 325 fmt.Fprintf(w, "... skipped %d frames in pkg %q. ..\n%s", skipCount, skipPkg, extra) |
467 skipCount = 0 | 326 skipCount = 0 |
468 skipPkg = "" | 327 skipPkg = "" |
469 } | 328 } |
470 } | 329 } |
471 » » for i, f := range r.Frames { | 330 » » for i, f := range r.frames { |
472 if needNL { | 331 if needNL { |
473 w.Write(nlSlice) | 332 w.Write(nlSlice) |
474 needNL = false | 333 needNL = false |
475 } | 334 } |
476 » » » if excludeSet.Has(f.Pkg) { | 335 » » » if excludeSet.Has(f.pkg) { |
477 » » » » if skipPkg == f.Pkg { | 336 » » » » if skipPkg == f.pkg { |
478 skipCount++ | 337 skipCount++ |
479 } else { | 338 } else { |
480 flushSkips("") | 339 flushSkips("") |
481 skipCount++ | 340 skipCount++ |
482 » » » » » skipPkg = f.Pkg | 341 » » » » » skipPkg = f.pkg |
483 } | 342 } |
484 continue | 343 continue |
485 } | 344 } |
486 flushSkips("\n") | 345 flushSkips("\n") |
487 » » » if len(f.Wrappers) > 0 { | 346 » » » if len(f.wrappers) > 0 { |
488 f.DumpWrappersTo(w, lastIdx, i) | 347 f.DumpWrappersTo(w, lastIdx, i) |
489 w.Write(nlSlice) | 348 w.Write(nlSlice) |
490 } | 349 } |
491 » » » if len(f.Annotations) > 0 { | 350 » » » if len(f.annotations) > 0 { |
492 lastIdx = i | 351 lastIdx = i |
493 needNL = true | 352 needNL = true |
494 } | 353 } |
495 » » » f.DumpTo(w, i) | 354 » » » f.dumpTo(w, i) |
496 } | 355 } |
497 flushSkips("") | 356 flushSkips("") |
498 | 357 |
499 return nil | 358 return nil |
500 }) | 359 }) |
501 } | 360 } |
502 | 361 |
503 // RenderedError is a series of RenderedStacks, one for each goroutine that the | 362 // renderedError is a series of RenderedStacks, one for each goroutine that the |
Vadim Sh.
2017/06/29 02:08:31
tbh, I don't understand why we want to keep render
iannucci
2017/06/29 23:22:51
yeah we don't I'll do this in a followup
| |
504 // error was annotated on. | 363 // error was annotated on. |
505 type RenderedError struct { | 364 type renderedError struct { |
506 » OriginalError string | 365 » originalError string |
507 » Stacks []*RenderedStack | 366 » stacks []*renderedStack |
508 } | 367 } |
509 | 368 |
510 // ToLines renders a full-information stack trace as a series of lines. | 369 // toLines renders a full-information stack trace as a series of lines. |
511 func (r *RenderedError) ToLines(excludePkgs ...string) Lines { | 370 func (r *renderedError) toLines(excludePkgs ...string) lines { |
512 buf := bytes.Buffer{} | 371 buf := bytes.Buffer{} |
513 » r.DumpTo(&buf, excludePkgs...) | 372 » r.dumpTo(&buf, excludePkgs...) |
514 return strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n") | 373 return strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n") |
515 } | 374 } |
516 | 375 |
517 // DumpTo writes the full-information stack trace to the writer. | 376 // dumpTo writes the full-information stack trace to the writer. |
518 func (r *RenderedError) DumpTo(w io.Writer, excludePkgs ...string) (n int, err e rror) { | 377 func (r *renderedError) dumpTo(w io.Writer, excludePkgs ...string) (n int, err e rror) { |
519 return iotools.WriteTracker(w, func(w io.Writer) error { | 378 return iotools.WriteTracker(w, func(w io.Writer) error { |
520 » » if r.OriginalError != "" { | 379 » » if r.originalError != "" { |
521 » » » fmt.Fprintf(w, "original error: %s\n\n", r.OriginalError ) | 380 » » » fmt.Fprintf(w, "original error: %s\n\n", r.originalError ) |
522 } | 381 } |
523 | 382 |
524 » » for i := len(r.Stacks) - 1; i >= 0; i-- { | 383 » » for i := len(r.stacks) - 1; i >= 0; i-- { |
525 » » » if i != len(r.Stacks)-1 { | 384 » » » if i != len(r.stacks)-1 { |
526 w.Write(nlSlice) | 385 w.Write(nlSlice) |
527 } | 386 } |
528 » » » r.Stacks[i].DumpTo(w, excludePkgs...) | 387 » » » r.stacks[i].dumpTo(w, excludePkgs...) |
529 } | 388 } |
530 return nil | 389 return nil |
531 }) | 390 }) |
532 } | 391 } |
533 | 392 |
534 func frameHeaderDetails(frm uintptr) (pkg, filename, funcname string, lineno int ) { | 393 func frameHeaderDetails(frm uintptr) (pkg, filename, funcName string, lineno int ) { |
535 // this `frm--` is to get the correct line/function information, since t he | 394 // this `frm--` is to get the correct line/function information, since t he |
536 // Frame is actually the `return` pc. See runtime.Callers. | 395 // Frame is actually the `return` pc. See runtime.Callers. |
537 frm-- | 396 frm-- |
538 | 397 |
539 fn := runtime.FuncForPC(frm) | 398 fn := runtime.FuncForPC(frm) |
540 file, lineno := fn.FileLine(frm) | 399 file, lineno := fn.FileLine(frm) |
541 | 400 |
542 var dirpath string | 401 var dirpath string |
543 dirpath, filename = filepath.Split(file) | 402 dirpath, filename = filepath.Split(file) |
544 pkgTopLevelName := filepath.Base(dirpath) | 403 pkgTopLevelName := filepath.Base(dirpath) |
545 | 404 |
546 fnName := fn.Name() | 405 fnName := fn.Name() |
547 lastSlash := strings.LastIndex(fnName, "/") | 406 lastSlash := strings.LastIndex(fnName, "/") |
548 if lastSlash == -1 { | 407 if lastSlash == -1 { |
549 » » funcname = fnName | 408 » » funcName = fnName |
550 pkg = pkgTopLevelName | 409 pkg = pkgTopLevelName |
551 } else { | 410 } else { |
552 » » funcname = fnName[lastSlash+1:] | 411 » » funcName = fnName[lastSlash+1:] |
553 pkg = fmt.Sprintf("%s/%s", fnName[:lastSlash], pkgTopLevelName) | 412 pkg = fmt.Sprintf("%s/%s", fnName[:lastSlash], pkgTopLevelName) |
554 } | 413 } |
555 return | 414 return |
556 } | 415 } |
557 | 416 |
558 // RenderStack renders the error to a RenderedError. | 417 // RenderStack renders the error to a list of lines. |
559 func RenderStack(err error) *RenderedError { | 418 func RenderStack(err error, excludePkgs ...string) []string { |
560 » ret := &RenderedError{} | 419 » return renderStack(err).toLines(excludePkgs...) |
420 } | |
421 | |
422 func renderStack(err error) *renderedError { | |
423 » ret := &renderedError{} | |
561 | 424 |
562 lastAnnotatedFrame := 0 | 425 lastAnnotatedFrame := 0 |
563 » var wrappers = []Lines{} | 426 » var wrappers = []lines{} |
564 » getCurFrame := func(fi *stackFrameInfo) *RenderedFrame { | 427 » getCurFrame := func(fi *stackFrameInfo) *renderedFrame { |
565 » » if len(ret.Stacks) == 0 || ret.Stacks[len(ret.Stacks)-1].GoID != fi.forStack.id { | 428 » » if len(ret.stacks) == 0 || ret.stacks[len(ret.stacks)-1].goID != fi.forStack.id { |
566 lastAnnotatedFrame = len(fi.forStack.frames) - 1 | 429 lastAnnotatedFrame = len(fi.forStack.frames) - 1 |
567 » » » toAdd := &RenderedStack{ | 430 » » » toAdd := &renderedStack{ |
568 » » » » GoID: fi.forStack.id, | 431 » » » » goID: fi.forStack.id, |
569 » » » » Frames: make([]*RenderedFrame, len(fi.forStack.f rames)), | 432 » » » » frames: make([]*renderedFrame, len(fi.forStack.f rames)), |
570 } | 433 } |
571 for i, frm := range fi.forStack.frames { | 434 for i, frm := range fi.forStack.frames { |
572 pkgPath, filename, functionName, line := frameHe aderDetails(frm) | 435 pkgPath, filename, functionName, line := frameHe aderDetails(frm) |
573 » » » » toAdd.Frames[i] = &RenderedFrame{ | 436 » » » » toAdd.frames[i] = &renderedFrame{ |
574 » » » » » Pkg: pkgPath, File: filename, LineNum: l ine, FuncName: functionName} | 437 » » » » » pkg: pkgPath, file: filename, lineNum: l ine, funcName: functionName} |
575 } | 438 } |
576 » » » ret.Stacks = append(ret.Stacks, toAdd) | 439 » » » ret.stacks = append(ret.stacks, toAdd) |
577 } | 440 } |
578 » » curStack := ret.Stacks[len(ret.Stacks)-1] | 441 » » curStack := ret.stacks[len(ret.stacks)-1] |
579 | 442 |
580 if fi.frameIdx < lastAnnotatedFrame { | 443 if fi.frameIdx < lastAnnotatedFrame { |
581 lastAnnotatedFrame = fi.frameIdx | 444 lastAnnotatedFrame = fi.frameIdx |
582 » » » frm := curStack.Frames[lastAnnotatedFrame] | 445 » » » frm := curStack.frames[lastAnnotatedFrame] |
583 » » » frm.Wrappers = wrappers | 446 » » » frm.wrappers = wrappers |
584 wrappers = nil | 447 wrappers = nil |
585 return frm | 448 return frm |
586 } | 449 } |
587 » » return curStack.Frames[lastAnnotatedFrame] | 450 » » return curStack.frames[lastAnnotatedFrame] |
588 } | 451 } |
589 | 452 |
590 for err != nil { | 453 for err != nil { |
591 if sc, ok := err.(stackContexter); ok { | 454 if sc, ok := err.(stackContexter); ok { |
592 ctx := sc.stackContext() | 455 ctx := sc.stackContext() |
593 if stk := ctx.frameInfo.forStack; stk != nil { | 456 if stk := ctx.frameInfo.forStack; stk != nil { |
594 frm := getCurFrame(&ctx.frameInfo) | 457 frm := getCurFrame(&ctx.frameInfo) |
595 if rendered := ctx.render(); len(rendered) > 0 { | 458 if rendered := ctx.render(); len(rendered) > 0 { |
596 » » » » » frm.Annotations = append(frm.Annotations , rendered) | 459 » » » » » frm.annotations = append(frm.annotations , rendered) |
597 } | 460 } |
598 } else { | 461 } else { |
599 wrappers = append(wrappers, ctx.render()) | 462 wrappers = append(wrappers, ctx.render()) |
600 } | 463 } |
601 } else { | 464 } else { |
602 » » » wrappers = append(wrappers, Lines{fmt.Sprintf("unknown w rapper %T", err)}) | 465 » » » wrappers = append(wrappers, lines{fmt.Sprintf("unknown w rapper %T", err)}) |
603 } | 466 } |
604 switch x := err.(type) { | 467 switch x := err.(type) { |
605 case MultiError: | 468 case MultiError: |
606 // TODO(riannucci): it's kinda dumb that we have to walk the MultiError | 469 // TODO(riannucci): it's kinda dumb that we have to walk the MultiError |
607 // twice (once in its stackContext method, and again her e). | 470 // twice (once in its stackContext method, and again her e). |
608 err = x.First() | 471 err = x.First() |
609 case Wrapped: | 472 case Wrapped: |
610 err = x.InnerError() | 473 err = x.InnerError() |
611 default: | 474 default: |
612 » » » ret.OriginalError = err.Error() | 475 » » » ret.originalError = err.Error() |
613 err = nil | 476 err = nil |
614 } | 477 } |
615 } | 478 } |
616 | 479 |
617 return ret | 480 return ret |
618 } | 481 } |
619 | 482 |
620 // Annotate captures the current stack frame and returns a new annotatable | 483 // Annotate captures the current stack frame and returns a new annotatable |
621 // error. You can add additional metadata to this error with its methods and | 484 // error, attaching the publically readable `reason` format string to the error. |
622 // then get the new derived error with the Err() function. | 485 // You can add additional metadata to this error with the 'InternalReason' and |
486 // 'Tag' methods, and then obtain a real `error` with the Err() function. | |
623 // | 487 // |
624 // If this is passed nil, it will return a no-op Annotator whose .Err() function | 488 // If this is passed nil, it will return a no-op Annotator whose .Err() function |
625 // will also return nil. | 489 // will also return nil. |
626 // | 490 // |
627 // The original error may be recovered by using Wrapped.InnerError on the | 491 // The original error may be recovered by using Wrapped.InnerError on the |
628 // returned error. | 492 // returned error. |
629 // | 493 // |
630 // Rendering the derived error with Error() will render a summary version of all | 494 // Rendering the derived error with Error() will render a summary version of all |
631 // the Reasons as well as the initial underlying errors Error() text. It is | 495 // the public 'reasons' as well as the initial underlying error's Error() text. |
632 // intended that the initial underlying error and all annotated Reasons only | 496 // It is intended that the initial underlying error and all annotated reasons |
633 // contain user-visible information, so that the accumulated error may be | 497 // only contain user-visible information, so that the accumulated error may be |
634 // returned to the user without worrying about leakage. | 498 // returned to the user without worrying about leakage. |
635 func Annotate(err error) *Annotator { | 499 // |
500 // You should assume that end-users (including unauthenticated end users) may | |
501 // see the text in the 'reason' field here. To only attach an internal reason, | |
Vadim Sh.
2017/06/29 02:08:31
how do you choose when to use ' and when ` ? :) Th
iannucci
2017/06/29 23:22:51
Done.
| |
502 // leave the `reason` argument blank and don't pass any additional formatting | |
503 // arguments. | |
504 // | |
505 // The `reason` string is formatted with `args` and may contain Sprintf-style | |
506 // formatting directives. | |
507 func Annotate(err error, reason string, args ...interface{}) *Annotator { | |
636 if err == nil { | 508 if err == nil { |
637 return nil | 509 return nil |
638 } | 510 } |
639 » return &Annotator{err, stackContext{frameInfo: stackFrameInfoForError(1, err)}} | 511 » return &Annotator{err, stackContext{ |
512 » » frameInfo: stackFrameInfoForError(1, err), | |
513 » » reason: fmt.Sprintf(reason, args...), | |
514 » }} | |
640 } | 515 } |
641 | 516 |
642 // Reason builds a new Annotator starting with reason. This allows you to use | 517 // Reason builds a new Annotator starting with reason. This allows you to use |
643 // all the formatting directives you would normally use with Annotate, in case | 518 // all the formatting directives you would normally use with Annotate, in case |
644 // your originating error needs formatting directives: | 519 // your originating error needs tags or an internal reason. |
645 // | 520 // |
646 // errors.Reason("something bad: %(value)d").D("value", 100)).Err() | 521 // errors.Reason("something bad: %d", value).Tag(transient.Tag).Err() |
647 // | 522 // |
648 // Prefer this form to errors.New(fmt.Sprintf("...")) | 523 // Prefer this form to errors.New(fmt.Sprintf("...")) |
649 func Reason(reason string) *Annotator { | 524 func Reason(reason string, args ...interface{}) *Annotator { |
650 currentStack := captureStack(1) | 525 currentStack := captureStack(1) |
651 frameInfo := stackFrameInfo{0, currentStack} | 526 frameInfo := stackFrameInfo{0, currentStack} |
652 » return (&Annotator{nil, stackContext{frameInfo: frameInfo}}).Reason(reas on) | 527 » return (&Annotator{nil, stackContext{ |
528 » » frameInfo: frameInfo, | |
529 » » reason: fmt.Sprintf(reason, args...), | |
530 » }}) | |
653 } | 531 } |
654 | 532 |
655 // New is an API-compatible version of the standard errors.New function. Unlike | 533 // New is an API-compatible version of the standard errors.New function. Unlike |
656 // the stdlib errors.New, this will capture the current stack information at the | 534 // the stdlib errors.New, this will capture the current stack information at the |
657 // place this error was created. | 535 // place this error was created. |
658 func New(msg string, tags ...TagValueGenerator) error { | 536 func New(msg string, tags ...TagValueGenerator) error { |
659 tse := &terminalStackError{ | 537 tse := &terminalStackError{ |
660 errors.New(msg), stackFrameInfo{forStack: captureStack(1)}, nil} | 538 errors.New(msg), stackFrameInfo{forStack: captureStack(1)}, nil} |
661 if len(tags) > 0 { | 539 if len(tags) > 0 { |
662 tse.tags = make(map[TagKey]interface{}, len(tags)) | 540 tse.tags = make(map[TagKey]interface{}, len(tags)) |
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
707 if currentlyCapturedStack == nil || currentStack.id != currentlyCaptured Stack.id { | 585 if currentlyCapturedStack == nil || currentStack.id != currentlyCaptured Stack.id { |
708 // This is the very first annotation on this error OR | 586 // This is the very first annotation on this error OR |
709 // We switched goroutines. | 587 // We switched goroutines. |
710 return stackFrameInfo{forStack: currentStack} | 588 return stackFrameInfo{forStack: currentStack} |
711 } | 589 } |
712 return stackFrameInfo{ | 590 return stackFrameInfo{ |
713 frameIdx: currentlyCapturedStack.findPointOfDivergence(currentSt ack), | 591 frameIdx: currentlyCapturedStack.findPointOfDivergence(currentSt ack), |
714 forStack: currentlyCapturedStack, | 592 forStack: currentlyCapturedStack, |
715 } | 593 } |
716 } | 594 } |
OLD | NEW |