-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstacktrace.go
More file actions
208 lines (191 loc) · 6.51 KB
/
stacktrace.go
File metadata and controls
208 lines (191 loc) · 6.51 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
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
// Package stacktrace provides optional stack trace support for errx errors.
//
// This package extends errx with stack trace capabilities while keeping the core
// errx package minimal and zero-dependency. It offers two usage patterns:
//
// 1. Per-error opt-in using Here() as a Classified:
// err := errx.Wrap("context", cause, ErrNotFound, stacktrace.Here())
//
// 2. Convenience functions that automatically capture traces:
// err := stacktrace.Wrap("context", cause, ErrNotFound)
//
// Stack traces can be extracted from any error in the chain using Extract():
//
// frames := stacktrace.Extract(err)
// for _, frame := range frames {
// fmt.Printf("%s:%d %s\n", frame.File, frame.Line, frame.Function)
// }
package stacktrace
import (
"errors"
"fmt"
"runtime"
"github.com/go-extras/errx"
)
// Frame represents a single stack frame with file, line, and function information.
type Frame struct {
File string // Full path to the source file
Line int // Line number in the source file
Function string // Fully qualified function name
}
// String returns a formatted representation of the frame.
func (f Frame) String() string {
return fmt.Sprintf("%s:%d %s", f.File, f.Line, f.Function)
}
// traced is an internal type that implements errx.Classified and captures stack trace.
type traced struct {
pcs []uintptr // Program counters captured from the stack
}
// Error returns a string representation of the traced error.
// This is primarily for debugging; the trace itself is accessed via Extract().
func (t *traced) Error() string {
frames := t.frames()
if len(frames) == 0 {
return "(empty stack trace)"
}
return fmt.Sprintf("stack trace: %d frames", len(frames))
}
// frames converts the stored program counters into Frame structs.
// This is done lazily to avoid the cost of frame resolution unless needed.
func (t *traced) frames() []Frame {
if len(t.pcs) == 0 {
return nil
}
frames := runtime.CallersFrames(t.pcs)
var result []Frame
for {
frame, more := frames.Next()
result = append(result, Frame{
File: frame.File,
Line: frame.Line,
Function: frame.Function,
})
if !more {
break
}
}
return result
}
// IsClassified implements the errx.Classified interface marker method.
// It always returns true to identify this as a Classified error.
func (*traced) IsClassified() bool {
return true
}
// Here captures the current stack trace and returns it as an errx.Classified.
// It can be used with errx.Wrap() or errx.Classify() to attach stack traces to errors.
//
// The stack trace is captured starting from the caller of Here(), skipping the
// Here() function itself and the runtime.Callers call.
//
// Example:
//
// err := errx.Wrap("operation failed", cause, ErrNotFound, stacktrace.Here())
//
// The captured stack trace can later be extracted using Extract().
func Here() errx.Classified {
return captureStack(2) // Skip Here() and runtime.Callers
}
// captureStack captures the current stack trace with the specified skip count.
// skip indicates how many stack frames to skip (0 = captureStack itself).
func captureStack(skip int) *traced {
const maxDepth = 32 // Reasonable default depth limit
pcs := make([]uintptr, maxDepth)
n := runtime.Callers(skip+1, pcs) // +1 to skip captureStack itself
return &traced{pcs: pcs[:n]}
}
// Extract returns stack frames from the first traced error found in the error chain.
// It traverses the entire error chain looking for a traced error and returns its frames.
//
// Returns nil if the error is nil or does not contain any stack trace.
//
// Example:
//
// frames := stacktrace.Extract(err)
// if frames != nil {
// for _, frame := range frames {
// fmt.Printf("%s:%d %s\n", frame.File, frame.Line, frame.Function)
// }
// }
func Extract(err error) []Frame {
if err == nil {
return nil
}
// Use errors.As to find the first traced error in the chain
var t *traced
if errors.As(err, &t) {
return t.frames()
}
return nil
}
// Wrap wraps an error with additional context text and optional classifications,
// automatically capturing a stack trace at the call site.
//
// This is a convenience function equivalent to:
//
// errx.Wrap(text, cause, append(classifications, stacktrace.Here())...)
//
// If cause is nil, Wrap returns nil.
//
// Example:
//
// err := stacktrace.Wrap("failed to process order", cause, ErrNotFound)
func Wrap(text string, cause error, classifications ...errx.Classified) error {
if cause == nil {
return nil
}
// Capture stack with skip=2 to skip Wrap() and runtime.Callers
trace := captureStack(2)
classifications = append(classifications, trace)
return errx.Wrap(text, cause, classifications...)
}
// Classify attaches one or more classifications to an error, automatically
// capturing a stack trace at the call site.
//
// This is a convenience function equivalent to:
//
// errx.Classify(cause, append(classifications, stacktrace.Here())...)
//
// If cause is nil, Classify returns nil.
//
// Example:
//
// err := stacktrace.Classify(cause, ErrNotFound)
func Classify(cause error, classifications ...errx.Classified) error {
if cause == nil {
return nil
}
// Capture stack with skip=2 to skip Classify() and runtime.Callers
trace := captureStack(2)
classifications = append(classifications, trace)
return errx.Classify(cause, classifications...)
}
// ClassifyNew creates a new error with the given text and immediately classifies it
// with one or more classifications, automatically capturing a stack trace at the call site.
//
// This is a convenience function equivalent to:
//
// errx.ClassifyNew(text, append(classifications, stacktrace.Here())...)
//
// This function is useful when you want to create a new error, classify it, and
// capture a stack trace in a single step, reducing verbosity.
//
// Example:
//
// var ErrNotFound = errx.NewSentinel("not found")
// var ErrDatabase = errx.NewSentinel("database error")
//
// // Instead of:
// // err := stacktrace.Classify(errors.New("user record missing"), ErrNotFound, ErrDatabase)
//
// // You can write:
// err := stacktrace.ClassifyNew("user record missing", ErrNotFound, ErrDatabase)
//
// fmt.Println(err.Error()) // Output: user record missing
// fmt.Println(errors.Is(err, ErrNotFound)) // Output: true
// fmt.Println(stacktrace.Extract(err) != nil) // Output: true
func ClassifyNew(text string, classifications ...errx.Classified) error {
// Capture stack with skip=2 to skip ClassifyNew() and runtime.Callers
trace := captureStack(2)
classifications = append(classifications, trace)
return errx.ClassifyNew(text, classifications...)
}