stacktrace.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. // Copyright 2011 The Go Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. // Some code from the runtime/debug package of the Go standard library.
  5. package raven
  6. import (
  7. "bytes"
  8. "go/build"
  9. "io/ioutil"
  10. "path/filepath"
  11. "runtime"
  12. "strings"
  13. "sync"
  14. "github.com/pkg/errors"
  15. )
  16. // https://docs.getsentry.com/hosted/clientdev/interfaces/#failure-interfaces
  17. type Stacktrace struct {
  18. // Required
  19. Frames []*StacktraceFrame `json:"frames"`
  20. }
  21. func (s *Stacktrace) Class() string { return "stacktrace" }
  22. func (s *Stacktrace) Culprit() string {
  23. for i := len(s.Frames) - 1; i >= 0; i-- {
  24. frame := s.Frames[i]
  25. if frame.InApp == true && frame.Module != "" && frame.Function != "" {
  26. return frame.Module + "." + frame.Function
  27. }
  28. }
  29. return ""
  30. }
  31. type StacktraceFrame struct {
  32. // At least one required
  33. Filename string `json:"filename,omitempty"`
  34. Function string `json:"function,omitempty"`
  35. Module string `json:"module,omitempty"`
  36. // Optional
  37. Lineno int `json:"lineno,omitempty"`
  38. Colno int `json:"colno,omitempty"`
  39. AbsolutePath string `json:"abs_path,omitempty"`
  40. ContextLine string `json:"context_line,omitempty"`
  41. PreContext []string `json:"pre_context,omitempty"`
  42. PostContext []string `json:"post_context,omitempty"`
  43. InApp bool `json:"in_app"`
  44. }
  45. // Try to get stacktrace from err as an interface of github.com/pkg/errors, or else NewStacktrace()
  46. func GetOrNewStacktrace(err error, skip int, context int, appPackagePrefixes []string) *Stacktrace {
  47. stacktracer, errHasStacktrace := err.(interface {
  48. StackTrace() errors.StackTrace
  49. })
  50. if errHasStacktrace {
  51. var frames []*StacktraceFrame
  52. for _, f := range stacktracer.StackTrace() {
  53. pc := uintptr(f) - 1
  54. fn := runtime.FuncForPC(pc)
  55. var file string
  56. var line int
  57. if fn != nil {
  58. file, line = fn.FileLine(pc)
  59. } else {
  60. file = "unknown"
  61. }
  62. frame := NewStacktraceFrame(pc, file, line, context, appPackagePrefixes)
  63. if frame != nil {
  64. frames = append([]*StacktraceFrame{frame}, frames...)
  65. }
  66. }
  67. return &Stacktrace{Frames: frames}
  68. } else {
  69. return NewStacktrace(skip+1, context, appPackagePrefixes)
  70. }
  71. }
  72. // Intialize and populate a new stacktrace, skipping skip frames.
  73. //
  74. // context is the number of surrounding lines that should be included for context.
  75. // Setting context to 3 would try to get seven lines. Setting context to -1 returns
  76. // one line with no surrounding context, and 0 returns no context.
  77. //
  78. // appPackagePrefixes is a list of prefixes used to check whether a package should
  79. // be considered "in app".
  80. func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktrace {
  81. var frames []*StacktraceFrame
  82. for i := 1 + skip; ; i++ {
  83. pc, file, line, ok := runtime.Caller(i)
  84. if !ok {
  85. break
  86. }
  87. frame := NewStacktraceFrame(pc, file, line, context, appPackagePrefixes)
  88. if frame != nil {
  89. frames = append(frames, frame)
  90. }
  91. }
  92. // If there are no frames, the entire stacktrace is nil
  93. if len(frames) == 0 {
  94. return nil
  95. }
  96. // Optimize the path where there's only 1 frame
  97. if len(frames) == 1 {
  98. return &Stacktrace{frames}
  99. }
  100. // Sentry wants the frames with the oldest first, so reverse them
  101. for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
  102. frames[i], frames[j] = frames[j], frames[i]
  103. }
  104. return &Stacktrace{frames}
  105. }
  106. // Build a single frame using data returned from runtime.Caller.
  107. //
  108. // context is the number of surrounding lines that should be included for context.
  109. // Setting context to 3 would try to get seven lines. Setting context to -1 returns
  110. // one line with no surrounding context, and 0 returns no context.
  111. //
  112. // appPackagePrefixes is a list of prefixes used to check whether a package should
  113. // be considered "in app".
  114. func NewStacktraceFrame(pc uintptr, file string, line, context int, appPackagePrefixes []string) *StacktraceFrame {
  115. frame := &StacktraceFrame{AbsolutePath: file, Filename: trimPath(file), Lineno: line, InApp: false}
  116. frame.Module, frame.Function = functionName(pc)
  117. // `runtime.goexit` is effectively a placeholder that comes from
  118. // runtime/asm_amd64.s and is meaningless.
  119. if frame.Module == "runtime" && frame.Function == "goexit" {
  120. return nil
  121. }
  122. if frame.Module == "main" {
  123. frame.InApp = true
  124. } else {
  125. for _, prefix := range appPackagePrefixes {
  126. if strings.HasPrefix(frame.Module, prefix) && !strings.Contains(frame.Module, "vendor") && !strings.Contains(frame.Module, "third_party") {
  127. frame.InApp = true
  128. }
  129. }
  130. }
  131. if context > 0 {
  132. contextLines, lineIdx := fileContext(file, line, context)
  133. if len(contextLines) > 0 {
  134. for i, line := range contextLines {
  135. switch {
  136. case i < lineIdx:
  137. frame.PreContext = append(frame.PreContext, string(line))
  138. case i == lineIdx:
  139. frame.ContextLine = string(line)
  140. default:
  141. frame.PostContext = append(frame.PostContext, string(line))
  142. }
  143. }
  144. }
  145. } else if context == -1 {
  146. contextLine, _ := fileContext(file, line, 0)
  147. if len(contextLine) > 0 {
  148. frame.ContextLine = string(contextLine[0])
  149. }
  150. }
  151. return frame
  152. }
  153. // Retrieve the name of the package and function containing the PC.
  154. func functionName(pc uintptr) (pack string, name string) {
  155. fn := runtime.FuncForPC(pc)
  156. if fn == nil {
  157. return
  158. }
  159. name = fn.Name()
  160. // We get this:
  161. // runtime/debug.*T·ptrmethod
  162. // and want this:
  163. // pack = runtime/debug
  164. // name = *T.ptrmethod
  165. if idx := strings.LastIndex(name, "."); idx != -1 {
  166. pack = name[:idx]
  167. name = name[idx+1:]
  168. }
  169. name = strings.Replace(name, "·", ".", -1)
  170. return
  171. }
  172. var fileCacheLock sync.Mutex
  173. var fileCache = make(map[string][][]byte)
  174. func fileContext(filename string, line, context int) ([][]byte, int) {
  175. fileCacheLock.Lock()
  176. defer fileCacheLock.Unlock()
  177. lines, ok := fileCache[filename]
  178. if !ok {
  179. data, err := ioutil.ReadFile(filename)
  180. if err != nil {
  181. // cache errors as nil slice: code below handles it correctly
  182. // otherwise when missing the source or running as a different user, we try
  183. // reading the file on each error which is unnecessary
  184. fileCache[filename] = nil
  185. return nil, 0
  186. }
  187. lines = bytes.Split(data, []byte{'\n'})
  188. fileCache[filename] = lines
  189. }
  190. if lines == nil {
  191. // cached error from ReadFile: return no lines
  192. return nil, 0
  193. }
  194. line-- // stack trace lines are 1-indexed
  195. start := line - context
  196. var idx int
  197. if start < 0 {
  198. start = 0
  199. idx = line
  200. } else {
  201. idx = context
  202. }
  203. end := line + context + 1
  204. if line >= len(lines) {
  205. return nil, 0
  206. }
  207. if end > len(lines) {
  208. end = len(lines)
  209. }
  210. return lines[start:end], idx
  211. }
  212. var trimPaths []string
  213. // Try to trim the GOROOT or GOPATH prefix off of a filename
  214. func trimPath(filename string) string {
  215. for _, prefix := range trimPaths {
  216. if trimmed := strings.TrimPrefix(filename, prefix); len(trimmed) < len(filename) {
  217. return trimmed
  218. }
  219. }
  220. return filename
  221. }
  222. func init() {
  223. // Collect all source directories, and make sure they
  224. // end in a trailing "separator"
  225. for _, prefix := range build.Default.SrcDirs() {
  226. if prefix[len(prefix)-1] != filepath.Separator {
  227. prefix += string(filepath.Separator)
  228. }
  229. trimPaths = append(trimPaths, prefix)
  230. }
  231. }