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  }