forked from samber/oops
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstacktrace.go
More file actions
322 lines (276 loc) · 11 KB
/
stacktrace.go
File metadata and controls
322 lines (276 loc) · 11 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
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
package oops
import (
"fmt"
"reflect"
"runtime"
"strings"
"github.com/samber/lo"
)
///
/// Stack trace generation and processing functionality.
///
/// This module provides comprehensive stack trace capture and formatting
/// capabilities for oops.OopsError instances. It includes intelligent
/// frame filtering to exclude irrelevant stack frames and provides
/// formatted output suitable for debugging and logging.
///
/// Inspired by palantir/stacktrace repo
/// -> https://github.com/palantir/stacktrace/blob/master/stacktrace.go
/// -> Apache 2.0 LICENSE
///
// fake is a dummy struct used to determine the current package name
// for stack trace filtering. This allows the package to identify
// and filter out frames from its own code while preserving frames
// from user code and examples.
type fake struct{}
// Global configuration for stack trace generation
var (
// StackTraceMaxDepth controls the maximum number of stack frames
// to capture in a stack trace. This prevents stack traces from
// becoming excessively long while still providing sufficient
// context for debugging.
//
// The default value of 10 provides a good balance between
// detail and readability. For deep call stacks, this will
// capture the most recent 10 frames, which typically include
// the most relevant debugging information.
StackTraceMaxDepth int = 10
// packageName stores the current package name for frame filtering.
// This is determined at package initialization time and used
// to identify frames that should be excluded from stack traces.
packageName = reflect.TypeOf(fake{}).PkgPath()
)
// oopsStacktraceFrame represents a single frame in a stack trace.
// Each frame contains information about a function call in the
// call stack, including the program counter, file path, function
// name, and line number.
type oopsStacktraceFrame struct {
pc uintptr // Program counter for the function call
file string // File path where the call occurred
function string // Name of the function being called
line int // Line number in the file where the call occurred
}
// String returns a formatted string representation of the stack frame.
// The format follows the standard Go stack trace format: "file:line function()"
// or just "file:line" if no function name is available.
//
// This method is used for both individual frame display and as part
// of complete stack trace formatting.
//
// Example output:
//
// "main.go:42 main()"
// "handler.go:15 processRequest()"
func (frame *oopsStacktraceFrame) String() string {
currentFrame := fmt.Sprintf("%v:%v", frame.file, frame.line)
if frame.function != "" {
currentFrame = fmt.Sprintf("%v:%v %v()", frame.file, frame.line, frame.function)
}
return currentFrame
}
// oopsStacktrace represents a complete stack trace with multiple frames.
// It contains a span identifier for correlation and an ordered list
// of stack frames representing the call hierarchy.
type oopsStacktrace struct {
span string // Unique identifier for the stack trace
frames []oopsStacktraceFrame // Ordered list of stack frames (most recent first)
}
// Error implements the error interface for stack traces.
// This allows stack traces to be used directly as errors if needed.
func (st *oopsStacktrace) Error() string {
return st.String("")
}
// String returns a formatted string representation of the complete stack trace.
// The output includes all frames in the stack trace, formatted with proper
// indentation and structure for readability.
//
// The deepestFrame parameter is used to avoid duplicate frames when
// combining stack traces from nested errors. When a frame matches
// the deepestFrame, the formatting stops to prevent redundancy.
//
// Example output:
//
// " --- at main.go:42 main()
// --- at handler.go:15 processRequest()
// --- at server.go:123 handleHTTP()"
func (st *oopsStacktrace) String(deepestFrame string) string {
var str string
// Helper function to add newlines between frames
newline := func() {
if str != "" && !strings.HasSuffix(str, "\n") {
str += "\n"
}
}
// Iterate through all frames and format them
for _, frame := range st.frames {
if frame.file != "" {
currentFrame := frame.String()
// Stop if we've reached the deepest frame to avoid duplication
if currentFrame == deepestFrame {
break
}
newline()
str += " --- at " + currentFrame
}
}
return str
}
// Source returns the source code context for the first frame in the stack trace.
// This method provides both a header (file:line function()) and the actual
// source code lines around the error location for enhanced debugging.
//
// The source code includes a configurable number of lines before and after
// the error location, with the error line highlighted. This is particularly
// useful for understanding the context in which an error occurred.
//
// Performance: This method involves file I/O operations to read source code,
// which may have performance implications for frequently called code paths.
// The results are cached to minimize repeated file reads.
//
// Returns:
// - header: Formatted string like "main.go:42 main()"
// - body: Slice of strings containing source code lines with line numbers
func (st *oopsStacktrace) Source() (string, []string) {
if len(st.frames) == 0 {
return "", []string{}
}
firstFrame := st.frames[0]
header := firstFrame.String()
body := getSourceFromFrame(firstFrame)
return header, body
}
// newStacktrace creates a new stack trace by capturing the current call stack.
// This function walks up the call stack starting from the caller of this
// function and captures frame information while applying intelligent filtering.
//
// The function implements sophisticated frame filtering to provide relevant
// debugging information while excluding noise:
// - Excludes frames from the Go standard library (GOROOT)
// - Excludes frames from this package (except examples and tests)
// - Limits the number of frames to StackTraceMaxDepth
// - Includes frames from user code and package examples/tests
//
// Performance: This function has O(d) complexity where d is the depth
// of the call stack, with additional overhead for frame filtering and
// function name processing.
//
// Example usage:
//
// stack := newStacktrace("span-123")
// fmt.Println(stack.String(""))
func newStacktrace(span string) *oopsStacktrace {
frames := []oopsStacktraceFrame{}
// Walk up the call stack starting from the caller of this function
// Continue until we have enough frames or run out of stack frames
for i := 1; len(frames) < StackTraceMaxDepth; i++ {
// Get frame information from the runtime
pc, file, line, ok := runtime.Caller(i)
if !ok {
break // No more frames available
}
// Clean up the file path by removing Go path prefixes
file = removeGoPath(file)
// Get function information for the program counter
f := runtime.FuncForPC(pc)
if f == nil {
break // Invalid program counter
}
// Extract a short, readable function name
function := shortFuncName(f)
// Define package name patterns for filtering
packageNameExamples := packageName + "/examples/"
// Apply frame filtering logic
isGoPkg := len(runtime.GOROOT()) > 0 && strings.Contains(file, runtime.GOROOT()) // skip frames in GOROOT if it's set
isOopsPkg := strings.Contains(file, packageName) // skip frames in this package
isExamplePkg := strings.Contains(file, packageNameExamples) // do not skip frames in this package examples
isTestPkg := strings.Contains(file, "_test.go") // do not skip frames in tests
// Include frame if it passes all filtering criteria
if !isGoPkg && (!isOopsPkg || isExamplePkg || isTestPkg) {
frames = append(frames, oopsStacktraceFrame{
pc: pc,
file: file,
function: function,
line: line,
})
}
}
return &oopsStacktrace{
span: span,
frames: frames,
}
}
// shortFuncName extracts a short, readable function name from a runtime.Func.
// This function processes the full function name (which includes package path
// and receiver information) and returns a simplified version suitable for
// display in stack traces.
//
// The function handles various function name formats:
// - Package functions: "github.com/user/pkg.FuncName" -> "FuncName"
// - Methods: "github.com/user/pkg.Receiver.MethodName" -> "MethodName"
// - Pointer methods: "github.com/user/pkg.(*PtrReceiver).MethodName" -> "MethodName"
//
// Example transformations:
//
// "github.com/user/app.(*Handler).ProcessRequest" -> "ProcessRequest"
// "main.main" -> "main"
// "github.com/user/pkg.helper" -> "helper"
func shortFuncName(f *runtime.Func) string {
// f.Name() returns the full function name including package path
// Examples of possible formats:
// - "github.com/palantir/shield/package.FuncName"
// - "github.com/palantir/shield/package.Receiver.MethodName"
// - "github.com/palantir/shield/package.(*PtrReceiver).MethodName"
longName := f.Name()
// Remove the package path by finding the last "/" and taking everything after it
withoutPath := longName[strings.LastIndex(longName, "/")+1:]
// Remove the package name by finding the first "." and taking everything after it
withoutPackage := withoutPath[strings.Index(withoutPath, ".")+1:]
// Clean up the function name by removing parentheses and asterisks
// that are part of pointer receiver syntax
shortName := withoutPackage
shortName = strings.Replace(shortName, "(", "", 1) // Remove opening parenthesis
shortName = strings.Replace(shortName, "*", "", 1) // Remove asterisk
shortName = strings.Replace(shortName, ")", "", 1) // Remove closing parenthesis
return shortName
}
func framesToStacktraceBlocks(blocks []lo.Tuple3[error, string, []oopsStacktraceFrame]) []string {
output := []string{}
shownFrames := make(map[string]bool)
for _, e := range blocks {
err := lo.TernaryF(e.A != nil, func() string { return e.A.Error() }, func() string { return "" })
msg := coalesceOrEmpty(e.B, err, "Error")
// Build stacktrace for this error, avoiding already shown frames
var frameLines []string
firstFrame := true // we always show the first frame, because the PC of a recursive function might appear multiple time.
for _, frame := range e.C {
frameStr := frame.String()
if !shownFrames[frameStr] || firstFrame {
frameLines = append(frameLines, " --- at "+frame.String())
shownFrames[frameStr] = true
}
firstFrame = false
}
stacktraceStr := strings.Join(frameLines, "\n")
block := fmt.Sprintf("%s\n%s", msg, stacktraceStr)
output = append([]string{block}, output...)
}
return output
}
func framesToSourceBlocks(blocks []lo.Tuple2[string, *oopsStacktrace]) []string {
output := [][]string{}
for _, e := range blocks {
header, body := e.B.Source()
if e.A != "" {
header = fmt.Sprintf("%s\n%s", e.A, header)
}
if header != "" && len(body) > 0 {
output = append(
[][]string{append([]string{header}, body...)},
output...,
)
}
}
return lo.Map(output, func(items []string, _ int) string {
return strings.Join(items, "\n")
})
}