1
0

stacktrace.go 7.6 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. "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 fName string
  56. var file string
  57. var line int
  58. if fn != nil {
  59. file, line = fn.FileLine(pc)
  60. fName = fn.Name()
  61. } else {
  62. file = "unknown"
  63. fName = "unknown"
  64. }
  65. frame := NewStacktraceFrame(pc, fName, file, line, context, appPackagePrefixes)
  66. if frame != nil {
  67. frames = append([]*StacktraceFrame{frame}, frames...)
  68. }
  69. }
  70. return &Stacktrace{Frames: frames}
  71. } else {
  72. return NewStacktrace(skip+1, context, appPackagePrefixes)
  73. }
  74. }
  75. // Intialize and populate a new stacktrace, skipping skip frames.
  76. //
  77. // context is the number of surrounding lines that should be included for context.
  78. // Setting context to 3 would try to get seven lines. Setting context to -1 returns
  79. // one line with no surrounding context, and 0 returns no context.
  80. //
  81. // appPackagePrefixes is a list of prefixes used to check whether a package should
  82. // be considered "in app".
  83. func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktrace {
  84. var frames []*StacktraceFrame
  85. callerPcs := make([]uintptr, 100)
  86. numCallers := runtime.Callers(skip+2, callerPcs)
  87. // If there are no callers, the entire stacktrace is nil
  88. if numCallers == 0 {
  89. return nil
  90. }
  91. callersFrames := runtime.CallersFrames(callerPcs)
  92. for {
  93. fr, more := callersFrames.Next()
  94. if fr.Func != nil {
  95. frame := NewStacktraceFrame(fr.PC, fr.Function, fr.File, fr.Line, context, appPackagePrefixes)
  96. if frame != nil {
  97. frames = append(frames, frame)
  98. }
  99. }
  100. if !more {
  101. break
  102. }
  103. }
  104. // If there are no frames, the entire stacktrace is nil
  105. if len(frames) == 0 {
  106. return nil
  107. }
  108. // Optimize the path where there's only 1 frame
  109. if len(frames) == 1 {
  110. return &Stacktrace{frames}
  111. }
  112. // Sentry wants the frames with the oldest first, so reverse them
  113. for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
  114. frames[i], frames[j] = frames[j], frames[i]
  115. }
  116. return &Stacktrace{frames}
  117. }
  118. // Build a single frame using data returned from runtime.Caller.
  119. //
  120. // context is the number of surrounding lines that should be included for context.
  121. // Setting context to 3 would try to get seven lines. Setting context to -1 returns
  122. // one line with no surrounding context, and 0 returns no context.
  123. //
  124. // appPackagePrefixes is a list of prefixes used to check whether a package should
  125. // be considered "in app".
  126. func NewStacktraceFrame(pc uintptr, fName, file string, line, context int, appPackagePrefixes []string) *StacktraceFrame {
  127. frame := &StacktraceFrame{AbsolutePath: file, Filename: trimPath(file), Lineno: line, InApp: false}
  128. frame.Module, frame.Function = functionName(fName)
  129. // `runtime.goexit` is effectively a placeholder that comes from
  130. // runtime/asm_amd64.s and is meaningless.
  131. if frame.Module == "runtime" && frame.Function == "goexit" {
  132. return nil
  133. }
  134. if frame.Module == "main" {
  135. frame.InApp = true
  136. } else {
  137. for _, prefix := range appPackagePrefixes {
  138. if strings.HasPrefix(frame.Module, prefix) && !strings.Contains(frame.Module, "vendor") && !strings.Contains(frame.Module, "third_party") {
  139. frame.InApp = true
  140. }
  141. }
  142. }
  143. if context > 0 {
  144. contextLines, lineIdx := sourceCodeLoader.Load(file, line, context)
  145. if len(contextLines) > 0 {
  146. for i, line := range contextLines {
  147. switch {
  148. case i < lineIdx:
  149. frame.PreContext = append(frame.PreContext, string(line))
  150. case i == lineIdx:
  151. frame.ContextLine = string(line)
  152. default:
  153. frame.PostContext = append(frame.PostContext, string(line))
  154. }
  155. }
  156. }
  157. } else if context == -1 {
  158. contextLine, _ := sourceCodeLoader.Load(file, line, 0)
  159. if len(contextLine) > 0 {
  160. frame.ContextLine = string(contextLine[0])
  161. }
  162. }
  163. return frame
  164. }
  165. // Retrieve the name of the package and function containing the PC.
  166. func functionName(fName string) (pack string, name string) {
  167. name = fName
  168. // We get this:
  169. // runtime/debug.*T·ptrmethod
  170. // and want this:
  171. // pack = runtime/debug
  172. // name = *T.ptrmethod
  173. if idx := strings.LastIndex(name, "."); idx != -1 {
  174. pack = name[:idx]
  175. name = name[idx+1:]
  176. }
  177. name = strings.Replace(name, "·", ".", -1)
  178. return
  179. }
  180. type SourceCodeLoader interface {
  181. Load(filename string, line, context int) ([][]byte, int)
  182. }
  183. var sourceCodeLoader SourceCodeLoader = &fsLoader{cache: make(map[string][][]byte)}
  184. func SetSourceCodeLoader(loader SourceCodeLoader) {
  185. sourceCodeLoader = loader
  186. }
  187. type fsLoader struct {
  188. mu sync.Mutex
  189. cache map[string][][]byte
  190. }
  191. func (fs *fsLoader) Load(filename string, line, context int) ([][]byte, int) {
  192. fs.mu.Lock()
  193. defer fs.mu.Unlock()
  194. lines, ok := fs.cache[filename]
  195. if !ok {
  196. data, err := ioutil.ReadFile(filename)
  197. if err != nil {
  198. // cache errors as nil slice: code below handles it correctly
  199. // otherwise when missing the source or running as a different user, we try
  200. // reading the file on each error which is unnecessary
  201. fs.cache[filename] = nil
  202. return nil, 0
  203. }
  204. lines = bytes.Split(data, []byte{'\n'})
  205. fs.cache[filename] = lines
  206. }
  207. if lines == nil {
  208. // cached error from ReadFile: return no lines
  209. return nil, 0
  210. }
  211. line-- // stack trace lines are 1-indexed
  212. start := line - context
  213. var idx int
  214. if start < 0 {
  215. start = 0
  216. idx = line
  217. } else {
  218. idx = context
  219. }
  220. end := line + context + 1
  221. if line >= len(lines) {
  222. return nil, 0
  223. }
  224. if end > len(lines) {
  225. end = len(lines)
  226. }
  227. return lines[start:end], idx
  228. }
  229. var trimPaths []string
  230. // Try to trim the GOROOT or GOPATH prefix off of a filename
  231. func trimPath(filename string) string {
  232. for _, prefix := range trimPaths {
  233. if trimmed := strings.TrimPrefix(filename, prefix); len(trimmed) < len(filename) {
  234. return trimmed
  235. }
  236. }
  237. return filename
  238. }
  239. func init() {
  240. // Collect all source directories, and make sure they
  241. // end in a trailing "separator"
  242. for _, prefix := range build.Default.SrcDirs() {
  243. if prefix[len(prefix)-1] != filepath.Separator {
  244. prefix += string(filepath.Separator)
  245. }
  246. trimPaths = append(trimPaths, prefix)
  247. }
  248. }