Use a global raven.Client because they allocate an http.Client for each, with a separate CA bundle and infinite connection idle time. Infinite connection idle time means that if the client is never used again it will always keep the connection around, not verifying whether it's closed server side or not. This leaks about a megabyte of memory for each client every created. client.Close() doesn't help with this because the http.Client is still around, retained by its own goroutines. The thing with the map is just to retain the API on sendReport, even though there will in practice only ever be one DSN per process instance...
205 lines
5.0 KiB
Go
205 lines
5.0 KiB
Go
// Copyright (C) 2019 The Syncthing Authors.
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io/ioutil"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
|
|
raven "github.com/getsentry/raven-go"
|
|
"github.com/maruel/panicparse/stack"
|
|
)
|
|
|
|
const reportServer = "https://crash.syncthing.net/report/"
|
|
|
|
var loader = newGithubSourceCodeLoader()
|
|
|
|
func init() {
|
|
raven.SetSourceCodeLoader(loader)
|
|
}
|
|
|
|
var (
|
|
clients = make(map[string]*raven.Client)
|
|
clientsMut sync.Mutex
|
|
)
|
|
|
|
func sendReport(dsn, path string, report []byte) error {
|
|
pkt, err := parseReport(path, report)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
clientsMut.Lock()
|
|
defer clientsMut.Unlock()
|
|
|
|
cli, ok := clients[dsn]
|
|
if !ok {
|
|
cli, err = raven.New(dsn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
clients[dsn] = cli
|
|
}
|
|
|
|
// The client sets release and such on the packet before sending, in the
|
|
// misguided idea that it knows this better than than the packet we give
|
|
// it. So we copy the values from the packet to the client first...
|
|
cli.SetRelease(pkt.Release)
|
|
cli.SetEnvironment(pkt.Environment)
|
|
|
|
defer cli.Wait()
|
|
_, errC := cli.Capture(pkt, nil)
|
|
return <-errC
|
|
}
|
|
|
|
func parseReport(path string, report []byte) (*raven.Packet, error) {
|
|
parts := bytes.SplitN(report, []byte("\n"), 2)
|
|
if len(parts) != 2 {
|
|
return nil, errors.New("no first line")
|
|
}
|
|
|
|
version, err := parseVersion(string(parts[0]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
report = parts[1]
|
|
|
|
foundPanic := false
|
|
var subjectLine []byte
|
|
for {
|
|
parts = bytes.SplitN(report, []byte("\n"), 2)
|
|
if len(parts) != 2 {
|
|
return nil, errors.New("no panic line found")
|
|
}
|
|
|
|
line := parts[0]
|
|
report = parts[1]
|
|
|
|
if foundPanic {
|
|
// The previous line was our "Panic at ..." header. We are now
|
|
// at the beginning of the real panic trace and this is our
|
|
// subject line.
|
|
subjectLine = line
|
|
break
|
|
} else if bytes.HasPrefix(line, []byte("Panic at")) {
|
|
foundPanic = true
|
|
}
|
|
}
|
|
|
|
r := bytes.NewReader(report)
|
|
ctx, err := stack.ParseDump(r, ioutil.Discard, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Lock the source code loader to the version we are processing here.
|
|
if version.commit != "" {
|
|
// We have a commit hash, so we know exactly which source to use
|
|
loader.LockWithVersion(version.commit)
|
|
} else if strings.HasPrefix(version.tag, "v") {
|
|
// Lets hope the tag is close enough
|
|
loader.LockWithVersion(version.tag)
|
|
} else {
|
|
// Last resort
|
|
loader.LockWithVersion("master")
|
|
}
|
|
defer loader.Unlock()
|
|
|
|
var trace raven.Stacktrace
|
|
for _, gr := range ctx.Goroutines {
|
|
if gr.First {
|
|
trace.Frames = make([]*raven.StacktraceFrame, len(gr.Stack.Calls))
|
|
for i, sc := range gr.Stack.Calls {
|
|
trace.Frames[len(trace.Frames)-1-i] = raven.NewStacktraceFrame(0, sc.Func.Name(), sc.SrcPath, sc.Line, 3, nil)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
pkt := &raven.Packet{
|
|
Message: string(subjectLine),
|
|
Platform: "go",
|
|
Release: version.tag,
|
|
Environment: version.environment(),
|
|
Tags: raven.Tags{
|
|
raven.Tag{Key: "version", Value: version.version},
|
|
raven.Tag{Key: "tag", Value: version.tag},
|
|
raven.Tag{Key: "codename", Value: version.codename},
|
|
raven.Tag{Key: "runtime", Value: version.runtime},
|
|
raven.Tag{Key: "goos", Value: version.goos},
|
|
raven.Tag{Key: "goarch", Value: version.goarch},
|
|
raven.Tag{Key: "builder", Value: version.builder},
|
|
},
|
|
Extra: raven.Extra{
|
|
"url": reportServer + path,
|
|
},
|
|
Interfaces: []raven.Interface{&trace},
|
|
}
|
|
if version.commit != "" {
|
|
pkt.Tags = append(pkt.Tags, raven.Tag{Key: "commit", Value: version.commit})
|
|
}
|
|
|
|
return pkt, nil
|
|
}
|
|
|
|
// 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
|
|
var longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)`)
|
|
|
|
type version struct {
|
|
version string // "v1.1.4-rc.1+30-g6aaae618-dirty-crashrep"
|
|
tag string // "v1.1.4-rc.1"
|
|
commit string // "6aaae618", blank when absent
|
|
codename string // "Erbium Earthworm"
|
|
runtime string // "go1.12.5"
|
|
goos string // "darwin"
|
|
goarch string // "amd64"
|
|
builder string // "jb@kvin.kastelo.net"
|
|
}
|
|
|
|
func (v version) environment() string {
|
|
if v.commit != "" {
|
|
return "Development"
|
|
}
|
|
if strings.Contains(v.tag, "-rc.") {
|
|
return "Candidate"
|
|
}
|
|
if strings.Contains(v.tag, "-") {
|
|
return "Beta"
|
|
}
|
|
return "Stable"
|
|
}
|
|
|
|
func parseVersion(line string) (version, error) {
|
|
m := longVersionRE.FindStringSubmatch(line)
|
|
if len(m) == 0 {
|
|
return version{}, errors.New("unintelligeble version string")
|
|
}
|
|
|
|
v := version{
|
|
version: m[1],
|
|
codename: m[2],
|
|
runtime: m[3],
|
|
goos: m[4],
|
|
goarch: m[5],
|
|
builder: m[6],
|
|
}
|
|
parts := strings.Split(v.version, "+")
|
|
v.tag = parts[0]
|
|
if len(parts) > 1 {
|
|
fields := strings.Split(parts[1], "-")
|
|
if len(fields) >= 2 && strings.HasPrefix(fields[1], "g") {
|
|
v.commit = fields[1][1:]
|
|
}
|
|
}
|
|
|
|
return v, nil
|
|
}
|