github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/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/loki/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/loki-usage-report"
    28  	statsPrefix   = "github.com/grafana/loki/"
    29  	targetKey     = "target"
    30  	editionKey    = "edition"
    31  )
    32  
    33  // Report is the JSON object sent to the stats server
    34  type Report struct {
    35  	ClusterID              string    `json:"clusterID"`
    36  	CreatedAt              time.Time `json:"createdAt"`
    37  	Interval               time.Time `json:"interval"`
    38  	IntervalPeriod         float64   `json:"intervalPeriod"`
    39  	Target                 string    `json:"target"`
    40  	prom.PrometheusVersion `json:"version"`
    41  	Os                     string                 `json:"os"`
    42  	Arch                   string                 `json:"arch"`
    43  	Edition                string                 `json:"edition"`
    44  	Metrics                map[string]interface{} `json:"metrics"`
    45  }
    46  
    47  // sendReport sends the report to the stats server
    48  func sendReport(ctx context.Context, seed *ClusterSeed, interval time.Time) error {
    49  	report := buildReport(seed, interval)
    50  	out, err := jsoniter.MarshalIndent(report, "", " ")
    51  	if err != nil {
    52  		return err
    53  	}
    54  	req, err := http.NewRequest(http.MethodPost, usageStatsURL, bytes.NewBuffer(out))
    55  	if err != nil {
    56  		return err
    57  	}
    58  	req.Header.Set("Content-Type", "application/json")
    59  	resp, err := httpClient.Do(req.WithContext(ctx))
    60  	if err != nil {
    61  		return err
    62  	}
    63  	defer resp.Body.Close()
    64  	if resp.StatusCode/100 != 2 {
    65  		data, err := io.ReadAll(resp.Body)
    66  		if err != nil {
    67  			return err
    68  		}
    69  		return fmt.Errorf("failed to send usage stats: %s  body: %s", resp.Status, string(data))
    70  	}
    71  	return nil
    72  }
    73  
    74  // buildReport builds the report to be sent to the stats server
    75  func buildReport(seed *ClusterSeed, interval time.Time) Report {
    76  	var (
    77  		targetName  string
    78  		editionName string
    79  	)
    80  	if target := expvar.Get(statsPrefix + targetKey); target != nil {
    81  		if target, ok := target.(*expvar.String); ok {
    82  			targetName = target.Value()
    83  		}
    84  	}
    85  	if edition := expvar.Get(statsPrefix + editionKey); edition != nil {
    86  		if edition, ok := edition.(*expvar.String); ok {
    87  			editionName = edition.Value()
    88  		}
    89  	}
    90  
    91  	return Report{
    92  		ClusterID:         seed.UID,
    93  		PrometheusVersion: build.GetVersion(),
    94  		CreatedAt:         seed.CreatedAt,
    95  		Interval:          interval,
    96  		IntervalPeriod:    reportInterval.Seconds(),
    97  		Os:                runtime.GOOS,
    98  		Arch:              runtime.GOARCH,
    99  		Target:            targetName,
   100  		Edition:           editionName,
   101  		Metrics:           buildMetrics(),
   102  	}
   103  }
   104  
   105  // buildMetrics builds the metrics part of the report to be sent to the stats server
   106  func buildMetrics() map[string]interface{} {
   107  	result := map[string]interface{}{
   108  		"memstats":      memstats(),
   109  		"num_cpu":       runtime.NumCPU(),
   110  		"num_goroutine": runtime.NumGoroutine(),
   111  	}
   112  	expvar.Do(func(kv expvar.KeyValue) {
   113  		if !strings.HasPrefix(kv.Key, statsPrefix) || kv.Key == statsPrefix+targetKey || kv.Key == statsPrefix+editionKey {
   114  			return
   115  		}
   116  		var value interface{}
   117  		switch v := kv.Value.(type) {
   118  		case *expvar.Int:
   119  			value = v.Value()
   120  		case *expvar.Float:
   121  			value = v.Value()
   122  		case *expvar.String:
   123  			value = v.Value()
   124  		case *Statistics:
   125  			value = v.Value()
   126  		case *Counter:
   127  			v.updateRate()
   128  			value = v.Value()
   129  			v.reset()
   130  		case *WordCounter:
   131  			value = v.Value()
   132  		default:
   133  			value = v.String()
   134  		}
   135  		result[strings.TrimPrefix(kv.Key, statsPrefix)] = value
   136  	})
   137  	return result
   138  }
   139  
   140  func memstats() interface{} {
   141  	stats := new(runtime.MemStats)
   142  	runtime.ReadMemStats(stats)
   143  	return map[string]interface{}{
   144  		"alloc":           stats.Alloc,
   145  		"total_alloc":     stats.TotalAlloc,
   146  		"sys":             stats.Sys,
   147  		"heap_alloc":      stats.HeapAlloc,
   148  		"heap_inuse":      stats.HeapInuse,
   149  		"stack_inuse":     stats.StackInuse,
   150  		"pause_total_ns":  stats.PauseTotalNs,
   151  		"num_gc":          stats.NumGC,
   152  		"gc_cpu_fraction": stats.GCCPUFraction,
   153  	}
   154  }
   155  
   156  // NewFloat returns a new Float stats object.
   157  // If a Float stats object with the same name already exists it is returned.
   158  func NewFloat(name string) *expvar.Float {
   159  	existing := expvar.Get(statsPrefix + name)
   160  	if existing != nil {
   161  		if f, ok := existing.(*expvar.Float); ok {
   162  			return f
   163  		}
   164  		panic(fmt.Sprintf("%v is set to a non-float value", name))
   165  	}
   166  	return expvar.NewFloat(statsPrefix + name)
   167  }
   168  
   169  // NewInt returns a new Int stats object.
   170  // If an Int stats object object with the same name already exists it is returned.
   171  func NewInt(name string) *expvar.Int {
   172  	existing := expvar.Get(statsPrefix + name)
   173  	if existing != nil {
   174  		if i, ok := existing.(*expvar.Int); ok {
   175  			return i
   176  		}
   177  		panic(fmt.Sprintf("%v is set to a non-int value", name))
   178  	}
   179  	return expvar.NewInt(statsPrefix + name)
   180  }
   181  
   182  // NewString returns a new String stats object.
   183  // If a String stats object with the same name already exists it is returned.
   184  func NewString(name string) *expvar.String {
   185  	existing := expvar.Get(statsPrefix + name)
   186  	if existing != nil {
   187  		if s, ok := existing.(*expvar.String); ok {
   188  			return s
   189  		}
   190  		panic(fmt.Sprintf("%v is set to a non-string value", name))
   191  	}
   192  	return expvar.NewString(statsPrefix + name)
   193  }
   194  
   195  // Target sets the target name. This can be set multiple times.
   196  func Target(target string) {
   197  	NewString(targetKey).Set(target)
   198  }
   199  
   200  // Edition sets the edition name. This can be set multiple times.
   201  func Edition(edition string) {
   202  	NewString(editionKey).Set(edition)
   203  }
   204  
   205  type Statistics struct {
   206  	min   *atomic.Float64
   207  	max   *atomic.Float64
   208  	count *atomic.Int64
   209  
   210  	avg *atomic.Float64
   211  
   212  	// require for stddev and stdvar
   213  	mean  *atomic.Float64
   214  	value *atomic.Float64
   215  }
   216  
   217  // NewStatistics returns a new Statistics object.
   218  // Statistics object is thread-safe and compute statistics on the fly based on sample recorded.
   219  // Available statistics are:
   220  // - min
   221  // - max
   222  // - avg
   223  // - count
   224  // - stddev
   225  // - stdvar
   226  // If a Statistics object with the same name already exists it is returned.
   227  func NewStatistics(name string) *Statistics {
   228  	s := &Statistics{
   229  		min:   atomic.NewFloat64(math.Inf(0)),
   230  		max:   atomic.NewFloat64(math.Inf(-1)),
   231  		count: atomic.NewInt64(0),
   232  		avg:   atomic.NewFloat64(0),
   233  		mean:  atomic.NewFloat64(0),
   234  		value: atomic.NewFloat64(0),
   235  	}
   236  	existing := expvar.Get(statsPrefix + name)
   237  	if existing != nil {
   238  		if s, ok := existing.(*Statistics); ok {
   239  			return s
   240  		}
   241  		panic(fmt.Sprintf("%v is set to a non-Statistics value", name))
   242  	}
   243  	expvar.Publish(statsPrefix+name, s)
   244  	return s
   245  }
   246  
   247  func (s *Statistics) String() string {
   248  	b, _ := json.Marshal(s.Value())
   249  	return string(b)
   250  }
   251  
   252  func (s *Statistics) Value() map[string]interface{} {
   253  	stdvar := s.value.Load() / float64(s.count.Load())
   254  	stddev := math.Sqrt(stdvar)
   255  	min := s.min.Load()
   256  	max := s.max.Load()
   257  	result := map[string]interface{}{
   258  		"avg":   s.avg.Load(),
   259  		"count": s.count.Load(),
   260  	}
   261  	if !math.IsInf(min, 0) {
   262  		result["min"] = min
   263  	}
   264  	if !math.IsInf(max, 0) {
   265  		result["max"] = s.max.Load()
   266  	}
   267  	if !math.IsNaN(stddev) {
   268  		result["stddev"] = stddev
   269  	}
   270  	if !math.IsNaN(stdvar) {
   271  		result["stdvar"] = stdvar
   272  	}
   273  	return result
   274  }
   275  
   276  func (s *Statistics) Record(v float64) {
   277  	for {
   278  		min := s.min.Load()
   279  		if min <= v {
   280  			break
   281  		}
   282  		if s.min.CAS(min, v) {
   283  			break
   284  		}
   285  	}
   286  	for {
   287  		max := s.max.Load()
   288  		if max >= v {
   289  			break
   290  		}
   291  		if s.max.CAS(max, v) {
   292  			break
   293  		}
   294  	}
   295  	for {
   296  		avg := s.avg.Load()
   297  		count := s.count.Load()
   298  		mean := s.mean.Load()
   299  		value := s.value.Load()
   300  
   301  		delta := v - mean
   302  		newCount := count + 1
   303  		newMean := mean + (delta / float64(newCount))
   304  		newValue := value + (delta * (v - newMean))
   305  		newAvg := avg + ((v - avg) / float64(newCount))
   306  		if s.avg.CAS(avg, newAvg) && s.count.CAS(count, newCount) && s.mean.CAS(mean, newMean) && s.value.CAS(value, newValue) {
   307  			break
   308  		}
   309  	}
   310  }
   311  
   312  type Counter struct {
   313  	total *atomic.Int64
   314  	rate  *atomic.Float64
   315  
   316  	resetTime time.Time
   317  }
   318  
   319  // NewCounter returns a new Counter stats object.
   320  // If a Counter stats object with the same name already exists it is returned.
   321  func NewCounter(name string) *Counter {
   322  	c := &Counter{
   323  		total:     atomic.NewInt64(0),
   324  		rate:      atomic.NewFloat64(0),
   325  		resetTime: time.Now(),
   326  	}
   327  	existing := expvar.Get(statsPrefix + name)
   328  	if existing != nil {
   329  		if c, ok := existing.(*Counter); ok {
   330  			return c
   331  		}
   332  		panic(fmt.Sprintf("%v is set to a non-Counter value", name))
   333  	}
   334  	expvar.Publish(statsPrefix+name, c)
   335  	return c
   336  }
   337  
   338  func (c *Counter) updateRate() {
   339  	total := c.total.Load()
   340  	c.rate.Store(float64(total) / time.Since(c.resetTime).Seconds())
   341  }
   342  
   343  func (c *Counter) reset() {
   344  	c.total.Store(0)
   345  	c.rate.Store(0)
   346  	c.resetTime = time.Now()
   347  }
   348  
   349  func (c *Counter) Inc(i int64) {
   350  	c.total.Add(i)
   351  }
   352  
   353  func (c *Counter) String() string {
   354  	b, _ := json.Marshal(c.Value())
   355  	return string(b)
   356  }
   357  
   358  func (c *Counter) Value() map[string]interface{} {
   359  	return map[string]interface{}{
   360  		"total": c.total.Load(),
   361  		"rate":  c.rate.Load(),
   362  	}
   363  }
   364  
   365  type WordCounter struct {
   366  	words sync.Map
   367  	count *atomic.Int64
   368  }
   369  
   370  // NewWordCounter returns a new WordCounter stats object.
   371  // The WordCounter object is thread-safe and counts the number of words recorded.
   372  // If a WordCounter stats object with the same name already exists it is returned.
   373  func NewWordCounter(name string) *WordCounter {
   374  	c := &WordCounter{
   375  		count: atomic.NewInt64(0),
   376  		words: sync.Map{},
   377  	}
   378  	existing := expvar.Get(statsPrefix + name)
   379  	if existing != nil {
   380  		if w, ok := existing.(*WordCounter); ok {
   381  			return w
   382  		}
   383  		panic(fmt.Sprintf("%v is set to a non-WordCounter value", name))
   384  	}
   385  	expvar.Publish(statsPrefix+name, c)
   386  	return c
   387  }
   388  
   389  func (w *WordCounter) Add(word string) {
   390  	if _, loaded := w.words.LoadOrStore(xxhash.Sum64String(word), struct{}{}); !loaded {
   391  		w.count.Add(1)
   392  	}
   393  }
   394  
   395  func (w *WordCounter) String() string {
   396  	b, _ := json.Marshal(w.Value())
   397  	return string(b)
   398  }
   399  
   400  func (w *WordCounter) Value() int64 {
   401  	return w.count.Load()
   402  }