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 }