-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathstack.go
170 lines (145 loc) · 4.81 KB
/
stack.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
package gerror
import (
"runtime"
"strconv"
"strings"
)
// StackType identifies the depth of a stack desired.
// Generating stacks requires significant computation, smaller stacks use less.
type StackType int
const (
// NoStack means do not generate a stack.
NoStack StackType = 0
// SourceStack retrieves the minimum sack possible to populate "source".
// Note; this is a noop for errors with a defined source.
// CAUTION: this is dubious... We're adding multiple stack elements to try and find
// a more valid stack.... who knows if it will work.
SourceStack StackType = 4
// ShortStack gets a max stack of 16 elements.
ShortStack StackType = 16
// DefaultStack gets a max stack of 32 elements.
DefaultStack StackType = 32
)
// StackSkip indicates how many stack layers to skip to get the correct start point.
type StackSkip int
const (
// defaultSkip is 4 because that is how many layers the stack processor itself consumes.
defaultSkip = 4
)
// A Stack represents each line of a Stack trace.
type Stack []StackElem
// String returns a formatted sting with all.
func (s Stack) String() string {
sb := strings.Builder{}
for _, e := range s {
sb.WriteString("\n" + e.Name)
sb.WriteString("\n\t" + e.File + ":" + strconv.Itoa(e.LineNumber))
}
return sb.String()
}
// NearestExternal finds the firs caller outside this package.
// The effectiveness of this method is limited to the depth of the Stack fetched.
func (s Stack) NearestExternal() StackElem {
// attempt to find the first element not in this package:
if pkgName, ok := getCurrentPackage(); ok {
for _, elem := range s {
if !strings.HasPrefix(elem.Name, pkgName) {
return elem
}
}
}
return s[0]
}
func getCurrentPackage() (string, bool) {
pc, _, _, _ := runtime.Caller(1)
splitName := strings.Split(pcToStackElem(pc).Name, ".")
if len(splitName) == 0 { // should literally be impossible?
return "", false
}
return strings.Join(splitName[:len(splitName)-1], "."), true
}
// StackElem represents a single line in a Stack trace.
type StackElem struct {
// Name is the fully qualified package function path (?).
// e.g. github.com/drshriveer/gtools/gerror.TestGError_WithStack
Name string
// File is the full path of the file.
File string
// LineNumber of the Stack element.
LineNumber int
}
// SourceInfo returns info about where this error was propagated including packageName,
// and other identifying information.
// The ambiguity of the other identifying information is unfortunate; but that's what we're
// starting with. I'd love info here...
func (e StackElem) SourceInfo() (packageName string, parts []string) {
splitName := strings.Split(e.Name, "/")
last := splitName[len(splitName)-1]
// Next step: handle generics which show up as funcName[....]
// I'd love to do:
// last = strings.Replace(last, "[...]", "[T]", 1)
// but this probably isn't metric safe.
// I also assume that [...] handles N types so maybe [T] wouldn't quite work.
// FIXME: very possible the [...] only comes out in specific situaions.
last = strings.TrimSuffix(last, "[...]")
// "last" has a number of variations, I don't yet understand the rules.
// (See tests for examples / more info.)
// But generally, the first 3 nodes seem to be the most useful.
// the first of which is _always_ a package.
vals := strings.Split(last, ".")
if len(vals) >= 1 {
packageName = vals[0]
vals = vals[1:]
}
// cut it down-- anything after funcX is not very useful imo
for i, val := range vals {
// conditions for truncaing the set.
if strings.HasPrefix(val, "func") || val == "" {
vals = vals[:i]
break
}
}
return packageName, vals
}
// Metric returns a metric-safe(?) string of the source info.
func (e StackElem) Metric() string {
// delim is the metric node delimiter.
// TODO: reconsider this delimiter when support for it is clearer.
const delim = ":"
pkg, theRest := e.SourceInfo()
// Experimental .. drop repeats.
outer:
for i := 1; i < len(theRest); i++ {
curr := theRest[i]
for j := 0; j < i; j++ {
if curr == theRest[j] {
// only use everything before the repeat.
theRest = theRest[:i]
break outer
}
}
}
// convertToMetricNode takes a list of string elements and transforms
// them into a delimited metric-safe string skipping any empty entries.
return pkg + delim + strings.Join(theRest, delim)
}
func makeStack(depth StackType, skip StackSkip) Stack {
pcs := make([]uintptr, depth)
n := runtime.Callers(int(skip), pcs)
pcs = pcs[0:n] // drop unwritten elements.
stack := make(Stack, n)
for i := range stack {
stack[i] = pcToStackElem(pcs[i])
}
return stack
}
func pcToStackElem(pc uintptr) StackElem {
pc--
fu := runtime.FuncForPC(pc)
if fu == nil {
return StackElem{Name: "unknown", File: "unknown"}
}
fName, fLine := fu.FileLine(pc)
fu.Entry()
return StackElem{Name: fu.Name(), File: fName, LineNumber: fLine}
}