sentry.go 8.8 KB

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