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