sentry.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  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. "errors"
  10. "io/ioutil"
  11. "regexp"
  12. "strings"
  13. "sync"
  14. raven "github.com/getsentry/raven-go"
  15. "github.com/maruel/panicparse/stack"
  16. )
  17. const reportServer = "https://crash.syncthing.net/report/"
  18. var loader = newGithubSourceCodeLoader()
  19. func init() {
  20. raven.SetSourceCodeLoader(loader)
  21. }
  22. var (
  23. clients = make(map[string]*raven.Client)
  24. clientsMut sync.Mutex
  25. )
  26. func sendReport(dsn, path string, report []byte) error {
  27. pkt, err := parseReport(path, report)
  28. if err != nil {
  29. return err
  30. }
  31. clientsMut.Lock()
  32. defer clientsMut.Unlock()
  33. cli, ok := clients[dsn]
  34. if !ok {
  35. cli, err = raven.New(dsn)
  36. if err != nil {
  37. return err
  38. }
  39. clients[dsn] = cli
  40. }
  41. // The client sets release and such on the packet before sending, in the
  42. // misguided idea that it knows this better than than the packet we give
  43. // it. So we copy the values from the packet to the client first...
  44. cli.SetRelease(pkt.Release)
  45. cli.SetEnvironment(pkt.Environment)
  46. defer cli.Wait()
  47. _, errC := cli.Capture(pkt, nil)
  48. return <-errC
  49. }
  50. func parseReport(path string, report []byte) (*raven.Packet, error) {
  51. parts := bytes.SplitN(report, []byte("\n"), 2)
  52. if len(parts) != 2 {
  53. return nil, errors.New("no first line")
  54. }
  55. version, err := parseVersion(string(parts[0]))
  56. if err != nil {
  57. return nil, err
  58. }
  59. report = parts[1]
  60. foundPanic := false
  61. var subjectLine []byte
  62. for {
  63. parts = bytes.SplitN(report, []byte("\n"), 2)
  64. if len(parts) != 2 {
  65. return nil, errors.New("no panic line found")
  66. }
  67. line := parts[0]
  68. report = parts[1]
  69. if foundPanic {
  70. // The previous line was our "Panic at ..." header. We are now
  71. // at the beginning of the real panic trace and this is our
  72. // subject line.
  73. subjectLine = line
  74. break
  75. } else if bytes.HasPrefix(line, []byte("Panic at")) {
  76. foundPanic = true
  77. }
  78. }
  79. r := bytes.NewReader(report)
  80. ctx, err := stack.ParseDump(r, ioutil.Discard, false)
  81. if err != nil {
  82. return nil, err
  83. }
  84. // Lock the source code loader to the version we are processing here.
  85. if version.commit != "" {
  86. // We have a commit hash, so we know exactly which source to use
  87. loader.LockWithVersion(version.commit)
  88. } else if strings.HasPrefix(version.tag, "v") {
  89. // Lets hope the tag is close enough
  90. loader.LockWithVersion(version.tag)
  91. } else {
  92. // Last resort
  93. loader.LockWithVersion("master")
  94. }
  95. defer loader.Unlock()
  96. var trace raven.Stacktrace
  97. for _, gr := range ctx.Goroutines {
  98. if gr.First {
  99. trace.Frames = make([]*raven.StacktraceFrame, len(gr.Stack.Calls))
  100. for i, sc := range gr.Stack.Calls {
  101. trace.Frames[len(trace.Frames)-1-i] = raven.NewStacktraceFrame(0, sc.Func.Name(), sc.SrcPath, sc.Line, 3, nil)
  102. }
  103. break
  104. }
  105. }
  106. pkt := &raven.Packet{
  107. Message: string(subjectLine),
  108. Platform: "go",
  109. Release: version.tag,
  110. Environment: version.environment(),
  111. Tags: raven.Tags{
  112. raven.Tag{Key: "version", Value: version.version},
  113. raven.Tag{Key: "tag", Value: version.tag},
  114. raven.Tag{Key: "codename", Value: version.codename},
  115. raven.Tag{Key: "runtime", Value: version.runtime},
  116. raven.Tag{Key: "goos", Value: version.goos},
  117. raven.Tag{Key: "goarch", Value: version.goarch},
  118. raven.Tag{Key: "builder", Value: version.builder},
  119. },
  120. Extra: raven.Extra{
  121. "url": reportServer + path,
  122. },
  123. Interfaces: []raven.Interface{&trace},
  124. }
  125. if version.commit != "" {
  126. pkt.Tags = append(pkt.Tags, raven.Tag{Key: "commit", Value: version.commit})
  127. }
  128. return pkt, nil
  129. }
  130. // 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
  131. var longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)`)
  132. type version struct {
  133. version string // "v1.1.4-rc.1+30-g6aaae618-dirty-crashrep"
  134. tag string // "v1.1.4-rc.1"
  135. commit string // "6aaae618", blank when absent
  136. codename string // "Erbium Earthworm"
  137. runtime string // "go1.12.5"
  138. goos string // "darwin"
  139. goarch string // "amd64"
  140. builder string // "[email protected]"
  141. }
  142. func (v version) environment() string {
  143. if v.commit != "" {
  144. return "Development"
  145. }
  146. if strings.Contains(v.tag, "-rc.") {
  147. return "Candidate"
  148. }
  149. if strings.Contains(v.tag, "-") {
  150. return "Beta"
  151. }
  152. return "Stable"
  153. }
  154. func parseVersion(line string) (version, error) {
  155. m := longVersionRE.FindStringSubmatch(line)
  156. if len(m) == 0 {
  157. return version{}, errors.New("unintelligeble version string")
  158. }
  159. v := version{
  160. version: m[1],
  161. codename: m[2],
  162. runtime: m[3],
  163. goos: m[4],
  164. goarch: m[5],
  165. builder: m[6],
  166. }
  167. parts := strings.Split(v.version, "+")
  168. v.tag = parts[0]
  169. if len(parts) > 1 {
  170. fields := strings.Split(parts[1], "-")
  171. if len(fields) >= 2 && strings.HasPrefix(fields[1], "g") {
  172. v.commit = fields[1][1:]
  173. }
  174. }
  175. return v, nil
  176. }