-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstack.go
More file actions
133 lines (123 loc) · 4.7 KB
/
Copy pathstack.go
File metadata and controls
133 lines (123 loc) · 4.7 KB
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
package errors
import (
stderrors "errors"
"runtime"
"strconv"
"strings"
)
// maxStackDepth caps the number of program counters captured by [WithStack].
// 32 frames is deep enough to pinpoint the origin of any realistic failure
// without bloating every error with the full goroutine stack.
const maxStackDepth = 32
// Frame is one resolved entry in a captured call stack: the function name and
// the file:line where the [WithStack] capture saw it. It is the symbolized,
// human-facing form of a single program counter.
//
// Frames are produced lazily by [StackOf] / [Error.Format], not at capture
// time: [WithStack] stores only the raw program counters (one cheap
// [runtime.Callers] call), and symbolization happens once, on demand, when a
// stack is actually rendered. The vast majority of errors are never printed
// with %+v, so the expensive [runtime.CallersFrames] walk is deferred until it
// is known to be needed.
type Frame struct {
// Function is the fully-qualified function name (for example
// "github.com/pleme-io/foo.(*T).Method").
Function string
// File is the absolute source path of the call site.
File string
// Line is the 1-based line number within File.
Line int
}
// String renders a frame as "function\n\tfile:line", matching the format the
// Go runtime uses for panic traces so captured stacks read identically to
// native ones.
func (f Frame) String() string {
return f.Function + "\n\t" + f.File + ":" + strconv.Itoa(f.Line)
}
// WithStack captures the call stack at the point the option runs, recording it
// on the error being built. Capture is cheap (a single [runtime.Callers] call
// storing raw program counters); symbolization is deferred to render time (see
// [Frame]). The stack is invisible in [Error.Error] — it surfaces only through
// [StackOf] or the "%+v" verb of [Error.Format], so adding provenance never
// changes a single-line error message.
//
// Capture the stack as close to the failure as possible:
//
// if err := step(); err != nil {
// return errs.Wrap(err, "reconcile", errs.WithStack())
// }
//
// Only the first stack in a chain matters: [StackOf] returns the deepest
// (innermost) captured stack, which is the one nearest the originating call,
// so re-capturing on every wrap is wasteful but harmless.
func WithStack() Option {
return func(e *Error) {
// Skip runtime.Callers, this closure, and the option-applying loop in
// New/Wrap so frame 0 is the caller of New/Wrap.
var pcs [maxStackDepth]uintptr
n := runtime.Callers(3, pcs[:])
e.stack = pcs[:n:n]
}
}
// stackCarrier is any error that carries captured program counters. *[Error]
// implements it; an external error type can opt in to [StackOf] without
// depending on this package by exposing the same method (Cheney's
// behaviour-over-type, Law 5).
type stackCarrier interface {
error
// StackPCs returns the raw, unsymbolized program counters of the capture,
// innermost frame first, or nil if no stack was captured.
StackPCs() []uintptr
}
// StackPCs returns this layer's captured program counters (nil if [WithStack]
// was not used). It satisfies [stackCarrier] so [StackOf] can find the stack
// while walking a chain. Prefer the package-level [StackOf], which returns the
// resolved [Frame] slice and walks the whole chain.
func (e *Error) StackPCs() []uintptr { return e.stack }
// StackOf returns the resolved call stack of err, symbolizing on demand. It
// walks the cause chain and returns the deepest (innermost) captured stack —
// the one nearest the originating failure — so that wrapping with additional
// [WithStack] options never hides the most useful trace. It returns nil when
// err is nil or when nothing in the chain captured a stack.
func StackOf(err error) []Frame {
var deepest []uintptr
for err != nil {
var c stackCarrier
if !stderrors.As(err, &c) {
break
}
if pcs := c.StackPCs(); pcs != nil {
deepest = pcs // keep descending; innermost wins.
}
err = stderrors.Unwrap(c)
}
return framesOf(deepest)
}
// framesOf symbolizes raw program counters into [Frame] values, runtime
// internals trimmed. Returns nil for an empty input so callers can test the
// "no stack" case with a plain nil check.
func framesOf(pcs []uintptr) []Frame {
if len(pcs) == 0 {
return nil
}
frames := runtime.CallersFrames(pcs)
var out []Frame
for {
fr, more := frames.Next()
if fr.Function != "" {
out = append(out, Frame{Function: fr.Function, File: fr.File, Line: fr.Line})
}
if !more {
break
}
}
return out
}
// formatStack appends the resolved stack to b in the runtime's panic-trace
// shape, one frame per "function\n\tfile:line" pair.
func formatStack(b *strings.Builder, frames []Frame) {
for _, fr := range frames {
b.WriteByte('\n')
b.WriteString(fr.String())
}
}