stacktrace.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. package sentry
  2. import (
  3. "go/build"
  4. "path/filepath"
  5. "reflect"
  6. "regexp"
  7. "runtime"
  8. "strings"
  9. )
  10. const unknown string = "unknown"
  11. // The module download is split into two parts: downloading the go.mod and downloading the actual code.
  12. // If you have dependencies only needed for tests, then they will show up in your go.mod,
  13. // and go get will download their go.mods, but it will not download their code.
  14. // The test-only dependencies get downloaded only when you need it, such as the first time you run go test.
  15. //
  16. // https://github.com/golang/go/issues/26913#issuecomment-411976222
  17. // Stacktrace holds information about the frames of the stack.
  18. type Stacktrace struct {
  19. Frames []Frame `json:"frames,omitempty"`
  20. FramesOmitted []uint `json:"frames_omitted,omitempty"`
  21. }
  22. // NewStacktrace creates a stacktrace using `runtime.Callers`.
  23. func NewStacktrace() *Stacktrace {
  24. pcs := make([]uintptr, 100)
  25. n := runtime.Callers(1, pcs)
  26. if n == 0 {
  27. return nil
  28. }
  29. frames := extractFrames(pcs[:n])
  30. frames = filterFrames(frames)
  31. stacktrace := Stacktrace{
  32. Frames: frames,
  33. }
  34. return &stacktrace
  35. }
  36. // ExtractStacktrace creates a new `Stacktrace` based on the given `error` object.
  37. // TODO: Make it configurable so that anyone can provide their own implementation?
  38. // Use of reflection allows us to not have a hard dependency on any given package, so we don't have to import it
  39. func ExtractStacktrace(err error) *Stacktrace {
  40. method := extractReflectedStacktraceMethod(err)
  41. if !method.IsValid() {
  42. return nil
  43. }
  44. pcs := extractPcs(method)
  45. if len(pcs) == 0 {
  46. return nil
  47. }
  48. frames := extractFrames(pcs)
  49. frames = filterFrames(frames)
  50. stacktrace := Stacktrace{
  51. Frames: frames,
  52. }
  53. return &stacktrace
  54. }
  55. func extractReflectedStacktraceMethod(err error) reflect.Value {
  56. var method reflect.Value
  57. // https://github.com/pingcap/errors
  58. methodGetStackTracer := reflect.ValueOf(err).MethodByName("GetStackTracer")
  59. // https://github.com/pkg/errors
  60. methodStackTrace := reflect.ValueOf(err).MethodByName("StackTrace")
  61. // https://github.com/go-errors/errors
  62. methodStackFrames := reflect.ValueOf(err).MethodByName("StackFrames")
  63. if methodGetStackTracer.IsValid() {
  64. stacktracer := methodGetStackTracer.Call(make([]reflect.Value, 0))[0]
  65. stacktracerStackTrace := reflect.ValueOf(stacktracer).MethodByName("StackTrace")
  66. if stacktracerStackTrace.IsValid() {
  67. method = stacktracerStackTrace
  68. }
  69. }
  70. if methodStackTrace.IsValid() {
  71. method = methodStackTrace
  72. }
  73. if methodStackFrames.IsValid() {
  74. method = methodStackFrames
  75. }
  76. return method
  77. }
  78. func extractPcs(method reflect.Value) []uintptr {
  79. var pcs []uintptr
  80. stacktrace := method.Call(make([]reflect.Value, 0))[0]
  81. if stacktrace.Kind() != reflect.Slice {
  82. return nil
  83. }
  84. for i := 0; i < stacktrace.Len(); i++ {
  85. pc := stacktrace.Index(i)
  86. if pc.Kind() == reflect.Uintptr {
  87. pcs = append(pcs, uintptr(pc.Uint()))
  88. continue
  89. }
  90. if pc.Kind() == reflect.Struct {
  91. field := pc.FieldByName("ProgramCounter")
  92. if field.IsValid() && field.Kind() == reflect.Uintptr {
  93. pcs = append(pcs, uintptr(field.Uint()))
  94. continue
  95. }
  96. }
  97. }
  98. return pcs
  99. }
  100. // https://docs.sentry.io/development/sdk-dev/interfaces/stacktrace/
  101. type Frame struct {
  102. Function string `json:"function,omitempty"`
  103. Symbol string `json:"symbol,omitempty"`
  104. Module string `json:"module,omitempty"`
  105. Package string `json:"package,omitempty"`
  106. Filename string `json:"filename,omitempty"`
  107. AbsPath string `json:"abs_path,omitempty"`
  108. Lineno int `json:"lineno,omitempty"`
  109. Colno int `json:"colno,omitempty"`
  110. PreContext []string `json:"pre_context,omitempty"`
  111. ContextLine string `json:"context_line,omitempty"`
  112. PostContext []string `json:"post_context,omitempty"`
  113. InApp bool `json:"in_app,omitempty"`
  114. Vars map[string]interface{} `json:"vars,omitempty"`
  115. }
  116. // NewFrame assembles a stacktrace frame out of `runtime.Frame`.
  117. func NewFrame(f runtime.Frame) Frame {
  118. abspath := f.File
  119. filename := f.File
  120. function := f.Function
  121. var module string
  122. if filename != "" {
  123. filename = extractFilename(filename)
  124. } else {
  125. filename = unknown
  126. }
  127. if abspath == "" {
  128. abspath = unknown
  129. }
  130. if function != "" {
  131. module, function = deconstructFunctionName(function)
  132. }
  133. frame := Frame{
  134. AbsPath: abspath,
  135. Filename: filename,
  136. Lineno: f.Line,
  137. Module: module,
  138. Function: function,
  139. }
  140. frame.InApp = isInAppFrame(frame)
  141. return frame
  142. }
  143. func extractFrames(pcs []uintptr) []Frame {
  144. var frames []Frame
  145. callersFrames := runtime.CallersFrames(pcs)
  146. for {
  147. callerFrame, more := callersFrames.Next()
  148. frames = append([]Frame{
  149. NewFrame(callerFrame),
  150. }, frames...)
  151. if !more {
  152. break
  153. }
  154. }
  155. return frames
  156. }
  157. func filterFrames(frames []Frame) []Frame {
  158. isTestFileRegexp := regexp.MustCompile(`getsentry/sentry-go/.+_test.go`)
  159. isExampleFileRegexp := regexp.MustCompile(`getsentry/sentry-go/example/`)
  160. filteredFrames := make([]Frame, 0, len(frames))
  161. for _, frame := range frames {
  162. // go runtime frames
  163. if frame.Module == "runtime" || frame.Module == "testing" {
  164. continue
  165. }
  166. // sentry internal frames
  167. isTestFile := isTestFileRegexp.MatchString(frame.AbsPath)
  168. isExampleFile := isExampleFileRegexp.MatchString(frame.AbsPath)
  169. if strings.Contains(frame.AbsPath, "github.com/getsentry/sentry-go") &&
  170. !isTestFile &&
  171. !isExampleFile {
  172. continue
  173. }
  174. filteredFrames = append(filteredFrames, frame)
  175. }
  176. return filteredFrames
  177. }
  178. func extractFilename(path string) string {
  179. _, file := filepath.Split(path)
  180. return file
  181. }
  182. func isInAppFrame(frame Frame) bool {
  183. if strings.HasPrefix(frame.AbsPath, build.Default.GOROOT) ||
  184. strings.Contains(frame.Module, "vendor") ||
  185. strings.Contains(frame.Module, "third_party") {
  186. return false
  187. }
  188. return true
  189. }
  190. // Transform `runtime/debug.*T·ptrmethod` into `{ module: runtime/debug, function: *T.ptrmethod }`
  191. func deconstructFunctionName(name string) (module string, function string) {
  192. if idx := strings.LastIndex(name, "."); idx != -1 {
  193. module = name[:idx]
  194. function = name[idx+1:]
  195. }
  196. function = strings.Replace(function, "·", ".", -1)
  197. return module, function
  198. }
  199. func callerFunctionName() string {
  200. pcs := make([]uintptr, 1)
  201. runtime.Callers(3, pcs)
  202. callersFrames := runtime.CallersFrames(pcs)
  203. callerFrame, _ := callersFrames.Next()
  204. _, function := deconstructFunctionName(callerFrame.Function)
  205. return function
  206. }