github.com/grafana/pyroscope@v1.18.0/pkg/usagestats/stats.go (about) 1 package usagestats 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "expvar" 8 "fmt" 9 "io" 10 "math" 11 "net/http" 12 "runtime" 13 "strings" 14 "sync" 15 "time" 16 17 "github.com/grafana/pyroscope/pkg/util/build" 18 19 "github.com/cespare/xxhash/v2" 20 jsoniter "github.com/json-iterator/go" 21 prom "github.com/prometheus/prometheus/web/api/v1" 22 "go.uber.org/atomic" 23 ) 24 25 var ( 26 httpClient = http.Client{Timeout: 5 * time.Second} 27 usageStatsURL = "https://stats.grafana.org/phlare-usage-report" 28 statsPrefix = "github.com/grafana/pyroscope/" 29 targetKey = "target" 30 editionKey = "edition" 31 32 createLock sync.RWMutex 33 ) 34 35 // Report is the JSON object sent to the stats server 36 type Report struct { 37 ClusterID string `json:"clusterID"` 38 CreatedAt time.Time `json:"createdAt"` 39 Interval time.Time `json:"interval"` 40 IntervalPeriod float64 `json:"intervalPeriod"` 41 Target string `json:"target"` 42 prom.PrometheusVersion `json:"version"` 43 Os string `json:"os"` 44 Arch string `json:"arch"` 45 Edition string `json:"edition"` 46 Metrics map[string]interface{} `json:"metrics"` 47 } 48 49 // sendReport sends the report to the stats server 50 func sendReport(ctx context.Context, seed ClusterSeed, interval time.Time) error { 51 report := buildReport(seed, interval) 52 out, err := jsoniter.MarshalIndent(report, "", " ") 53 if err != nil { 54 return err 55 } 56 req, err := http.NewRequest(http.MethodPost, usageStatsURL, bytes.NewBuffer(out)) 57 if err != nil { 58 return err 59 } 60 req.Header.Set("Content-Type", "application/json") 61 resp, err := httpClient.Do(req.WithContext(ctx)) 62 if err != nil { 63 return err 64 } 65 defer resp.Body.Close() 66 if resp.StatusCode/100 != 2 { 67 data, err := io.ReadAll(resp.Body) 68 if err != nil { 69 return err 70 } 71 return fmt.Errorf("failed to send usage stats: %s body: %s", resp.Status, string(data)) 72 } 73 return nil 74 } 75 76 // buildReport builds the report to be sent to the stats server 77 func buildReport(seed ClusterSeed, interval time.Time) Report { 78 var ( 79 targetName string 80 editionName string 81 ) 82 if target := expvar.Get(statsPrefix + targetKey); target != nil { 83 if target, ok := target.(*expvar.String); ok { 84 targetName = target.Value() 85 } 86 } 87 if edition := expvar.Get(statsPrefix + editionKey); edition != nil { 88 if edition, ok := edition.(*expvar.String); ok { 89 editionName = edition.Value() 90 } 91 } 92 93 return Report{ 94 ClusterID: seed.UID, 95 PrometheusVersion: build.GetVersion(), 96 CreatedAt: seed.CreatedAt, 97 Interval: interval, 98 IntervalPeriod: reportInterval.Seconds(), 99 Os: runtime.GOOS, 100 Arch: runtime.GOARCH, 101 Target: targetName, 102 Edition: editionName, 103 Metrics: buildMetrics(), 104 } 105 } 106 107 // buildMetrics builds the metrics part of the report to be sent to the stats server 108 func buildMetrics() map[string]interface{} { 109 result := map[string]interface{}{ 110 "memstats": memstats(), 111 "num_cpu": runtime.NumCPU(), 112 "num_goroutine": runtime.NumGoroutine(), 113 } 114 expvar.Do(func(kv expvar.KeyValue) { 115 if !strings.HasPrefix(kv.Key, statsPrefix) || kv.Key == statsPrefix+targetKey || kv.Key == statsPrefix+editionKey { 116 return 117 } 118 var value interface{} 119 switch v := kv.Value.(type) { 120 case *expvar.Int: 121 value = v.Value() 122 case *expvar.Float: 123 value = v.Value() 124 case *expvar.String: 125 value = v.Value() 126 case *Statistics: 127 value = v.Value() 128 case *MultiStatistics: 129 value = v.Value() 130 case *Counter: 131 v.updateRate() 132 value = v.Value() 133 v.reset() 134 case *MultiCounter: 135 v.updateRate() 136 value = v.Value() 137 v.reset() 138 case *WordCounter: 139 value = v.Value() 140 default: 141 value = v.String() 142 } 143 result[strings.TrimPrefix(kv.Key, statsPrefix)] = value 144 }) 145 return result 146 } 147 148 func memstats() interface{} { 149 stats := new(runtime.MemStats) 150 runtime.ReadMemStats(stats) 151 return map[string]interface{}{ 152 "alloc": stats.Alloc, 153 "total_alloc": stats.TotalAlloc, 154 "sys": stats.Sys, 155 "heap_alloc": stats.HeapAlloc, 156 "heap_inuse": stats.HeapInuse, 157 "stack_inuse": stats.StackInuse, 158 "pause_total_ns": stats.PauseTotalNs, 159 "num_gc": stats.NumGC, 160 "gc_cpu_fraction": stats.GCCPUFraction, 161 } 162 } 163 164 // NewFloat returns a new Float stats object. 165 // If a Float stats object with the same name already exists it is returned. 166 func NewFloat(name string) *expvar.Float { 167 return createOrRetrieveExpvar( 168 func() (*expvar.Float, error) { // check 169 existing := expvar.Get(statsPrefix + name) 170 if existing != nil { 171 if f, ok := existing.(*expvar.Float); ok { 172 return f, nil 173 } 174 return nil, fmt.Errorf("%v is set to a non-float value", name) 175 } 176 return nil, nil 177 }, 178 func() *expvar.Float { // create 179 return expvar.NewFloat(statsPrefix + name) 180 }, 181 ) 182 } 183 184 // NewInt returns a new Int stats object. 185 // If an Int stats object object with the same name already exists it is returned. 186 func NewInt(name string) *expvar.Int { 187 return createOrRetrieveExpvar( 188 func() (*expvar.Int, error) { // check 189 existing := expvar.Get(statsPrefix + name) 190 if existing != nil { 191 if i, ok := existing.(*expvar.Int); ok { 192 return i, nil 193 } 194 return nil, fmt.Errorf("%v is set to a non-int value", name) 195 } 196 return nil, nil 197 }, 198 func() *expvar.Int { // create 199 return expvar.NewInt(statsPrefix + name) 200 }, 201 ) 202 } 203 204 // NewString returns a new String stats object. 205 // If a String stats object with the same name already exists it is returned. 206 func NewString(name string) *expvar.String { 207 return createOrRetrieveExpvar( 208 func() (*expvar.String, error) { // check 209 existing := expvar.Get(statsPrefix + name) 210 if existing != nil { 211 if s, ok := existing.(*expvar.String); ok { 212 return s, nil 213 } 214 return nil, fmt.Errorf("%v is set to a non-string value", name) 215 } 216 return nil, nil 217 }, 218 func() *expvar.String { // create 219 return expvar.NewString(statsPrefix + name) 220 }, 221 ) 222 } 223 224 // Target sets the target name. This can be set multiple times. 225 func Target(target string) { 226 NewString(targetKey).Set(target) 227 } 228 229 // Edition sets the edition name. This can be set multiple times. 230 func Edition(edition string) { 231 NewString(editionKey).Set(edition) 232 } 233 234 type Statistics struct { 235 min *atomic.Float64 236 max *atomic.Float64 237 count *atomic.Int64 238 239 avg *atomic.Float64 240 241 // require for stddev and stdvar 242 mean *atomic.Float64 243 value *atomic.Float64 244 } 245 246 // NewStatistics returns a new Statistics object. 247 // Statistics object is thread-safe and compute statistics on the fly based on sample recorded. 248 // Available statistics are: 249 // - min 250 // - max 251 // - avg 252 // - count 253 // - stddev 254 // - stdvar 255 // If a Statistics object with the same name already exists it is returned. 256 func NewStatistics(name string) *Statistics { 257 return createOrRetrieveExpvar( 258 func() (*Statistics, error) { // check 259 260 existing := expvar.Get(statsPrefix + name) 261 if existing != nil { 262 if s, ok := existing.(*Statistics); ok { 263 return s, nil 264 } 265 return nil, fmt.Errorf("%v is set to a non-Statistics value", name) 266 } 267 return nil, nil 268 }, 269 func() *Statistics { // create 270 s := &Statistics{ 271 min: atomic.NewFloat64(math.Inf(0)), 272 max: atomic.NewFloat64(math.Inf(-1)), 273 count: atomic.NewInt64(0), 274 avg: atomic.NewFloat64(0), 275 mean: atomic.NewFloat64(0), 276 value: atomic.NewFloat64(0), 277 } 278 expvar.Publish(statsPrefix+name, s) 279 return s 280 }, 281 ) 282 } 283 284 func (s *Statistics) String() string { 285 b, _ := json.Marshal(s.Value()) 286 return string(b) 287 } 288 289 func (s *Statistics) Value() map[string]interface{} { 290 stdvar := s.value.Load() / float64(s.count.Load()) 291 stddev := math.Sqrt(stdvar) 292 min := s.min.Load() 293 max := s.max.Load() 294 result := map[string]interface{}{ 295 "avg": s.avg.Load(), 296 "count": s.count.Load(), 297 } 298 if !math.IsInf(min, 0) { 299 result["min"] = min 300 } 301 if !math.IsInf(max, 0) { 302 result["max"] = s.max.Load() 303 } 304 if !math.IsNaN(stddev) { 305 result["stddev"] = stddev 306 } 307 if !math.IsNaN(stdvar) { 308 result["stdvar"] = stdvar 309 } 310 return result 311 } 312 313 func (s *Statistics) Record(v float64) { 314 for { 315 min := s.min.Load() 316 if min <= v { 317 break 318 } 319 if s.min.CompareAndSwap(min, v) { 320 break 321 } 322 } 323 for { 324 max := s.max.Load() 325 if max >= v { 326 break 327 } 328 if s.max.CompareAndSwap(max, v) { 329 break 330 } 331 } 332 for { 333 avg := s.avg.Load() 334 count := s.count.Load() 335 mean := s.mean.Load() 336 value := s.value.Load() 337 338 delta := v - mean 339 newCount := count + 1 340 newMean := mean + (delta / float64(newCount)) 341 newValue := value + (delta * (v - newMean)) 342 newAvg := avg + ((v - avg) / float64(newCount)) 343 if s.avg.CompareAndSwap(avg, newAvg) && s.count.CompareAndSwap(count, newCount) && s.mean.CompareAndSwap(mean, newMean) && s.value.CompareAndSwap(value, newValue) { 344 break 345 } 346 } 347 } 348 349 type MultiStatistics struct { 350 m sync.RWMutex 351 values map[string]*Statistics 352 keyName string 353 } 354 355 // NewMultiStatistics returns a new MultiStatistics object. 356 // MultiStatistics object is thread-safe and computes statistics on the fly based on recorded samples. 357 // Available statistics are: 358 // - min 359 // - max 360 // - avg 361 // - count 362 // - stddev 363 // - stdvar 364 // If a MultiStatistics object with the same name already exists it is returned. 365 func NewMultiStatistics(name string, keyName string) *MultiStatistics { 366 return createOrRetrieveExpvar( 367 func() (*MultiStatistics, error) { // check 368 existing := expvar.Get(statsPrefix + name) 369 if existing != nil { 370 if s, ok := existing.(*MultiStatistics); ok { 371 return s, nil 372 } 373 return nil, fmt.Errorf("%v is set to a non-MultiStatistics value", name) 374 } 375 return nil, nil 376 }, 377 func() *MultiStatistics { // create 378 s := &MultiStatistics{ 379 values: map[string]*Statistics{ 380 "__total__": { 381 min: atomic.NewFloat64(math.Inf(0)), 382 max: atomic.NewFloat64(math.Inf(-1)), 383 count: atomic.NewInt64(0), 384 avg: atomic.NewFloat64(0), 385 mean: atomic.NewFloat64(0), 386 value: atomic.NewFloat64(0), 387 }, 388 }, 389 keyName: keyName, 390 } 391 expvar.Publish(statsPrefix+name, s) 392 return s 393 }, 394 ) 395 } 396 397 func (s *MultiStatistics) String() string { 398 b, _ := json.Marshal(s.Value()) 399 return string(b) 400 } 401 402 func (s *MultiStatistics) Value() map[string]interface{} { 403 s.m.RLock() 404 defer s.m.RUnlock() 405 var value map[string]interface{} 406 valuesPerKey := make([]interface{}, 0, len(s.values)) 407 for k, v := range s.values { 408 if k == "__total__" { 409 value = v.Value() 410 } else { 411 valuesPerKey = append(valuesPerKey, map[string]interface{}{ 412 s.keyName: k, 413 "data": v.Value(), 414 }) 415 } 416 } 417 value["drilldown"] = valuesPerKey 418 return value 419 } 420 421 func (s *MultiStatistics) Record(v float64, key string) { 422 keyStats := s.getOrCreateStatistics(key) 423 keyStats.Record(v) 424 s.values["__total__"].Record(v) 425 } 426 427 func (s *MultiStatistics) getOrCreateStatistics(key string) *Statistics { 428 s.m.RLock() 429 keyStats, ok := s.values[key] 430 s.m.RUnlock() 431 if ok { 432 return keyStats 433 } 434 s.m.Lock() 435 defer s.m.Unlock() 436 keyStats, ok = s.values[key] 437 if ok { 438 return keyStats 439 } 440 keyStats = &Statistics{ 441 min: atomic.NewFloat64(math.Inf(0)), 442 max: atomic.NewFloat64(math.Inf(-1)), 443 count: atomic.NewInt64(0), 444 avg: atomic.NewFloat64(0), 445 mean: atomic.NewFloat64(0), 446 value: atomic.NewFloat64(0), 447 } 448 s.values[key] = keyStats 449 return keyStats 450 } 451 452 type Counter struct { 453 total *atomic.Int64 454 rate *atomic.Float64 455 456 resetTime time.Time 457 } 458 459 func createOrRetrieveExpvar[K any](check func() (*K, error), create func() *K) *K { 460 // check if string exists holding read lock 461 createLock.RLock() 462 s, err := check() 463 createLock.RUnlock() 464 if err != nil { 465 panic(err.Error()) 466 } 467 if s != nil { 468 return s 469 } 470 471 // acquire write lock and check again and create if still missing 472 createLock.Lock() 473 defer createLock.Unlock() 474 s, err = check() 475 if err != nil { 476 panic(err.Error()) 477 } 478 if s != nil { 479 return s 480 } 481 482 return create() 483 } 484 485 // NewCounter returns a new Counter stats object. 486 // If a Counter stats object with the same name already exists it is returned. 487 func NewCounter(name string) *Counter { 488 return createOrRetrieveExpvar( 489 func() (*Counter, error) { // check 490 existing := expvar.Get(statsPrefix + name) 491 if existing != nil { 492 if c, ok := existing.(*Counter); ok { 493 return c, nil 494 } 495 return nil, fmt.Errorf("%v is set to a non-Counter value", name) 496 } 497 return nil, nil 498 }, 499 func() *Counter { // create 500 c := &Counter{ 501 total: atomic.NewInt64(0), 502 rate: atomic.NewFloat64(0), 503 resetTime: time.Now(), 504 } 505 expvar.Publish(statsPrefix+name, c) 506 return c 507 }, 508 ) 509 } 510 511 func (c *Counter) updateRate() { 512 total := c.total.Load() 513 c.rate.Store(float64(total) / time.Since(c.resetTime).Seconds()) 514 } 515 516 func (c *Counter) reset() { 517 c.total.Store(0) 518 c.rate.Store(0) 519 c.resetTime = time.Now() 520 } 521 522 func (c *Counter) Inc(i int64) { 523 c.total.Add(i) 524 } 525 526 func (c *Counter) String() string { 527 b, _ := json.Marshal(c.Value()) 528 return string(b) 529 } 530 531 func (c *Counter) Value() map[string]interface{} { 532 return map[string]interface{}{ 533 "total": c.total.Load(), 534 "rate": c.rate.Load(), 535 } 536 } 537 538 type MultiCounter struct { 539 m sync.RWMutex 540 values map[string]*Counter 541 keyName string 542 } 543 544 // NewMultiCounter returns a new MultiCounter stats object. 545 // If a NewMultiCounter stats object with the same name already exists it is returned. 546 func NewMultiCounter(name string, keyName string) *MultiCounter { 547 return createOrRetrieveExpvar( 548 func() (*MultiCounter, error) { // check 549 existing := expvar.Get(statsPrefix + name) 550 if existing != nil { 551 if c, ok := existing.(*MultiCounter); ok { 552 return c, nil 553 } 554 return nil, fmt.Errorf("%v is set to a non-MultiCounter value", name) 555 } 556 return nil, nil 557 }, 558 func() *MultiCounter { // create 559 c := &MultiCounter{ 560 values: map[string]*Counter{ 561 "__total__": { 562 total: atomic.NewInt64(0), 563 rate: atomic.NewFloat64(0), 564 resetTime: time.Now(), 565 }, 566 }, 567 keyName: keyName, 568 } 569 expvar.Publish(statsPrefix+name, c) 570 return c 571 }, 572 ) 573 } 574 575 func (c *MultiCounter) updateRate() { 576 c.m.RLock() 577 defer c.m.RUnlock() 578 for _, v := range c.values { 579 v.updateRate() 580 } 581 } 582 583 func (c *MultiCounter) reset() { 584 c.m.RLock() 585 defer c.m.RUnlock() 586 for _, v := range c.values { 587 v.reset() 588 } 589 } 590 591 func (c *MultiCounter) Inc(i int64, keyValue string) { 592 v := c.getOrCreateCounter(keyValue) 593 v.Inc(i) 594 c.values["__total__"].Inc(i) 595 } 596 597 func (c *MultiCounter) getOrCreateCounter(keyValue string) *Counter { 598 c.m.RLock() 599 v, ok := c.values[keyValue] 600 c.m.RUnlock() 601 if ok { 602 return v 603 } 604 c.m.Lock() 605 defer c.m.Unlock() 606 v, ok = c.values[keyValue] 607 if ok { 608 return v 609 } 610 v = &Counter{ 611 total: atomic.NewInt64(0), 612 rate: atomic.NewFloat64(0), 613 resetTime: time.Now(), 614 } 615 c.values[keyValue] = v 616 return v 617 } 618 619 func (c *MultiCounter) String() string { 620 b, _ := json.Marshal(c.Value()) 621 return string(b) 622 } 623 624 func (c *MultiCounter) Value() map[string]interface{} { 625 c.m.RLock() 626 defer c.m.RUnlock() 627 var value map[string]interface{} 628 valuesPerKey := make([]interface{}, 0, len(c.values)) 629 for k, v := range c.values { 630 if k == "__total__" { 631 value = v.Value() 632 } else { 633 valuesPerKey = append(valuesPerKey, map[string]interface{}{ 634 c.keyName: k, 635 "data": v.Value(), 636 }) 637 } 638 } 639 value["drilldown"] = valuesPerKey 640 return value 641 } 642 643 type WordCounter struct { 644 words sync.Map 645 count *atomic.Int64 646 } 647 648 // NewWordCounter returns a new WordCounter stats object. 649 // The WordCounter object is thread-safe and counts the number of words recorded. 650 // If a WordCounter stats object with the same name already exists it is returned. 651 func NewWordCounter(name string) *WordCounter { 652 return createOrRetrieveExpvar( 653 func() (*WordCounter, error) { // check 654 existing := expvar.Get(statsPrefix + name) 655 if existing != nil { 656 if w, ok := existing.(*WordCounter); ok { 657 return w, nil 658 } 659 return nil, fmt.Errorf("%v is set to a non-WordCounter value", name) 660 } 661 return nil, nil 662 }, 663 func() *WordCounter { // create 664 c := &WordCounter{ 665 count: atomic.NewInt64(0), 666 words: sync.Map{}, 667 } 668 expvar.Publish(statsPrefix+name, c) 669 return c 670 }, 671 ) 672 } 673 674 func (w *WordCounter) Add(word string) { 675 if _, loaded := w.words.LoadOrStore(xxhash.Sum64String(word), struct{}{}); !loaded { 676 w.count.Add(1) 677 } 678 } 679 680 func (w *WordCounter) String() string { 681 b, _ := json.Marshal(w.Value()) 682 return string(b) 683 } 684 685 func (w *WordCounter) Value() int64 { 686 return w.count.Load() 687 }