-
Notifications
You must be signed in to change notification settings - Fork 6
/
oops.go
395 lines (351 loc) · 11.2 KB
/
oops.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
// +build !js
package oops
import (
"bytes"
"fmt"
"io"
"runtime"
"strings"
)
// stack is a comparable []uintptr slice.
type stack struct {
frames []uintptr
}
// A oopsError annotates a cause error with a stacktrace and an explanatory
// message.
type oopsError struct {
// inner is the next non-oops error in the chain.
inner error
// The previous oopsError, if any. This value is only used to follow stacktraces.
previous *oopsError
// The current stacktrace. Might be the same as previous' stacktrace if that
// is another oopsError.
stack *stack
// A small explanatory message what went wrong at this level in the stack.
reason string
// The index of the stack frame where this oopsError was added.
index int
}
// Error implements error, and outputs a full backtrace.
func (e *oopsError) Error() string {
var buffer bytes.Buffer
e.writeStackTrace(&buffer)
return buffer.String()
}
// MainStackToString will write the frames of the main goroutine to a string.
// This will return an empty string if the error is not an oopsError.
func MainStackToString(err error) string {
var e *oopsError
if ok := As(err, &e); !ok {
return ""
}
var base error
for err := error(e); err != nil; err = Unwrap(err) {
base = err
}
var buffer bytes.Buffer
fmt.Fprintf(&buffer, "%s", base.Error())
frames := Frames(err)
if frames == nil || len(frames) == 0 {
return ""
}
fmt.Fprintf(&buffer, "\n\n")
writeSingleFrameTrace(&buffer, frames[0])
return buffer.String()
}
// writeSingleFrameTrace writes the stack trace of frames into the writer.
func writeSingleFrameTrace(w io.Writer, frames []Frame) {
for _, frame := range frames {
// Print the current function.
if frame.Reason != "" {
fmt.Fprintf(w, "%s: %s\n", frame.Function, frame.Reason)
} else {
fmt.Fprintf(w, "%s\n", frame.Function)
}
fmt.Fprintf(w, "\t%s:%d\n", frame.File, frame.Line)
}
}
// Unwrap returns the next error in the error chain.
// If there is no next error, Unwrap returns nil.
func (err *oopsError) Unwrap() error {
// Unwrap doesn't follow the err.previous chain because that chain is only
// used for constructing Frames. err.inner is used for following wrapped error types.
if err.inner != nil {
return err.inner
}
return nil
}
type stackWithReasons struct {
stack *stack
reasons []string
}
// Frames extracts all frames from an oops error. If err is not an oops error,
// nil is returned.
func Frames(err error) [][]Frame {
var e *oopsError
if ok := As(err, &e); !ok {
return nil
}
// Walk the chain of oopsErrors backwards, collecting a set of stacks and
// reasons.
stacks := make([]stackWithReasons, 0, 8)
for ; e != nil; e = e.previous {
// If the current error's stack is different from the previous, add it to
// the set of stacks.
if len(stacks) == 0 || stacks[len(stacks)-1].stack != e.stack {
stacks = append(stacks, stackWithReasons{
stack: e.stack,
reasons: make([]string, len(e.stack.frames)),
})
}
// Store the reason with its stack frame.
stacks[len(stacks)-1].reasons[e.index] = e.reason
}
parsedStacks := make([][]Frame, 0, len(stacks))
// Walk the set of stacks backwards, starting with stack most closest to the
// cause error.
for i := len(stacks) - 1; i >= 0; i-- {
frames := stacks[i].stack.frames
reasons := stacks[i].reasons
parsedFrames := make([]Frame, 0, 8)
// Iterate over the stack frames.
iter := runtime.CallersFrames(frames)
// j tracks the index in the combined frames / reasons array of iter' stack
// frame. Each frame in frames / reasons array appears at least once in the
// iterator's frames, but the iterator's frame might have more frames (for
// example, cgo frames, or inlined frames.)
j := 0
for {
frame, ok := iter.Next()
if !ok {
break
}
// Advance j and load the reason whenever the current iterator's frame
// matches. The iterator's frame's PC might differ by 1 because the
// iterator adjusts for the difference between callsite and return
// address.
var reason string
if j < len(frames) && (frame.PC == frames[j] || frame.PC+1 == frames[j]) {
reason = reasons[j]
j++
}
file := frame.File
i := strings.LastIndex(file, "/src/")
if i >= 0 {
file = file[i+len("/src/"):]
}
parsedFrames = append(parsedFrames, Frame{
File: file,
Function: frame.Function,
Line: frame.Line,
Reason: reason,
})
}
parsedStacks = append(parsedStacks, parsedFrames)
}
return parsedStacks
}
// SkipFrames skips numFrames from the stack trace and returns a new copy of the error.
// If numFrames is greater than the number of frames in err, SkipFrames will do nothing and return the original err.
func SkipFrames(err error, numFrames int) error {
var e *oopsError
if ok := As(err, &e); !ok || numFrames <= 0 {
return err
}
st := e.stack
if st == nil {
return err
}
numLeftoverFrames := len(st.frames) - int(numFrames)
if numLeftoverFrames < 0 {
return err
}
frames := make([]uintptr, numLeftoverFrames)
copy(frames, st.frames[numFrames:])
return &oopsError{
inner: e.inner,
previous: e.previous,
stack: &stack{frames: frames},
reason: e.reason,
index: e.index,
}
}
// writeStackTrace unwinds a chain of oopsErrors and prints the stacktrace
// annotated with explanatory messages.
func (e *oopsError) writeStackTrace(w io.Writer) {
var base error
var fallbackBase error
for err := error(e); err != nil; err = Unwrap(err) {
if _, ok := err.(*oopsError); ok {
// We've found another oops error in the chain, our "base" is no longer valid.
// This is possible if another error wraps the oops error:
// e.g. Oops1 -> nonOopsWrappedErr -> Oops2 -> realError
base = nil
} else if base == nil {
// We've found a non-oops error, and this is the first non-oops error we've
// found in the current chain.
//
// e.g. for the wrapped chain Oops1 -> Oops2 -> networkErr -> realError
// we want to mark the "base" as networkErr (not realErr)
base = err
}
fallbackBase = err
}
if base == nil {
// This code probably shouldn't be necessary as long as oops errors can't
// be at the end of the chain (I'm paranoid).
base = fallbackBase
}
fmt.Fprintf(w, "%s\n\n", base.Error())
for i, stack := range Frames(e) {
// Include a newline between stacks.
if i > 0 {
fmt.Fprintf(w, "\n")
}
writeSingleFrameTrace(w, stack)
}
}
// Reason returns the reason chain of the error. Output can be an empty string.
// NOTE: This does not include inner error in the reason message.
func (e *oopsError) Reason() string {
output := []string{}
err := e
for err != nil {
if err.reason != "" {
output = append(output, err.reason)
}
err = err.previous
}
return strings.Join(output, ": ")
}
// isPrefix checks if a is a prefix of b.
func isPrefix(a []uintptr, b []uintptr) bool {
if len(a) > len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func wrapf(err error, reason string) error {
inner := err
var previous *oopsError
var st *stack
var index int
found := false
// Find the previous error in our input, if any.
var e *oopsError
if ok := As(err, &e); ok {
previous = e
// If the input error was not an oops error, then we want our new oops error
// to point to it, even if it goes on to point to other oops errors. Otherwise,
// we're in the case of wrapping an already oops-wrapped error and we can point to
// the same next error.
if _, ok := err.(*oopsError); ok {
inner = e.inner
}
// Figure out where we are in the existing callstack. Since Wrapf isn't
// guaranteed to be called at every stack frame, we need to search to find
// the current callsite. We start searching one level past the previous
// level (and assume that Wrapf is called at most once per stack).
st = e.stack
index = e.index + 1
// To check where we are, match a number of return frames in the stack. We
// check one level deeper than the level we are annotating, because the
// frame in the calling function likely doesn't match:
//
// - parent() calls Wrapf and return an error with a stacktrace - child()
// check cause's return value and then calls Wrapf on parent's error -
// compare() is the frame that gets compared
//
// When parent calls Wrapf and captures the stack frame, the program
// counter in child will point the if statement that checks the parent's
// return value. When the child then calls Wrapf, it's program counter
// will have advanced to the Wrapf call, and will no longer match the
// program originally captured by the parent. However, the program counter
// in compare will still match, and so we compare against that.
//
// To paper over small numbers of dupliate frames (eg. when using
// recursion), we compare not just 1 frame, but several. We compare only
// some frames (instead of all) to keep the runtime of Wrapf efficient.
var buffer [8]uintptr
// 0 is the frame of Callers, 1 is us, 2 is the public wrapper, 3 is its
// caller (child), 4 is the caller's caller (compare).
compare := buffer[:runtime.Callers(4, buffer[:])]
for index+1 < len(st.frames) {
if isPrefix(compare, st.frames[index+1:]) {
found = true
break
}
index++
}
}
if !found {
var buffer [256]uintptr
// 0 is the frame of Callers, 1 is us, 2 is the public wrapper, 3 is its
// caller.
n := runtime.Callers(3, buffer[:])
frames := make([]uintptr, n)
copy(frames, buffer[:n])
index = 0
st = &stack{frames: frames}
}
return &oopsError{
inner: inner,
previous: previous,
stack: st,
reason: reason,
index: index,
}
}
// Errorf creates a new error with a reason and a stacktrace.
//
// Use Errorf in places where you would otherwise return an error using
// fmt.Errorf or errors.New.
//
// Note that the result of Errorf includes a stacktrace. This means
// that Errorf is not suitable for storing in global variables. For
// such errors, keep using errors.New.
func Errorf(format string, a ...interface{}) error {
return wrapf(fmt.Errorf(format, a...), "")
}
// Wrapf annotates an error with a reason and a stacktrace. If err is nil,
// Wrapf returns nil.
//
// Use Wrapf in places where you would otherwise return an error directly. If
// the error passed to Wrapf is nil, Wrapf will also return nil. This makes it
// safe to use in one-line return statements.
//
// To check if a wrapped error is a specific error, such as io.EOF, you can
// extract the error passed in to Wrapf using Cause.
func Wrapf(err error, format string, a ...interface{}) error {
if err == nil {
return nil
}
return wrapf(err, fmt.Sprintf(format, a...))
}
// Cause extracts the cause error of an oops error. If err is not an oops
// error, err itself is returned.
//
// You can use Cause to check if an error is an expected error. For example, if
// you know than EOF error is fine, you can handle it with Cause.
func Cause(err error) error {
if e, ok := err.(*oopsError); ok {
return e.inner
}
return err
}
// Recover recovers from a panic in a defer. If there is no panic, Recover()
// returns nil. To use, call oops.Recover(recover()) and compare the result to nil.
func Recover(p interface{}) error {
if p == nil {
return nil
}
if err, ok := p.(error); ok {
return wrapf(err, "recovered panic")
}
return wrapf(fmt.Errorf("recovered panic: %v", p), "")
}