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 }