sentry.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. // Copyright (C) 2019 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. package main
  7. import (
  8. "bytes"
  9. "context"
  10. "errors"
  11. "io"
  12. "log"
  13. "regexp"
  14. "strings"
  15. "sync"
  16. raven "github.com/getsentry/raven-go"
  17. "github.com/maruel/panicparse/v2/stack"
  18. )
  19. const reportServer = "https://crash.syncthing.net/report/"
  20. var loader = newGithubSourceCodeLoader()
  21. func init() {
  22. raven.SetSourceCodeLoader(loader)
  23. }
  24. var (
  25. clients = make(map[string]*raven.Client)
  26. clientsMut sync.Mutex
  27. )
  28. type sentryService struct {
  29. dsn string
  30. inbox chan sentryRequest
  31. }
  32. type sentryRequest struct {
  33. reportID string
  34. data []byte
  35. }
  36. func (s *sentryService) Serve(ctx context.Context) {
  37. for {
  38. select {
  39. case req := <-s.inbox:
  40. pkt, err := parseCrashReport(req.reportID, req.data)
  41. if err != nil {
  42. log.Println("Failed to parse crash report:", err)
  43. continue
  44. }
  45. if err := sendReport(s.dsn, pkt, req.reportID); err != nil {
  46. log.Println("Failed to send crash report:", err)
  47. }
  48. case <-ctx.Done():
  49. return
  50. }
  51. }
  52. }
  53. func (s *sentryService) Send(reportID string, data []byte) bool {
  54. select {
  55. case s.inbox <- sentryRequest{reportID, data}:
  56. return true
  57. default:
  58. return false
  59. }
  60. }
  61. func sendReport(dsn string, pkt *raven.Packet, userID string) error {
  62. pkt.Interfaces = append(pkt.Interfaces, &raven.User{ID: userID})
  63. clientsMut.Lock()
  64. defer clientsMut.Unlock()
  65. cli, ok := clients[dsn]
  66. if !ok {
  67. var err error
  68. cli, err = raven.New(dsn)
  69. if err != nil {
  70. return err
  71. }
  72. clients[dsn] = cli
  73. }
  74. // The client sets release and such on the packet before sending, in the
  75. // misguided idea that it knows this better than than the packet we give
  76. // it. So we copy the values from the packet to the client first...
  77. cli.SetRelease(pkt.Release)
  78. cli.SetEnvironment(pkt.Environment)
  79. defer cli.Wait()
  80. _, errC := cli.Capture(pkt, nil)
  81. return <-errC
  82. }
  83. func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
  84. parts := bytes.SplitN(report, []byte("\n"), 2)
  85. if len(parts) != 2 {
  86. return nil, errors.New("no first line")
  87. }
  88. version, err := parseVersion(string(parts[0]))
  89. if err != nil {
  90. return nil, err
  91. }
  92. report = parts[1]
  93. foundPanic := false
  94. var subjectLine []byte
  95. for {
  96. parts = bytes.SplitN(report, []byte("\n"), 2)
  97. if len(parts) != 2 {
  98. return nil, errors.New("no panic line found")
  99. }
  100. line := parts[0]
  101. report = parts[1]
  102. if foundPanic {
  103. // The previous line was our "Panic at ..." header. We are now
  104. // at the beginning of the real panic trace and this is our
  105. // subject line.
  106. subjectLine = line
  107. break
  108. } else if bytes.HasPrefix(line, []byte("Panic at")) {
  109. foundPanic = true
  110. }
  111. }
  112. r := bytes.NewReader(report)
  113. ctx, _, err := stack.ScanSnapshot(r, io.Discard, stack.DefaultOpts())
  114. if err != nil && err != io.EOF {
  115. return nil, err
  116. }
  117. if ctx == nil || len(ctx.Goroutines) == 0 {
  118. return nil, errors.New("no goroutines found")
  119. }
  120. // Lock the source code loader to the version we are processing here.
  121. if version.commit != "" {
  122. // We have a commit hash, so we know exactly which source to use
  123. loader.LockWithVersion(version.commit)
  124. } else if strings.HasPrefix(version.tag, "v") {
  125. // Lets hope the tag is close enough
  126. loader.LockWithVersion(version.tag)
  127. } else {
  128. // Last resort
  129. loader.LockWithVersion("main")
  130. }
  131. defer loader.Unlock()
  132. var trace raven.Stacktrace
  133. for _, gr := range ctx.Goroutines {
  134. if gr.First {
  135. trace.Frames = make([]*raven.StacktraceFrame, len(gr.Stack.Calls))
  136. for i, sc := range gr.Stack.Calls {
  137. trace.Frames[len(trace.Frames)-1-i] = raven.NewStacktraceFrame(0, sc.Func.Name, sc.RemoteSrcPath, sc.Line, 3, nil)
  138. }
  139. break
  140. }
  141. }
  142. pkt := packet(version, "crash")
  143. pkt.Message = string(subjectLine)
  144. pkt.Extra = raven.Extra{
  145. "url": reportServer + path,
  146. }
  147. pkt.Interfaces = []raven.Interface{&trace}
  148. pkt.Fingerprint = crashReportFingerprint(pkt.Message)
  149. return pkt, nil
  150. }
  151. var (
  152. indexRe = regexp.MustCompile(`\[[-:0-9]+\]`)
  153. sizeRe = regexp.MustCompile(`(length|capacity) [0-9]+`)
  154. ldbPosRe = regexp.MustCompile(`(\(pos=)([0-9]+)\)`)
  155. ldbChecksumRe = regexp.MustCompile(`(want=0x)([a-z0-9]+)( got=0x)([a-z0-9]+)`)
  156. ldbFileRe = regexp.MustCompile(`(\[file=)([0-9]+)(\.ldb\])`)
  157. ldbInternalKeyRe = regexp.MustCompile(`(internal key ")[^"]+(", len=)[0-9]+`)
  158. ldbPathRe = regexp.MustCompile(`(open|write|read) .+[\\/].+[\\/]index[^\\/]+[\\/][^\\/]+: `)
  159. )
  160. func sanitizeMessageLDB(message string) string {
  161. message = ldbPosRe.ReplaceAllString(message, "${1}x)")
  162. message = ldbFileRe.ReplaceAllString(message, "${1}x${3}")
  163. message = ldbChecksumRe.ReplaceAllString(message, "${1}X${3}X")
  164. message = ldbInternalKeyRe.ReplaceAllString(message, "${1}x${2}x")
  165. message = ldbPathRe.ReplaceAllString(message, "$1 x: ")
  166. return message
  167. }
  168. func crashReportFingerprint(message string) []string {
  169. // Do not fingerprint on the stack in case of db corruption or fatal
  170. // db io error - where it occurs doesn't matter.
  171. orig := message
  172. message = sanitizeMessageLDB(message)
  173. if message != orig {
  174. return []string{message}
  175. }
  176. message = indexRe.ReplaceAllString(message, "[x]")
  177. message = sizeRe.ReplaceAllString(message, "$1 x")
  178. // {{ default }} is what sentry uses as a fingerprint by default. While
  179. // never specified, the docs point at this being some hash derived from the
  180. // stack trace. Here we include the filtered panic message on top of that.
  181. // https://docs.sentry.io/platforms/go/data-management/event-grouping/sdk-fingerprinting/#basic-example
  182. return []string{"{{ default }}", message}
  183. }
  184. // syncthing v1.1.4-rc.1+30-g6aaae618-dirty-crashrep "Erbium Earthworm" (go1.12.5 darwin-amd64) [email protected] 2019-05-23 16:08:14 UTC [foo, bar]
  185. var longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)[^\[]*(?:\[(.+)\])?$`)
  186. type version struct {
  187. version string // "v1.1.4-rc.1+30-g6aaae618-dirty-crashrep"
  188. tag string // "v1.1.4-rc.1"
  189. commit string // "6aaae618", blank when absent
  190. codename string // "Erbium Earthworm"
  191. runtime string // "go1.12.5"
  192. goos string // "darwin"
  193. goarch string // "amd64"
  194. builder string // "[email protected]"
  195. extra []string // "foo", "bar"
  196. }
  197. func (v version) environment() string {
  198. if v.commit != "" {
  199. return "Development"
  200. }
  201. if strings.Contains(v.tag, "-rc.") {
  202. return "Candidate"
  203. }
  204. if strings.Contains(v.tag, "-") {
  205. return "Beta"
  206. }
  207. return "Stable"
  208. }
  209. func parseVersion(line string) (version, error) {
  210. m := longVersionRE.FindStringSubmatch(line)
  211. if len(m) == 0 {
  212. return version{}, errors.New("unintelligeble version string")
  213. }
  214. v := version{
  215. version: m[1],
  216. codename: m[2],
  217. runtime: m[3],
  218. goos: m[4],
  219. goarch: m[5],
  220. builder: m[6],
  221. }
  222. parts := strings.Split(v.version, "+")
  223. v.tag = parts[0]
  224. if len(parts) > 1 {
  225. fields := strings.Split(parts[1], "-")
  226. if len(fields) >= 2 && strings.HasPrefix(fields[1], "g") {
  227. v.commit = fields[1][1:]
  228. }
  229. }
  230. if len(m) >= 8 && m[7] != "" {
  231. tags := strings.Split(m[7], ",")
  232. for i := range tags {
  233. tags[i] = strings.TrimSpace(tags[i])
  234. }
  235. v.extra = tags
  236. }
  237. return v, nil
  238. }
  239. func packet(version version, reportType string) *raven.Packet {
  240. pkt := &raven.Packet{
  241. Platform: "go",
  242. Release: version.tag,
  243. Environment: version.environment(),
  244. Tags: raven.Tags{
  245. raven.Tag{Key: "version", Value: version.version},
  246. raven.Tag{Key: "tag", Value: version.tag},
  247. raven.Tag{Key: "codename", Value: version.codename},
  248. raven.Tag{Key: "runtime", Value: version.runtime},
  249. raven.Tag{Key: "goos", Value: version.goos},
  250. raven.Tag{Key: "goarch", Value: version.goarch},
  251. raven.Tag{Key: "builder", Value: version.builder},
  252. raven.Tag{Key: "report_type", Value: reportType},
  253. },
  254. }
  255. if version.commit != "" {
  256. pkt.Tags = append(pkt.Tags, raven.Tag{Key: "commit", Value: version.commit})
  257. }
  258. for _, tag := range version.extra {
  259. pkt.Tags = append(pkt.Tags, raven.Tag{Key: tag, Value: "1"})
  260. }
  261. return pkt
  262. }