Merge branch 'infrastructure'
* infrastructure: build: let infra containers builds fail individually chore(ur): move structs to reduce dependency chain chore(stcrashreceiver): add profiler on metrics port chore(stcrashreceiver): compact diskstore in-memory representation chore(stcrashreceiver): better source cache & metrics chore(stcrashreceiver): metrics on ignore matches
This commit is contained in:
@@ -22,6 +22,7 @@ jobs:
|
|||||||
if: github.repository_owner == 'syncthing'
|
if: github.repository_owner == 'syncthing'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
pkg:
|
pkg:
|
||||||
- stcrashreceiver
|
- stcrashreceiver
|
||||||
|
|||||||
@@ -136,15 +136,25 @@ func (d *diskStore) Exists(path string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *diskStore) clean() {
|
func (d *diskStore) clean() {
|
||||||
for len(d.currentFiles) > 0 && (len(d.currentFiles) > d.maxFiles || d.currentSize > d.maxBytes) {
|
numDeleted := 0
|
||||||
f := d.currentFiles[0]
|
for idx := range d.currentFiles {
|
||||||
|
if len(d.currentFiles)-numDeleted < d.maxFiles && d.currentSize < d.maxBytes {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
f := d.currentFiles[idx]
|
||||||
log.Println("Removing", f.path)
|
log.Println("Removing", f.path)
|
||||||
if err := os.Remove(f.path); err != nil {
|
if err := os.Remove(f.path); err != nil {
|
||||||
log.Println("Failed to remove file:", err)
|
log.Println("Failed to remove file:", err)
|
||||||
}
|
}
|
||||||
d.currentFiles = d.currentFiles[1:]
|
|
||||||
d.currentSize -= f.size
|
d.currentSize -= f.size
|
||||||
|
numDeleted = idx + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compact currentFiles
|
||||||
|
copy(d.currentFiles, d.currentFiles[numDeleted:])
|
||||||
|
d.currentFiles = d.currentFiles[:len(d.currentFiles)-numDeleted]
|
||||||
|
|
||||||
var oldest time.Duration
|
var oldest time.Duration
|
||||||
if len(d.currentFiles) > 0 {
|
if len(d.currentFiles) > 0 {
|
||||||
oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute)
|
oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute)
|
||||||
@@ -158,7 +168,7 @@ func (d *diskStore) clean() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *diskStore) inventory() error {
|
func (d *diskStore) inventory() error {
|
||||||
d.currentFiles = nil
|
d.currentFiles = d.currentFiles[:0]
|
||||||
d.currentSize = 0
|
d.currentSize = 0
|
||||||
err := filepath.Walk(d.dir, func(path string, info os.FileInfo, err error) error {
|
err := filepath.Walk(d.dir, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/pprof"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -29,7 +30,7 @@ import (
|
|||||||
raven "github.com/getsentry/raven-go"
|
raven "github.com/getsentry/raven-go"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"github.com/syncthing/syncthing/lib/build"
|
"github.com/syncthing/syncthing/lib/build"
|
||||||
"github.com/syncthing/syncthing/lib/ur"
|
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxRequestSize = 1 << 20 // 1 MiB
|
const maxRequestSize = 1 << 20 // 1 MiB
|
||||||
@@ -89,6 +90,7 @@ func main() {
|
|||||||
if params.MetricsListen != "" {
|
if params.MetricsListen != "" {
|
||||||
mmux := http.NewServeMux()
|
mmux := http.NewServeMux()
|
||||||
mmux.Handle("/metrics", promhttp.Handler())
|
mmux.Handle("/metrics", promhttp.Handler())
|
||||||
|
mmux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||||
go func() {
|
go func() {
|
||||||
if err := http.ListenAndServe(params.MetricsListen, mmux); err != nil {
|
if err := http.ListenAndServe(params.MetricsListen, mmux); err != nil {
|
||||||
log.Fatalln("HTTP serve metrics:", err)
|
log.Fatalln("HTTP serve metrics:", err)
|
||||||
@@ -123,12 +125,13 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := ignore.match(bs); ok {
|
if pat, ok := ignore.match(bs); ok {
|
||||||
|
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc()
|
||||||
result = "ignored"
|
result = "ignored"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var reports []ur.FailureReport
|
var reports []contract.FailureReport
|
||||||
err = json.Unmarshal(bs, &reports)
|
err = json.Unmarshal(bs, &reports)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
@@ -176,7 +179,7 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveFailureWithGoroutines(data ur.FailureData, failureDir string) (string, error) {
|
func saveFailureWithGoroutines(data contract.FailureData, failureDir string) (string, error) {
|
||||||
bs := make([]byte, len(data.Description)+len(data.Goroutines))
|
bs := make([]byte, len(data.Description)+len(data.Goroutines))
|
||||||
copy(bs, data.Description)
|
copy(bs, data.Description)
|
||||||
copy(bs[len(data.Description):], data.Goroutines)
|
copy(bs[len(data.Description):], data.Goroutines)
|
||||||
|
|||||||
@@ -42,4 +42,19 @@ var (
|
|||||||
Subsystem: "crashreceiver",
|
Subsystem: "crashreceiver",
|
||||||
Name: "sentry_reports_total",
|
Name: "sentry_reports_total",
|
||||||
}, []string{"result"})
|
}, []string{"result"})
|
||||||
|
metricIgnoreMatchesTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "crashreceiver",
|
||||||
|
Name: "ignore_matches_total",
|
||||||
|
}, []string{"pattern"})
|
||||||
|
metricSourceCodeLoadsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "crashreceiver",
|
||||||
|
Name: "source_code_loads_total",
|
||||||
|
}, []string{"result"})
|
||||||
|
metricSourceCodeCacheSize = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "crashreceiver",
|
||||||
|
Name: "source_code_cache_size",
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,23 +15,33 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
lru "github.com/hashicorp/golang-lru/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/"
|
urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/"
|
||||||
httpTimeout = 10 * time.Second
|
httpTimeout = 10 * time.Second
|
||||||
|
maxCacheEntries = 1000
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type cacheKey struct {
|
||||||
|
version string
|
||||||
|
file string
|
||||||
|
}
|
||||||
|
|
||||||
type githubSourceCodeLoader struct {
|
type githubSourceCodeLoader struct {
|
||||||
mut sync.Mutex
|
mut sync.Mutex
|
||||||
version string
|
version string
|
||||||
cache map[string]map[string][][]byte // version -> file -> lines
|
|
||||||
client *http.Client
|
cache *lru.TwoQueueCache[cacheKey, [][]byte] // version & file -> lines
|
||||||
|
client *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGithubSourceCodeLoader() *githubSourceCodeLoader {
|
func newGithubSourceCodeLoader() *githubSourceCodeLoader {
|
||||||
|
cache, _ := lru.New2Q[cacheKey, [][]byte](maxCacheEntries)
|
||||||
return &githubSourceCodeLoader{
|
return &githubSourceCodeLoader{
|
||||||
cache: make(map[string]map[string][][]byte),
|
cache: cache,
|
||||||
client: &http.Client{Timeout: httpTimeout},
|
client: &http.Client{Timeout: httpTimeout},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,9 +49,6 @@ func newGithubSourceCodeLoader() *githubSourceCodeLoader {
|
|||||||
func (l *githubSourceCodeLoader) LockWithVersion(version string) {
|
func (l *githubSourceCodeLoader) LockWithVersion(version string) {
|
||||||
l.mut.Lock()
|
l.mut.Lock()
|
||||||
l.version = version
|
l.version = version
|
||||||
if _, ok := l.cache[version]; !ok {
|
|
||||||
l.cache[version] = make(map[string][][]byte)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *githubSourceCodeLoader) Unlock() {
|
func (l *githubSourceCodeLoader) Unlock() {
|
||||||
@@ -50,11 +57,13 @@ func (l *githubSourceCodeLoader) Unlock() {
|
|||||||
|
|
||||||
func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]byte, int) {
|
func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]byte, int) {
|
||||||
filename = filepath.ToSlash(filename)
|
filename = filepath.ToSlash(filename)
|
||||||
lines, ok := l.cache[l.version][filename]
|
key := cacheKey{version: l.version, file: filename}
|
||||||
|
lines, ok := l.cache.Get(key)
|
||||||
if !ok {
|
if !ok {
|
||||||
// Cache whatever we managed to find (or nil if nothing, so we don't try again)
|
// Cache whatever we managed to find (or nil if nothing, so we don't try again)
|
||||||
defer func() {
|
defer func() {
|
||||||
l.cache[l.version][filename] = lines
|
l.cache.Add(key, lines)
|
||||||
|
metricSourceCodeCacheSize.Set(float64(l.cache.Len()))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
knownPrefixes := []string{"/lib/", "/cmd/"}
|
knownPrefixes := []string{"/lib/", "/cmd/"}
|
||||||
@@ -73,19 +82,25 @@ func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]b
|
|||||||
resp, err := l.client.Get(url)
|
resp, err := l.client.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Loading source:", err)
|
fmt.Println("Loading source:", err)
|
||||||
|
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
|
||||||
return nil, 0
|
return nil, 0
|
||||||
}
|
}
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
fmt.Println("Loading source:", resp.Status)
|
fmt.Println("Loading source:", resp.Status)
|
||||||
|
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
|
||||||
return nil, 0
|
return nil, 0
|
||||||
}
|
}
|
||||||
data, err := io.ReadAll(resp.Body)
|
data, err := io.ReadAll(resp.Body)
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Loading source:", err.Error())
|
fmt.Println("Loading source:", err.Error())
|
||||||
|
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
|
||||||
return nil, 0
|
return nil, 0
|
||||||
}
|
}
|
||||||
lines = bytes.Split(data, []byte{'\n'})
|
lines = bytes.Split(data, []byte{'\n'})
|
||||||
|
metricSourceCodeLoadsTotal.WithLabelValues("loaded").Inc()
|
||||||
|
} else {
|
||||||
|
metricSourceCodeLoadsTotal.WithLabelValues("cached").Inc()
|
||||||
}
|
}
|
||||||
|
|
||||||
return getLineFromLines(lines, line, context)
|
return getLineFromLines(lines, line, context)
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
|
|||||||
first := string(bytes.TrimSpace(bytes.Split(bs, []byte("\n"))[0]))
|
first := string(bytes.TrimSpace(bytes.Split(bs, []byte("\n"))[0]))
|
||||||
|
|
||||||
if pat, ok := r.ignore.match(bs); ok {
|
if pat, ok := r.ignore.match(bs); ok {
|
||||||
|
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc()
|
||||||
result = "ignored"
|
result = "ignored"
|
||||||
log.Printf("Ignored report %s, matched: %s (%s)", reportID[:8], pat, first)
|
log.Printf("Ignored report %s, matched: %s (%s)", reportID[:8], pat, first)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
"github.com/syncthing/syncthing/lib/svcutil"
|
"github.com/syncthing/syncthing/lib/svcutil"
|
||||||
"github.com/syncthing/syncthing/lib/ur"
|
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||||
)
|
)
|
||||||
|
|
||||||
type indexHandler struct {
|
type indexHandler struct {
|
||||||
@@ -470,7 +470,7 @@ func (s *indexHandler) logSequenceAnomaly(msg string, extra map[string]any) {
|
|||||||
extraStrs[k] = fmt.Sprint(v)
|
extraStrs[k] = fmt.Sprint(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.evLogger.Log(events.Failure, ur.FailureData{
|
s.evLogger.Log(events.Failure, contract.FailureData{
|
||||||
Description: msg,
|
Description: msg,
|
||||||
Extra: extraStrs,
|
Extra: extraStrs,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -282,3 +282,16 @@ func clear(v interface{}, since int) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FailureReport struct {
|
||||||
|
FailureData
|
||||||
|
|
||||||
|
Count int
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FailureData struct {
|
||||||
|
Description string
|
||||||
|
Goroutines string
|
||||||
|
Extra map[string]string
|
||||||
|
}
|
||||||
|
|||||||
+14
-26
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
"github.com/syncthing/syncthing/lib/svcutil"
|
"github.com/syncthing/syncthing/lib/svcutil"
|
||||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||||
|
|
||||||
"github.com/thejerf/suture/v4"
|
"github.com/thejerf/suture/v4"
|
||||||
)
|
)
|
||||||
@@ -39,23 +40,10 @@ var (
|
|||||||
invalidEventDataType = "failure event data is not a string"
|
invalidEventDataType = "failure event data is not a string"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FailureReport struct {
|
func FailureDataWithGoroutines(description string) contract.FailureData {
|
||||||
FailureData
|
|
||||||
|
|
||||||
Count int
|
|
||||||
Version string
|
|
||||||
}
|
|
||||||
|
|
||||||
type FailureData struct {
|
|
||||||
Description string
|
|
||||||
Goroutines string
|
|
||||||
Extra map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func FailureDataWithGoroutines(description string) FailureData {
|
|
||||||
var buf strings.Builder
|
var buf strings.Builder
|
||||||
pprof.Lookup("goroutine").WriteTo(&buf, 1)
|
pprof.Lookup("goroutine").WriteTo(&buf, 1)
|
||||||
return FailureData{
|
return contract.FailureData{
|
||||||
Description: description,
|
Description: description,
|
||||||
Goroutines: buf.String(),
|
Goroutines: buf.String(),
|
||||||
Extra: make(map[string]string),
|
Extra: make(map[string]string),
|
||||||
@@ -86,7 +74,7 @@ type failureHandler struct {
|
|||||||
type failureStat struct {
|
type failureStat struct {
|
||||||
first, last time.Time
|
first, last time.Time
|
||||||
count int
|
count int
|
||||||
data FailureData
|
data contract.FailureData
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *failureHandler) Serve(ctx context.Context) error {
|
func (h *failureHandler) Serve(ctx context.Context) error {
|
||||||
@@ -105,24 +93,24 @@ func (h *failureHandler) Serve(ctx context.Context) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
// Just to be safe - shouldn't ever happen, as
|
// Just to be safe - shouldn't ever happen, as
|
||||||
// evChan is set to nil when unsubscribing.
|
// evChan is set to nil when unsubscribing.
|
||||||
h.addReport(FailureData{Description: evChanClosed}, time.Now())
|
h.addReport(contract.FailureData{Description: evChanClosed}, time.Now())
|
||||||
evChan = nil
|
evChan = nil
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var data FailureData
|
var data contract.FailureData
|
||||||
switch d := e.Data.(type) {
|
switch d := e.Data.(type) {
|
||||||
case string:
|
case string:
|
||||||
data.Description = d
|
data.Description = d
|
||||||
case FailureData:
|
case contract.FailureData:
|
||||||
data = d
|
data = d
|
||||||
default:
|
default:
|
||||||
// Same here, shouldn't ever happen.
|
// Same here, shouldn't ever happen.
|
||||||
h.addReport(FailureData{Description: invalidEventDataType}, time.Now())
|
h.addReport(contract.FailureData{Description: invalidEventDataType}, time.Now())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
h.addReport(data, e.Time)
|
h.addReport(data, e.Time)
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
reports := make([]FailureReport, 0, len(h.buf))
|
reports := make([]contract.FailureReport, 0, len(h.buf))
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for descr, stat := range h.buf {
|
for descr, stat := range h.buf {
|
||||||
if now.Sub(stat.last) > minDelay || now.Sub(stat.first) > maxDelay {
|
if now.Sub(stat.last) > minDelay || now.Sub(stat.first) > maxDelay {
|
||||||
@@ -152,7 +140,7 @@ func (h *failureHandler) Serve(ctx context.Context) error {
|
|||||||
if sub != nil {
|
if sub != nil {
|
||||||
sub.Unsubscribe()
|
sub.Unsubscribe()
|
||||||
if len(h.buf) > 0 {
|
if len(h.buf) > 0 {
|
||||||
reports := make([]FailureReport, 0, len(h.buf))
|
reports := make([]contract.FailureReport, 0, len(h.buf))
|
||||||
for _, stat := range h.buf {
|
for _, stat := range h.buf {
|
||||||
reports = append(reports, newFailureReport(stat))
|
reports = append(reports, newFailureReport(stat))
|
||||||
}
|
}
|
||||||
@@ -179,7 +167,7 @@ func (h *failureHandler) applyOpts(opts config.OptionsConfiguration, sub events.
|
|||||||
return url, nil, nil
|
return url, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *failureHandler) addReport(data FailureData, evTime time.Time) {
|
func (h *failureHandler) addReport(data contract.FailureData, evTime time.Time) {
|
||||||
if stat, ok := h.buf[data.Description]; ok {
|
if stat, ok := h.buf[data.Description]; ok {
|
||||||
stat.last = evTime
|
stat.last = evTime
|
||||||
stat.count++
|
stat.count++
|
||||||
@@ -204,7 +192,7 @@ func (*failureHandler) String() string {
|
|||||||
return "FailureHandler"
|
return "FailureHandler"
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendFailureReports(ctx context.Context, reports []FailureReport, url string) {
|
func sendFailureReports(ctx context.Context, reports []contract.FailureReport, url string) {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
if err := json.NewEncoder(&b).Encode(reports); err != nil {
|
if err := json.NewEncoder(&b).Encode(reports); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -235,8 +223,8 @@ func sendFailureReports(ctx context.Context, reports []FailureReport, url string
|
|||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFailureReport(stat *failureStat) FailureReport {
|
func newFailureReport(stat *failureStat) contract.FailureReport {
|
||||||
return FailureReport{
|
return contract.FailureReport{
|
||||||
FailureData: stat.data,
|
FailureData: stat.data,
|
||||||
Count: stat.count,
|
Count: stat.count,
|
||||||
Version: build.LongVersion,
|
Version: build.LongVersion,
|
||||||
|
|||||||
Reference in New Issue
Block a user