Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/syncthing: Add more stats to usage reports (ref #3628) #4347

Closed
wants to merge 12 commits into from
Closed
23 changes: 10 additions & 13 deletions cmd/syncthing/gui.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

"github.com/rcrowley/go-metrics"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/events"
Expand Down Expand Up @@ -100,6 +101,8 @@ type modelIntf interface {
CurrentSequence(folder string) (int64, bool)
RemoteSequence(folder string) (int64, bool)
State(folder string) (string, time.Time, error)
Connections() []connections.Connection
BlockStats() map[string]int
}

type configIntf interface {
Expand All @@ -119,6 +122,7 @@ type configIntf interface {

type connectionsIntf interface {
Status() map[string]interface{}
NATType() string
}

type rater interface {
Expand Down Expand Up @@ -800,18 +804,6 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
}
}

// Fixup usage reporting settings

if curAcc := s.cfg.Options().URAccepted; to.Options.URAccepted > curAcc {
// UR was enabled
to.Options.URAccepted = usageReportVersion
to.Options.URUniqueID = rand.String(8)
} else if to.Options.URAccepted < curAcc {
// UR was disabled
to.Options.URAccepted = -1
to.Options.URUniqueID = ""
}

// Activate and save

if err := s.cfg.Replace(to); err != nil {
Expand Down Expand Up @@ -903,6 +895,7 @@ func (s *apiService) getSystemStatus(w http.ResponseWriter, r *http.Request) {
// gives us percent
res["cpuPercent"] = s.cpu.Rate() / 10 / float64(runtime.NumCPU())
res["pathSeparator"] = string(filepath.Separator)
res["urVersionMax"] = usageReportVersion
res["uptime"] = int(time.Since(startTime).Seconds())
res["startTime"] = startTime

Expand Down Expand Up @@ -981,7 +974,11 @@ func (s *apiService) getSystemDiscovery(w http.ResponseWriter, r *http.Request)
}

func (s *apiService) getReport(w http.ResponseWriter, r *http.Request) {
sendJSON(w, reportData(s.cfg, s.model))
version := usageReportVersion
if val, _ := strconv.Atoi(r.URL.Query().Get("version")); val > 0 {
version = val
}
sendJSON(w, reportData(s.cfg, s.model, s.connectionsService, version))
}

func (s *apiService) getRandomString(w http.ResponseWriter, r *http.Request) {
Expand Down
16 changes: 3 additions & 13 deletions cmd/syncthing/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -869,24 +869,14 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
// Unique ID will be set and config saved below if necessary.
}

if opts.URAccepted > 0 && opts.URAccepted < usageReportVersion {
l.Infoln("Anonymous usage report has changed; revoking acceptance")
opts.URAccepted = 0
opts.URUniqueID = ""
cfg.SetOptions(opts)
}

if opts.URAccepted >= usageReportVersion && opts.URUniqueID == "" {
// Generate and save a new unique ID if it is missing.
if opts.URUniqueID == "" {
opts.URUniqueID = rand.String(8)
cfg.SetOptions(opts)
cfg.Save()
}

// The usageReportingManager registers itself to listen to configuration
// changes, and there's nothing more we need to tell it from the outside.
// Hence we don't keep the returned pointer.
newUsageReportingManager(cfg, m)
usageReportingSvc := newUsageReportingService(cfg, m, connectionsService)
mainService.Add(usageReportingSvc)

if opts.RestartOnWakeup {
go standbyMonitor()
Expand Down
4 changes: 4 additions & 0 deletions cmd/syncthing/mocked_connections_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ type mockedConnections struct{}
func (m *mockedConnections) Status() map[string]interface{} {
return nil
}

func (m *mockedConnections) NATType() string {
return ""
}
9 changes: 9 additions & 0 deletions cmd/syncthing/mocked_model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package main
import (
"time"

"github.com/syncthing/syncthing/lib/connections"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/protocol"
Expand Down Expand Up @@ -114,3 +115,11 @@ func (m *mockedModel) RemoteSequence(folder string) (int64, bool) {
func (m *mockedModel) State(folder string) (string, time.Time, error) {
return "", time.Time{}, nil
}

func (m *mockedModel) Connections() []connections.Connection {
return nil
}

func (m *mockedModel) BlockStats() map[string]int {
return nil
}
124 changes: 56 additions & 68 deletions cmd/syncthing/usage_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,79 +12,32 @@ import (
"crypto/rand"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"runtime"
"sort"
"strings"
"time"

"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/scanner"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/thejerf/suture"
)

// Current version number of the usage report, for acceptance purposes. If
// fields are added or changed this integer must be incremented so that users
// are prompted for acceptance of the new report.
const usageReportVersion = 2

type usageReportingManager struct {
cfg *config.Wrapper
model *model.Model
sup *suture.Supervisor
}

func newUsageReportingManager(cfg *config.Wrapper, m *model.Model) *usageReportingManager {
mgr := &usageReportingManager{
cfg: cfg,
model: m,
}

// Start UR if it's enabled.
mgr.CommitConfiguration(config.Configuration{}, cfg.RawCopy())

// Listen to future config changes so that we can start and stop as
// appropriate.
cfg.Subscribe(mgr)

return mgr
}

func (m *usageReportingManager) VerifyConfiguration(from, to config.Configuration) error {
return nil
}

func (m *usageReportingManager) CommitConfiguration(from, to config.Configuration) bool {
if to.Options.URAccepted >= usageReportVersion && m.sup == nil {
// Usage reporting was turned on; lets start it.
service := newUsageReportingService(m.cfg, m.model)
m.sup = suture.NewSimple("usageReporting")
m.sup.Add(service)
m.sup.ServeBackground()
} else if to.Options.URAccepted < usageReportVersion && m.sup != nil {
// Usage reporting was turned off
m.sup.Stop()
m.sup = nil
}

return true
}

func (m *usageReportingManager) String() string {
return fmt.Sprintf("usageReportingManager@%p", m)
}
const usageReportVersion = 3

// reportData returns the data to be sent in a usage report. It's used in
// various places, so not part of the usageReportingManager object.
func reportData(cfg configIntf, m modelIntf) map[string]interface{} {
func reportData(cfg configIntf, m modelIntf, connectionsService connectionsIntf, version int) map[string]interface{} {
opts := cfg.Options()
res := make(map[string]interface{})
res["urVersion"] = usageReportVersion
res["urVersion"] = version
res["uniqueID"] = opts.URUniqueID
res["version"] = Version
res["longVersion"] = LongVersion
Expand Down Expand Up @@ -227,25 +180,42 @@ func reportData(cfg configIntf, m modelIntf) map[string]interface{} {
res["upgradeAllowedAuto"] = !(upgrade.DisabledByCompilation || noUpgradeFromEnv) && opts.AutoUpgradeIntervalH > 0
res["upgradeAllowedPre"] = !(upgrade.DisabledByCompilation || noUpgradeFromEnv) && opts.AutoUpgradeIntervalH > 0 && opts.UpgradeToPreReleases

if version >= 3 {
connTypes := make(map[string]int)
for _, conn := range m.Connections() {
connTypes[conn.Transport()]++
}
res["connectionTypes"] = connTypes
res["blockStats"] = m.BlockStats()
res["uptime"] = time.Now().Sub(startTime).Seconds()
res["natType"] = connectionsService.NATType()
}

return res
}

type usageReportingService struct {
cfg *config.Wrapper
model *model.Model
stop chan struct{}
cfg *config.Wrapper
model *model.Model
connectionsService *connections.Service
forceRun chan struct{}
stop chan struct{}
}

func newUsageReportingService(cfg *config.Wrapper, model *model.Model) *usageReportingService {
return &usageReportingService{
cfg: cfg,
model: model,
stop: make(chan struct{}),
func newUsageReportingService(cfg *config.Wrapper, model *model.Model, connectionsService *connections.Service) *usageReportingService {
svc := &usageReportingService{
cfg: cfg,
model: model,
connectionsService: connectionsService,
forceRun: make(chan struct{}),
stop: make(chan struct{}),
}
cfg.Subscribe(svc)
return svc
}

func (s *usageReportingService) sendUsageReport() error {
d := reportData(s.cfg, s.model)
d := reportData(s.cfg, s.model, s.connectionsService, s.cfg.Options().URAccepted)
var b bytes.Buffer
json.NewEncoder(&b).Encode(d)

Expand All @@ -264,27 +234,45 @@ func (s *usageReportingService) sendUsageReport() error {

func (s *usageReportingService) Serve() {
s.stop = make(chan struct{})

l.Infoln("Starting usage reporting")
defer l.Infoln("Stopping usage reporting")

t := time.NewTimer(time.Duration(s.cfg.Options().URInitialDelayS) * time.Second) // time to initial report at start
t := time.NewTimer(time.Duration(s.cfg.Options().URInitialDelayS) * time.Second)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defer t.Stop()

for {
select {
case <-s.stop:
return
case <-s.forceRun:
t.Reset(0)
case <-t.C:
err := s.sendUsageReport()
if err != nil {
l.Infoln("Usage report:", err)
if s.cfg.Options().URAccepted >= 2 {
err := s.sendUsageReport()
if err != nil {
l.Infoln("Usage report:", err)
} else {
l.Infof("Sent usage report (version %d)", s.cfg.Options().URAccepted)
}
}
t.Reset(24 * time.Hour) // next report tomorrow
}
}
}

func (s *usageReportingService) VerifyConfiguration(from, to config.Configuration) error {
return nil
}

func (s *usageReportingService) CommitConfiguration(from, to config.Configuration) bool {
if from.Options.URAccepted != to.Options.URAccepted || from.Options.URUniqueID != to.Options.URUniqueID || from.Options.URURL != to.Options.URURL {
s.forceRun <- struct{}{}
}
return true
}

func (s *usageReportingService) Stop() {
close(s.stop)
close(s.forceRun)
}

func (usageReportingService) String() string {
return "usageReportingService"
}

// cpuBench returns CPU performance as a measure of single threaded SHA-256 MiB/s
Expand Down