github.com/blend/go-sdk@v1.20220411.3/ex/stack_trace.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package ex 9 10 import ( 11 "encoding/json" 12 "fmt" 13 "path" 14 "runtime" 15 "strings" 16 ) 17 18 // StackTraceProvider is a type that can return an exception class. 19 type StackTraceProvider interface { 20 StackTrace() StackTrace 21 } 22 23 // GetStackTrace is a utility method to get the current stack trace at call time. 24 func GetStackTrace() string { 25 return fmt.Sprintf("%+v", Callers(DefaultStartDepth)) 26 } 27 28 // Callers returns stack pointers. 29 func Callers(startDepth int) StackPointers { 30 const depth = 32 31 var pcs [depth]uintptr 32 n := runtime.Callers(startDepth, pcs[:]) 33 var st StackPointers = pcs[0:n] 34 return st 35 } 36 37 // StackTrace is a stack trace provider. 38 type StackTrace interface { 39 fmt.Formatter 40 Strings() []string 41 String() string 42 } 43 44 // StackPointers is stack of uintptr stack frames from innermost (newest) to outermost (oldest). 45 type StackPointers []uintptr 46 47 // Format formats the stack trace. 48 func (st StackPointers) Format(s fmt.State, verb rune) { 49 switch verb { 50 case 'v': 51 switch { 52 case s.Flag('+'): 53 for _, f := range st { 54 fmt.Fprintf(s, "\n%+v", Frame(f)) 55 } 56 case s.Flag('#'): 57 for _, f := range st { 58 fmt.Fprintf(s, "\n%#v", Frame(f)) 59 } 60 default: 61 for _, f := range st { 62 fmt.Fprintf(s, "\n%v", Frame(f)) 63 } 64 } 65 case 's': 66 for _, f := range st { 67 fmt.Fprintf(s, "\n%s", Frame(f)) 68 } 69 } 70 } 71 72 // Strings dereferences the StackTrace as a string slice 73 func (st StackPointers) Strings() []string { 74 res := make([]string, len(st)) 75 for i, frame := range st { 76 res[i] = fmt.Sprintf("%+v", Frame(frame)) 77 } 78 return res 79 } 80 81 // String returns a single string representation of the stack pointers. 82 func (st StackPointers) String() string { 83 return fmt.Sprintf("%+v", st) 84 } 85 86 //MarshalJSON is a custom json marshaler. 87 func (st StackPointers) MarshalJSON() ([]byte, error) { 88 return json.Marshal(st.Strings()) 89 } 90 91 // StackStrings represents a stack trace as string literals. 92 type StackStrings []string 93 94 // Format formats the stack trace. 95 func (ss StackStrings) Format(s fmt.State, verb rune) { 96 switch verb { 97 case 'v': 98 switch { 99 case s.Flag('+'): 100 for _, f := range ss { 101 fmt.Fprintf(s, "\n%+v", f) 102 } 103 case s.Flag('#'): 104 fmt.Fprintf(s, "%#v", []string(ss)) 105 default: 106 for _, f := range ss { 107 fmt.Fprintf(s, "\n%v", f) 108 } 109 } 110 case 's': 111 for _, f := range ss { 112 fmt.Fprintf(s, "\n%v", f) 113 } 114 } 115 } 116 117 // Strings returns the stack strings as a string slice. 118 func (ss StackStrings) Strings() []string { 119 return []string(ss) 120 } 121 122 // String returns a single string representation of the stack pointers. 123 func (ss StackStrings) String() string { 124 return fmt.Sprintf("%+v", ss) 125 } 126 127 //MarshalJSON is a custom json marshaler. 128 func (ss StackStrings) MarshalJSON() ([]byte, error) { 129 return json.Marshal(ss) 130 } 131 132 // Frame represents a program counter inside a stack frame. 133 type Frame uintptr 134 135 // PC returns the program counter for this frame; 136 // multiple frames may have the same PC value. 137 func (f Frame) PC() uintptr { return uintptr(f) - 1 } 138 139 // File returns the full path to the file that contains the 140 // function for this Frame's pc. 141 func (f Frame) File() string { 142 fn := runtime.FuncForPC(f.PC()) 143 if fn == nil { 144 return "unknown" 145 } 146 file, _ := fn.FileLine(f.PC()) 147 return file 148 } 149 150 // Line returns the line number of source code of the 151 // function for this Frame's pc. 152 func (f Frame) Line() int { 153 fn := runtime.FuncForPC(f.PC()) 154 if fn == nil { 155 return 0 156 } 157 _, line := fn.FileLine(f.PC()) 158 return line 159 } 160 161 // Func returns the func name. 162 func (f Frame) Func() string { 163 name := runtime.FuncForPC(f.PC()).Name() 164 return funcname(name) 165 } 166 167 // Format formats the frame according to the fmt.Formatter interface. 168 // 169 // %s source file 170 // %d source line 171 // %n function name 172 // %v equivalent to %s:%d 173 // 174 // Format accepts flags that alter the printing of some verbs, as follows: 175 // 176 // %+s path of source file relative to the compile time GOPATH 177 // %+v equivalent to %+s:%d 178 func (f Frame) Format(s fmt.State, verb rune) { 179 switch verb { 180 case 's': 181 switch { 182 case s.Flag('+'): 183 pc := f.PC() 184 fn := runtime.FuncForPC(pc) 185 if fn == nil { 186 fmt.Fprint(s, "unknown") 187 } else { 188 file, _ := fn.FileLine(pc) 189 fname := fn.Name() 190 fmt.Fprintf(s, "%s\n\t%s", fname, trimGOPATH(fname, file)) 191 } 192 default: 193 fmt.Fprint(s, path.Base(f.File())) 194 } 195 case 'd': 196 fmt.Fprintf(s, "%d", f.Line()) 197 case 'n': 198 name := runtime.FuncForPC(f.PC()).Name() 199 fmt.Fprint(s, funcname(name)) 200 case 'v': 201 f.Format(s, 's') 202 fmt.Fprint(s, ":") 203 f.Format(s, 'd') 204 } 205 } 206 207 // funcname removes the path prefix component of a function's name reported by func.Name(). 208 func funcname(name string) string { 209 i := strings.LastIndex(name, "/") 210 name = name[i+1:] 211 i = strings.Index(name, ".") 212 return name[i+1:] 213 } 214 215 func trimGOPATH(name, file string) string { 216 // Here we want to get the source file path relative to the compile time 217 // GOPATH. As of Go 1.6.x there is no direct way to know the compiled 218 // GOPATH at runtime, but we can infer the number of path segments in the 219 // GOPATH. We note that fn.Name() returns the function name qualified by 220 // the import path, which does not include the GOPATH. Thus we can trim 221 // segments from the beginning of the file path until the number of path 222 // separators remaining is one more than the number of path separators in 223 // the function name. For example, given: 224 // 225 // GOPATH /home/user 226 // file /home/user/src/pkg/sub/file.go 227 // fn.Name() pkg/sub.Type.Method 228 // 229 // We want to produce: 230 // 231 // pkg/sub/file.go 232 // 233 // From this we can easily see that fn.Name() has one less path separator 234 // than our desired output. We count separators from the end of the file 235 // path until it finds two more than in the function name and then move 236 // one character forward to preserve the initial path segment without a 237 // leading separator. 238 const sep = "/" 239 goal := strings.Count(name, sep) + 2 240 i := len(file) 241 for n := 0; n < goal; n++ { 242 i = strings.LastIndex(file[:i], sep) 243 if i == -1 { 244 // not enough separators found, set i so that the slice expression 245 // below leaves file unmodified 246 i = -len(sep) 247 break 248 } 249 } 250 // get back to 0 or trim the leading separator 251 file = file[i+len(sep):] 252 return file 253 }