sentry.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  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"
  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 string, pkt *raven.Packet, userID string) error {
  27. pkt.Interfaces = append(pkt.Interfaces, &raven.User{ID: userID})
  28. clientsMut.Lock()
  29. defer clientsMut.Unlock()
  30. cli, ok := clients[dsn]
  31. if !ok {
  32. var err error
  33. cli, err = raven.New(dsn)
  34. if err != nil {
  35. return err
  36. }
  37. clients[dsn] = cli
  38. }
  39. // The client sets release and such on the packet before sending, in the
  40. // misguided idea that it knows this better than than the packet we give
  41. // it. So we copy the values from the packet to the client first...
  42. cli.SetRelease(pkt.Release)
  43. cli.SetEnvironment(pkt.Environment)
  44. defer cli.Wait()
  45. _, errC := cli.Capture(pkt, nil)
  46. return <-errC
  47. }
  48. func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
  49. parts := bytes.SplitN(report, []byte("\n"), 2)
  50. if len(parts) != 2 {
  51. return nil, errors.New("no first line")
  52. }
  53. version, err := parseVersion(string(parts[0]))
  54. if err != nil {
  55. return nil, err
  56. }
  57. report = parts[1]
  58. foundPanic := false
  59. var subjectLine []byte
  60. for {
  61. parts = bytes.SplitN(report, []byte("\n"), 2)
  62. if len(parts) != 2 {
  63. return nil, errors.New("no panic line found")
  64. }
  65. line := parts[0]
  66. report = parts[1]
  67. if foundPanic {
  68. // The previous line was our "Panic at ..." header. We are now
  69. // at the beginning of the real panic trace and this is our
  70. // subject line.
  71. subjectLine = line
  72. break
  73. } else if bytes.HasPrefix(line, []byte("Panic at")) {
  74. foundPanic = true
  75. }
  76. }
  77. r := bytes.NewReader(report)
  78. ctx, err := stack.ParseDump(r, io.Discard, false)
  79. if err != nil {
  80. return nil, err
  81. }
  82. // Lock the source code loader to the version we are processing here.
  83. if version.commit != "" {
  84. // We have a commit hash, so we know exactly which source to use
  85. loader.LockWithVersion(version.commit)
  86. } else if strings.HasPrefix(version.tag, "v") {
  87. // Lets hope the tag is close enough
  88. loader.LockWithVersion(version.tag)
  89. } else {
  90. // Last resort
  91. loader.LockWithVersion("main")
  92. }
  93. defer loader.Unlock()
  94. var trace raven.Stacktrace
  95. for _, gr := range ctx.Goroutines {
  96. if gr.First {
  97. trace.Frames = make([]*raven.StacktraceFrame, len(gr.Stack.Calls))
  98. for i, sc := range gr.Stack.Calls {
  99. trace.Frames[len(trace.Frames)-1-i] = raven.NewStacktraceFrame(0, sc.Func.Name(), sc.SrcPath, sc.Line, 3, nil)
  100. }
  101. break
  102. }
  103. }
  104. pkt := packet(version, "crash")
  105. pkt.Message = string(subjectLine)
  106. pkt.Extra = raven.Extra{
  107. "url": reportServer + path,
  108. }
  109. pkt.Interfaces = []raven.Interface{&trace}
  110. pkt.Fingerprint = crashReportFingerprint(pkt.Message)
  111. return pkt, nil
  112. }
  113. var (
  114. indexRe = regexp.MustCompile(`\[[-:0-9]+\]`)
  115. sizeRe = regexp.MustCompile(`(length|capacity) [0-9]+`)
  116. ldbPosRe = regexp.MustCompile(`(\(pos=)([0-9]+)\)`)
  117. ldbChecksumRe = regexp.MustCompile(`(want=0x)([a-z0-9]+)( got=0x)([a-z0-9]+)`)
  118. ldbFileRe = regexp.MustCompile(`(\[file=)([0-9]+)(\.ldb\])`)
  119. ldbInternalKeyRe = regexp.MustCompile(`(internal key ")[^"]+(", len=)[0-9]+`)
  120. ldbPathRe = regexp.MustCompile(`(open|write|read) .+[\\/].+[\\/]index[^\\/]+[\\/][^\\/]+: `)
  121. )
  122. func sanitizeMessageLDB(message string) string {
  123. message = ldbPosRe.ReplaceAllString(message, "${1}x)")
  124. message = ldbFileRe.ReplaceAllString(message, "${1}x${3}")
  125. message = ldbChecksumRe.ReplaceAllString(message, "${1}X${3}X")
  126. message = ldbInternalKeyRe.ReplaceAllString(message, "${1}x${2}x")
  127. message = ldbPathRe.ReplaceAllString(message, "$1 x: ")
  128. return message
  129. }
  130. func crashReportFingerprint(message string) []string {
  131. // Do not fingerprint on the stack in case of db corruption or fatal
  132. // db io error - where it occurs doesn't matter.
  133. orig := message
  134. message = sanitizeMessageLDB(message)
  135. if message != orig {
  136. return []string{message}
  137. }
  138. message = indexRe.ReplaceAllString(message, "[x]")
  139. message = sizeRe.ReplaceAllString(message, "$1 x")
  140. // {{ default }} is what sentry uses as a fingerprint by default. While
  141. // never specified, the docs point at this being some hash derived from the
  142. // stack trace. Here we include the filtered panic message on top of that.
  143. // https://docs.sentry.io/platforms/go/data-management/event-grouping/sdk-fingerprinting/#basic-example
  144. return []string{"{{ default }}", message}
  145. }
  146. // syncthing v1.1.4-rc.1+30-g6aaae618-dirty-crashrep "Erbium Earthworm" (go1.12.5 darwin-amd64) jb@kvin.kastelo.net 2019-05-23 16:08:14 UTC [foo, bar]
  147. var longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)[^\[]*(?:\[(.+)\])?$`)
  148. type version struct {
  149. version string // "v1.1.4-rc.1+30-g6aaae618-dirty-crashrep"
  150. tag string // "v1.1.4-rc.1"
  151. commit string // "6aaae618", blank when absent
  152. codename string // "Erbium Earthworm"
  153. runtime string // "go1.12.5"
  154. goos string // "darwin"
  155. goarch string // "amd64"
  156. builder string // "jb@kvin.kastelo.net"
  157. extra []string // "foo", "bar"
  158. }
  159. func (v version) environment() string {
  160. if v.commit != "" {
  161. return "Development"
  162. }
  163. if strings.Contains(v.tag, "-rc.") {
  164. return "Candidate"
  165. }
  166. if strings.Contains(v.tag, "-") {
  167. return "Beta"
  168. }
  169. return "Stable"
  170. }
  171. func parseVersion(line string) (version, error) {
  172. m := longVersionRE.FindStringSubmatch(line)
  173. if len(m) == 0 {
  174. return version{}, errors.New("unintelligeble version string")
  175. }
  176. v := version{
  177. version: m[1],
  178. codename: m[2],
  179. runtime: m[3],
  180. goos: m[4],
  181. goarch: m[5],
  182. builder: m[6],
  183. }
  184. parts := strings.Split(v.version, "+")
  185. v.tag = parts[0]
  186. if len(parts) > 1 {
  187. fields := strings.Split(parts[1], "-")
  188. if len(fields) >= 2 && strings.HasPrefix(fields[1], "g") {
  189. v.commit = fields[1][1:]
  190. }
  191. }
  192. if len(m) >= 8 && m[7] != "" {
  193. tags := strings.Split(m[7], ",")
  194. for i := range tags {
  195. tags[i] = strings.TrimSpace(tags[i])
  196. }
  197. v.extra = tags
  198. }
  199. return v, nil
  200. }
  201. func packet(version version, reportType string) *raven.Packet {
  202. pkt := &raven.Packet{
  203. Platform: "go",
  204. Release: version.tag,
  205. Environment: version.environment(),
  206. Tags: raven.Tags{
  207. raven.Tag{Key: "version", Value: version.version},
  208. raven.Tag{Key: "tag", Value: version.tag},
  209. raven.Tag{Key: "codename", Value: version.codename},
  210. raven.Tag{Key: "runtime", Value: version.runtime},
  211. raven.Tag{Key: "goos", Value: version.goos},
  212. raven.Tag{Key: "goarch", Value: version.goarch},
  213. raven.Tag{Key: "builder", Value: version.builder},
  214. raven.Tag{Key: "report_type", Value: reportType},
  215. },
  216. }
  217. if version.commit != "" {
  218. pkt.Tags = append(pkt.Tags, raven.Tag{Key: "commit", Value: version.commit})
  219. }
  220. for _, tag := range version.extra {
  221. pkt.Tags = append(pkt.Tags, raven.Tag{Key: tag, Value: "1"})
  222. }
  223. return pkt
  224. }