stacktrace.go 7.4 KB

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