github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/analytics/analytics.go (about)

     1  /*
     2  Package analytics deals with collecting pyroscope server usage data.
     3  
     4  By default pyroscope server sends anonymized usage data to Pyroscope team.
     5  This helps us understand how people use Pyroscope and prioritize features accordingly.
     6  We take privacy of our users very seriously and only collect high-level stats
     7  such as number of apps added, types of spies used, etc.
     8  
     9  You can disable this with a flag or an environment variable
    10  
    11  	pyroscope server -analytics-opt-out
    12  	...
    13  	PYROSCOPE_ANALYTICS_OPT_OUT=true pyroscope server
    14  */
    15  package analytics
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"io"
    21  	"net/http"
    22  	"os"
    23  	"reflect"
    24  	"runtime"
    25  	"strconv"
    26  	"time"
    27  
    28  	"github.com/google/uuid"
    29  	"github.com/sirupsen/logrus"
    30  
    31  	"github.com/pyroscope-io/pyroscope/pkg/build"
    32  	"github.com/pyroscope-io/pyroscope/pkg/config"
    33  	"github.com/pyroscope-io/pyroscope/pkg/storage"
    34  )
    35  
    36  var (
    37  	host                      = "https://analytics.pyroscope.io"
    38  	path                      = "/api/events"
    39  	gracePeriod               = 5 * time.Minute
    40  	oldMetricsUploadFrequency = 24 * time.Hour
    41  	metricsUploadFrequency    = 1 * time.Hour
    42  	snapshotFrequency         = 10 * time.Minute
    43  	multiplier                = 1
    44  )
    45  
    46  type Analytics struct {
    47  	// metadata
    48  	InstallID            string    `json:"install_id"`
    49  	RunID                string    `json:"run_id"`
    50  	Version              string    `json:"version"`
    51  	GitSHA               string    `json:"git_sha"`
    52  	BuildTime            string    `json:"build_time"`
    53  	Timestamp            time.Time `json:"timestamp"`
    54  	UploadIndex          int       `json:"upload_index"`
    55  	GOOS                 string    `json:"goos"`
    56  	GOARCH               string    `json:"goarch"`
    57  	GoVersion            string    `json:"go_version"`
    58  	AnalyticsPersistence bool      `json:"analytics_persistence"`
    59  
    60  	// gauges
    61  	MemAlloc         int `json:"mem_alloc"`
    62  	MemTotalAlloc    int `json:"mem_total_alloc"`
    63  	MemSys           int `json:"mem_sys"`
    64  	MemNumGC         int `json:"mem_num_gc"`
    65  	BadgerMain       int `json:"badger_main"`
    66  	BadgerTrees      int `json:"badger_trees"`
    67  	BadgerDicts      int `json:"badger_dicts"`
    68  	BadgerDimensions int `json:"badger_dimensions"`
    69  	BadgerSegments   int `json:"badger_segments"`
    70  	AppsCount        int `json:"apps_count"`
    71  
    72  	// counters
    73  	ControllerIndex      int `json:"controller_index" kind:"cumulative"`
    74  	ControllerComparison int `json:"controller_comparison" kind:"cumulative"`
    75  	ControllerDiff       int `json:"controller_diff" kind:"cumulative"`
    76  	ControllerIngest     int `json:"controller_ingest" kind:"cumulative"`
    77  	ControllerRender     int `json:"controller_render" kind:"cumulative"`
    78  	SpyRbspy             int `json:"spy_rbspy" kind:"cumulative"`
    79  	SpyPyspy             int `json:"spy_pyspy" kind:"cumulative"`
    80  	SpyGospy             int `json:"spy_gospy" kind:"cumulative"`
    81  	SpyEbpfspy           int `json:"spy_ebpfspy" kind:"cumulative"`
    82  	SpyPhpspy            int `json:"spy_phpspy" kind:"cumulative"`
    83  	SpyDotnetspy         int `json:"spy_dotnetspy" kind:"cumulative"`
    84  	SpyJavaspy           int `json:"spy_javaspy" kind:"cumulative"`
    85  }
    86  
    87  type StatsProvider interface {
    88  	Stats() map[string]int
    89  	AppsCount() int
    90  }
    91  
    92  func NewService(cfg *config.Server, s *storage.Storage, p StatsProvider) *Service {
    93  	if mOverride := os.Getenv("PYROSCOPE_ANALYTICS_MULTIPLIER"); mOverride != "" {
    94  		multiplier, _ = strconv.Atoi(mOverride)
    95  		if multiplier < 1 {
    96  			multiplier = 1
    97  		}
    98  	}
    99  
   100  	return &Service{
   101  		cfg:  cfg,
   102  		s:    s,
   103  		p:    p,
   104  		base: &Analytics{},
   105  		httpClient: &http.Client{
   106  			Transport: &http.Transport{
   107  				MaxConnsPerHost: 1,
   108  			},
   109  			Timeout: 60 * time.Second,
   110  		},
   111  		stop: make(chan struct{}),
   112  		done: make(chan struct{}),
   113  	}
   114  }
   115  
   116  type Service struct {
   117  	cfg        *config.Server
   118  	s          *storage.Storage
   119  	p          StatsProvider
   120  	base       *Analytics
   121  	httpClient *http.Client
   122  	uploads    int
   123  
   124  	stop chan struct{}
   125  	done chan struct{}
   126  }
   127  
   128  func (s *Service) Start() {
   129  	defer close(s.done)
   130  	err := s.s.LoadAnalytics(s.base)
   131  	if err != nil {
   132  		// this is not really an error, this will always be !nil on the first run, hence Debug level
   133  		logrus.WithError(err).Debug("failed to load analytics data")
   134  	}
   135  
   136  	timer := time.NewTimer(gracePeriod / time.Duration(multiplier))
   137  	select {
   138  	case <-s.stop:
   139  		return
   140  	case <-timer.C:
   141  	}
   142  	s.sendReport()
   143  	s.sendMetrics()
   144  	oldMetricsTicker := time.NewTicker(oldMetricsUploadFrequency / time.Duration(multiplier))
   145  	metricsTicker := time.NewTicker(metricsUploadFrequency / time.Duration(multiplier))
   146  	snapshotTicker := time.NewTicker(snapshotFrequency)
   147  	defer oldMetricsTicker.Stop()
   148  	defer metricsTicker.Stop()
   149  	defer snapshotTicker.Stop()
   150  	for {
   151  		select {
   152  		case <-oldMetricsTicker.C:
   153  			s.sendReport()
   154  		case <-metricsTicker.C:
   155  			s.sendMetrics()
   156  		case <-snapshotTicker.C:
   157  			s.s.SaveAnalytics(s.getAnalytics())
   158  		case <-s.stop:
   159  			return
   160  		}
   161  	}
   162  }
   163  
   164  // TODO: reflection is always tricky to work with. Maybe long term we should just put all counters
   165  //
   166  //	in one map (map[string]int), and put all gauges in another map(map[string]int) and then
   167  //	for gauges we would override old values and for counters we would sum the values up.
   168  func (*Service) rebaseAnalytics(base *Analytics, current *Analytics) *Analytics {
   169  	rebased := &(*current)
   170  	vRebased := reflect.ValueOf(rebased).Elem()
   171  	vCur := reflect.ValueOf(*current)
   172  	vBase := reflect.ValueOf(*base)
   173  	tAnalytics := reflect.TypeOf(*base)
   174  	for i := 0; i < vBase.NumField(); i++ {
   175  		name := tAnalytics.Field(i).Name
   176  		tField := tAnalytics.Field(i).Type
   177  		vBaseField := vBase.FieldByName(name)
   178  		vCurrentField := vCur.FieldByName(name)
   179  		vRebasedField := vRebased.FieldByName(name)
   180  		tag, ok := tAnalytics.Field(i).Tag.Lookup("kind")
   181  		if ok && tag == "cumulative" {
   182  			switch tField.Kind() {
   183  			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
   184  				vRebasedField.SetInt(vBaseField.Int() + vCurrentField.Int())
   185  			}
   186  		}
   187  	}
   188  	return rebased
   189  }
   190  
   191  func (s *Service) Stop() {
   192  	s.s.SaveAnalytics(s.getAnalytics())
   193  	close(s.stop)
   194  	<-s.done
   195  }
   196  
   197  func (s *Service) getAnalytics() *Analytics {
   198  	var ms runtime.MemStats
   199  	runtime.ReadMemStats(&ms)
   200  	du := s.s.DiskUsage()
   201  
   202  	controllerStats := s.p.Stats()
   203  
   204  	a := &Analytics{
   205  		// metadata
   206  		InstallID:            s.s.InstallID(),
   207  		RunID:                uuid.New().String(),
   208  		Version:              build.Version,
   209  		GitSHA:               build.GitSHA,
   210  		BuildTime:            build.Time,
   211  		Timestamp:            time.Now(),
   212  		UploadIndex:          s.uploads,
   213  		GOOS:                 runtime.GOOS,
   214  		GOARCH:               runtime.GOARCH,
   215  		GoVersion:            runtime.Version(),
   216  		AnalyticsPersistence: true,
   217  
   218  		// gauges
   219  		MemAlloc:         int(ms.Alloc),
   220  		MemTotalAlloc:    int(ms.TotalAlloc),
   221  		MemSys:           int(ms.Sys),
   222  		MemNumGC:         int(ms.NumGC),
   223  		BadgerMain:       int(du["main"]),
   224  		BadgerTrees:      int(du["trees"]),
   225  		BadgerDicts:      int(du["dicts"]),
   226  		BadgerDimensions: int(du["dimensions"]),
   227  		BadgerSegments:   int(du["segments"]),
   228  		AppsCount:        s.p.AppsCount(),
   229  
   230  		// counters
   231  		ControllerIndex:      controllerStats["index"],
   232  		ControllerComparison: controllerStats["comparison"],
   233  		ControllerDiff:       controllerStats["diff"],
   234  		ControllerIngest:     controllerStats["ingest"],
   235  		ControllerRender:     controllerStats["render"],
   236  		SpyGospy:             controllerStats["ingest:gospy"],
   237  		SpyEbpfspy:           controllerStats["ingest:ebpfspy"],
   238  		SpyPhpspy:            controllerStats["ingest:phpspy"],
   239  		SpyDotnetspy:         controllerStats["ingest:dotnetspy"],
   240  		SpyJavaspy:           controllerStats["ingest:javaspy"],
   241  	}
   242  	a = s.rebaseAnalytics(s.base, a)
   243  	return a
   244  }
   245  
   246  func (s *Service) sendReport() {
   247  	logrus.Debug("sending analytics report")
   248  
   249  	a := s.getAnalytics()
   250  
   251  	buf, err := json.Marshal(a)
   252  	if err != nil {
   253  		logrus.WithField("err", err).Error("Error happened when preparing JSON")
   254  		return
   255  	}
   256  
   257  	if hostOverride := os.Getenv("PYROSCOPE_ANALYTICS_HOST"); hostOverride != "" {
   258  		host = hostOverride
   259  	}
   260  
   261  	url := host + path
   262  	resp, err := s.httpClient.Post(url, "application/json", bytes.NewReader(buf))
   263  	if err != nil {
   264  		logrus.WithField("err", err).Error("Error happened when uploading anonymized usage data")
   265  	}
   266  	if resp != nil {
   267  		_, err := io.ReadAll(resp.Body)
   268  		if err != nil {
   269  			logrus.WithField("err", err).Error("Error happened when uploading reading server response")
   270  			return
   271  		}
   272  	}
   273  
   274  	s.uploads++
   275  }