github.com/uber-go/tally/v4@v4.1.16/scope_test.go (about) 1 // Copyright (c) 2023 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package tally 22 23 import ( 24 "fmt" 25 "math" 26 "math/rand" 27 "strconv" 28 "strings" 29 "sync" 30 "sync/atomic" 31 "testing" 32 "time" 33 34 "github.com/stretchr/testify/assert" 35 "github.com/stretchr/testify/require" 36 "go.uber.org/goleak" 37 ) 38 39 func TestMain(m *testing.M) { 40 goleak.VerifyTestMain(m) 41 } 42 43 var ( 44 // alphanumericSanitizerOpts is the options to create a sanitizer which uses 45 // the alphanumeric SanitizeFn. 46 alphanumericSanitizerOpts = SanitizeOptions{ 47 NameCharacters: ValidCharacters{ 48 Ranges: AlphanumericRange, 49 Characters: UnderscoreDashCharacters, 50 }, 51 KeyCharacters: ValidCharacters{ 52 Ranges: AlphanumericRange, 53 Characters: UnderscoreDashCharacters, 54 }, 55 ValueCharacters: ValidCharacters{ 56 Ranges: AlphanumericRange, 57 Characters: UnderscoreDashCharacters, 58 }, 59 ReplacementCharacter: DefaultReplacementCharacter, 60 } 61 ) 62 63 type testIntValue struct { 64 val int64 65 tags map[string]string 66 reporter *testStatsReporter 67 } 68 69 func (m *testIntValue) ReportCount(value int64) { 70 m.val = value 71 m.reporter.cg.Done() 72 } 73 74 func (m *testIntValue) ReportTimer(interval time.Duration) { 75 m.val = int64(interval) 76 m.reporter.tg.Done() 77 } 78 79 type testFloatValue struct { 80 val float64 81 tags map[string]string 82 reporter *testStatsReporter 83 } 84 85 func (m *testFloatValue) ReportGauge(value float64) { 86 m.val = value 87 m.reporter.gg.Done() 88 } 89 90 type testHistogramValue struct { 91 tags map[string]string 92 valueSamples map[float64]int 93 durationSamples map[time.Duration]int 94 } 95 96 func newTestHistogramValue() *testHistogramValue { 97 return &testHistogramValue{ 98 valueSamples: make(map[float64]int), 99 durationSamples: make(map[time.Duration]int), 100 } 101 } 102 103 type testStatsReporter struct { 104 cg sync.WaitGroup 105 gg sync.WaitGroup 106 tg sync.WaitGroup 107 hg sync.WaitGroup 108 109 counters map[string]*testIntValue 110 gauges map[string]*testFloatValue 111 timers map[string]*testIntValue 112 histograms map[string]*testHistogramValue 113 114 flushes int32 115 } 116 117 // newTestStatsReporter returns a new TestStatsReporter 118 func newTestStatsReporter() *testStatsReporter { 119 return &testStatsReporter{ 120 counters: make(map[string]*testIntValue), 121 gauges: make(map[string]*testFloatValue), 122 timers: make(map[string]*testIntValue), 123 histograms: make(map[string]*testHistogramValue), 124 } 125 } 126 127 func (r *testStatsReporter) getCounters() map[string]*testIntValue { 128 dst := make(map[string]*testIntValue, len(r.counters)) 129 for k, v := range r.counters { 130 var ( 131 parts = strings.Split(k, "+") 132 name string 133 ) 134 if len(parts) > 0 { 135 name = parts[0] 136 } 137 138 dst[name] = v 139 } 140 141 return dst 142 } 143 144 func (r *testStatsReporter) getGauges() map[string]*testFloatValue { 145 dst := make(map[string]*testFloatValue, len(r.gauges)) 146 for k, v := range r.gauges { 147 var ( 148 parts = strings.Split(k, "+") 149 name string 150 ) 151 if len(parts) > 0 { 152 name = parts[0] 153 } 154 155 dst[name] = v 156 } 157 158 return dst 159 } 160 161 func (r *testStatsReporter) getTimers() map[string]*testIntValue { 162 dst := make(map[string]*testIntValue, len(r.timers)) 163 for k, v := range r.timers { 164 var ( 165 parts = strings.Split(k, "+") 166 name string 167 ) 168 if len(parts) > 0 { 169 name = parts[0] 170 } 171 172 dst[name] = v 173 } 174 175 return dst 176 } 177 178 func (r *testStatsReporter) getHistograms() map[string]*testHistogramValue { 179 dst := make(map[string]*testHistogramValue, len(r.histograms)) 180 for k, v := range r.histograms { 181 var ( 182 parts = strings.Split(k, "+") 183 name string 184 ) 185 if len(parts) > 0 { 186 name = parts[0] 187 } 188 189 dst[name] = v 190 } 191 192 return dst 193 } 194 195 func (r *testStatsReporter) WaitAll() { 196 r.cg.Wait() 197 r.gg.Wait() 198 r.tg.Wait() 199 r.hg.Wait() 200 } 201 202 func (r *testStatsReporter) AllocateCounter( 203 name string, tags map[string]string, 204 ) CachedCount { 205 counter := &testIntValue{ 206 val: 0, 207 tags: tags, 208 reporter: r, 209 } 210 r.counters[name] = counter 211 return counter 212 } 213 214 func (r *testStatsReporter) ReportCounter(name string, tags map[string]string, value int64) { 215 r.counters[name] = &testIntValue{ 216 val: value, 217 tags: tags, 218 } 219 r.cg.Done() 220 } 221 222 func (r *testStatsReporter) AllocateGauge( 223 name string, tags map[string]string, 224 ) CachedGauge { 225 gauge := &testFloatValue{ 226 val: 0, 227 tags: tags, 228 reporter: r, 229 } 230 r.gauges[name] = gauge 231 return gauge 232 } 233 234 func (r *testStatsReporter) ReportGauge(name string, tags map[string]string, value float64) { 235 r.gauges[name] = &testFloatValue{ 236 val: value, 237 tags: tags, 238 } 239 r.gg.Done() 240 } 241 242 func (r *testStatsReporter) AllocateTimer( 243 name string, tags map[string]string, 244 ) CachedTimer { 245 timer := &testIntValue{ 246 val: 0, 247 tags: tags, 248 reporter: r, 249 } 250 r.timers[name] = timer 251 return timer 252 } 253 254 func (r *testStatsReporter) ReportTimer(name string, tags map[string]string, interval time.Duration) { 255 r.timers[name] = &testIntValue{ 256 val: int64(interval), 257 tags: tags, 258 } 259 r.tg.Done() 260 } 261 262 func (r *testStatsReporter) AllocateHistogram( 263 name string, 264 tags map[string]string, 265 buckets Buckets, 266 ) CachedHistogram { 267 return testStatsReporterCachedHistogram{r, name, tags, buckets} 268 } 269 270 type testStatsReporterCachedHistogram struct { 271 r *testStatsReporter 272 name string 273 tags map[string]string 274 buckets Buckets 275 } 276 277 func (h testStatsReporterCachedHistogram) ValueBucket( 278 bucketLowerBound, bucketUpperBound float64, 279 ) CachedHistogramBucket { 280 return testStatsReporterCachedHistogramValueBucket{h, bucketLowerBound, bucketUpperBound} 281 } 282 283 func (h testStatsReporterCachedHistogram) DurationBucket( 284 bucketLowerBound, bucketUpperBound time.Duration, 285 ) CachedHistogramBucket { 286 return testStatsReporterCachedHistogramDurationBucket{h, bucketLowerBound, bucketUpperBound} 287 } 288 289 type testStatsReporterCachedHistogramValueBucket struct { 290 histogram testStatsReporterCachedHistogram 291 bucketLowerBound float64 292 bucketUpperBound float64 293 } 294 295 func (b testStatsReporterCachedHistogramValueBucket) ReportSamples(v int64) { 296 b.histogram.r.ReportHistogramValueSamples( 297 b.histogram.name, b.histogram.tags, 298 b.histogram.buckets, b.bucketLowerBound, b.bucketUpperBound, v, 299 ) 300 } 301 302 type testStatsReporterCachedHistogramDurationBucket struct { 303 histogram testStatsReporterCachedHistogram 304 bucketLowerBound time.Duration 305 bucketUpperBound time.Duration 306 } 307 308 func (b testStatsReporterCachedHistogramDurationBucket) ReportSamples(v int64) { 309 b.histogram.r.ReportHistogramDurationSamples( 310 b.histogram.name, b.histogram.tags, 311 b.histogram.buckets, b.bucketLowerBound, b.bucketUpperBound, v, 312 ) 313 } 314 315 func (r *testStatsReporter) ReportHistogramValueSamples( 316 name string, 317 tags map[string]string, 318 buckets Buckets, 319 bucketLowerBound float64, 320 bucketUpperBound float64, 321 samples int64, 322 ) { 323 key := KeyForPrefixedStringMap(name, tags) 324 value, ok := r.histograms[key] 325 if !ok { 326 value = newTestHistogramValue() 327 value.tags = tags 328 r.histograms[key] = value 329 } 330 value.valueSamples[bucketUpperBound] = int(samples) 331 r.hg.Done() 332 } 333 334 func (r *testStatsReporter) ReportHistogramDurationSamples( 335 name string, 336 tags map[string]string, 337 buckets Buckets, 338 bucketLowerBound time.Duration, 339 bucketUpperBound time.Duration, 340 samples int64, 341 ) { 342 key := KeyForPrefixedStringMap(name, tags) 343 value, ok := r.histograms[key] 344 if !ok { 345 value = newTestHistogramValue() 346 value.tags = tags 347 r.histograms[key] = value 348 } 349 value.durationSamples[bucketUpperBound] = int(samples) 350 r.hg.Done() 351 } 352 353 func (r *testStatsReporter) Capabilities() Capabilities { 354 return capabilitiesReportingNoTagging 355 } 356 357 func (r *testStatsReporter) Flush() { 358 atomic.AddInt32(&r.flushes, 1) 359 } 360 361 func TestWriteTimerImmediately(t *testing.T) { 362 r := newTestStatsReporter() 363 s, closer := NewRootScope(ScopeOptions{Reporter: r, OmitCardinalityMetrics: true}, 0) 364 defer closer.Close() 365 r.tg.Add(1) 366 s.Timer("ticky").Record(time.Millisecond * 175) 367 r.tg.Wait() 368 } 369 370 func TestWriteTimerClosureImmediately(t *testing.T) { 371 r := newTestStatsReporter() 372 s, closer := NewRootScope(ScopeOptions{Reporter: r, OmitCardinalityMetrics: true}, 0) 373 defer closer.Close() 374 r.tg.Add(1) 375 tm := s.Timer("ticky") 376 tm.Start().Stop() 377 r.tg.Wait() 378 } 379 380 func TestWriteReportLoop(t *testing.T) { 381 r := newTestStatsReporter() 382 s, closer := NewRootScope(ScopeOptions{Reporter: r, OmitCardinalityMetrics: true}, 10) 383 defer closer.Close() 384 385 r.cg.Add(1) 386 s.Counter("bar").Inc(1) 387 r.gg.Add(1) 388 s.Gauge("zed").Update(1) 389 r.tg.Add(1) 390 s.Timer("ticky").Record(time.Millisecond * 175) 391 r.hg.Add(1) 392 s.Histogram("baz", MustMakeLinearValueBuckets(0, 10, 10)). 393 RecordValue(42.42) 394 395 r.WaitAll() 396 } 397 398 func TestWriteReportLoopDefaultInterval(t *testing.T) { 399 r := newTestStatsReporter() 400 s, closer := NewRootScopeWithDefaultInterval( 401 ScopeOptions{Reporter: r, OmitCardinalityMetrics: true}, 402 ) 403 defer closer.Close() 404 405 r.cg.Add(1) 406 s.Counter("bar").Inc(1) 407 r.gg.Add(1) 408 s.Gauge("zed").Update(1) 409 r.tg.Add(1) 410 s.Timer("ticky").Record(time.Millisecond * 175) 411 r.hg.Add(1) 412 s.Histogram("baz", MustMakeLinearValueBuckets(0, 10, 10)). 413 RecordValue(42.42) 414 415 r.WaitAll() 416 } 417 418 func TestCachedReportLoop(t *testing.T) { 419 r := newTestStatsReporter() 420 s, closer := NewRootScope(ScopeOptions{CachedReporter: r, OmitCardinalityMetrics: true}, 10) 421 defer closer.Close() 422 423 r.cg.Add(1) 424 s.Counter("bar").Inc(1) 425 r.gg.Add(1) 426 s.Gauge("zed").Update(1) 427 r.tg.Add(1) 428 s.Timer("ticky").Record(time.Millisecond * 175) 429 r.hg.Add(1) 430 s.Histogram("baz", MustMakeLinearValueBuckets(0, 10, 10)). 431 RecordValue(42.42) 432 r.WaitAll() 433 } 434 435 func testReportLoopFlushOnce(t *testing.T, cached bool) { 436 r := newTestStatsReporter() 437 438 scopeOpts := ScopeOptions{CachedReporter: r, OmitCardinalityMetrics: true} 439 if !cached { 440 scopeOpts = ScopeOptions{Reporter: r, OmitCardinalityMetrics: true} 441 } 442 443 s, closer := NewRootScope(scopeOpts, 10*time.Minute) 444 445 r.cg.Add(2) 446 s.Counter("foobar").Inc(1) 447 s.SubScope("baz").Counter("bar").Inc(1) 448 r.gg.Add(2) 449 s.Gauge("zed").Update(1) 450 s.SubScope("baz").Gauge("zed").Update(1) 451 r.tg.Add(2) 452 s.Timer("ticky").Record(time.Millisecond * 175) 453 s.SubScope("woof").Timer("sod").Record(time.Millisecond * 175) 454 r.hg.Add(2) 455 s.SubScope("woofers").Histogram("boo", MustMakeLinearValueBuckets(0, 10, 10)). 456 RecordValue(42.42) 457 s.Histogram("baz", MustMakeLinearValueBuckets(0, 10, 10)). 458 RecordValue(42.42) 459 460 closer.Close() 461 r.WaitAll() 462 463 v := atomic.LoadInt32(&r.flushes) 464 assert.Equal(t, int32(1), v) 465 } 466 467 func TestCachedReporterFlushOnce(t *testing.T) { 468 testReportLoopFlushOnce(t, true) 469 } 470 471 func TestReporterFlushOnce(t *testing.T) { 472 testReportLoopFlushOnce(t, false) 473 } 474 475 func TestWriteOnce(t *testing.T) { 476 r := newTestStatsReporter() 477 478 root, closer := NewRootScope(ScopeOptions{Reporter: r, OmitCardinalityMetrics: true}, 0) 479 defer closer.Close() 480 481 s := root.(*scope) 482 483 r.cg.Add(1) 484 s.Counter("bar").Inc(1) 485 r.gg.Add(1) 486 s.Gauge("zed").Update(1) 487 r.tg.Add(1) 488 s.Timer("ticky").Record(time.Millisecond * 175) 489 r.hg.Add(1) 490 s.Histogram("baz", MustMakeLinearValueBuckets(0, 10, 10)). 491 RecordValue(42.42) 492 r.hg.Add(1) 493 s.Histogram("bat", MustMakeLinearValueBuckets(1, 1, 3)).RecordValue(2.1) 494 r.hg.Add(1) 495 s.SubScope("test").Histogram("bat", MustMakeLinearValueBuckets(1, 1, 3)).RecordValue(1.1) 496 r.hg.Add(1) 497 s.SubScope("test").Histogram("bat", MustMakeLinearValueBuckets(1, 1, 3)).RecordValue(2.1) 498 499 buckets := MustMakeLinearValueBuckets(100, 10, 3) 500 r.hg.Add(1) 501 s.SubScope("test").Histogram("qux", buckets).RecordValue(135.0) 502 r.hg.Add(1) 503 s.SubScope("test").Histogram("quux", buckets).RecordValue(101.0) 504 r.hg.Add(1) 505 s.SubScope("test2").Histogram("quux", buckets).RecordValue(101.0) 506 507 s.reportLoopRun() 508 509 r.WaitAll() 510 511 var ( 512 counters = r.getCounters() 513 gauges = r.getGauges() 514 timers = r.getTimers() 515 histograms = r.getHistograms() 516 ) 517 518 assert.EqualValues(t, 1, counters["bar"].val) 519 assert.EqualValues(t, 1, gauges["zed"].val) 520 assert.EqualValues(t, time.Millisecond*175, timers["ticky"].val) 521 assert.EqualValues(t, 1, histograms["baz"].valueSamples[50.0]) 522 assert.EqualValues(t, 1, histograms["bat"].valueSamples[3.0]) 523 assert.EqualValues(t, 1, histograms["test.bat"].valueSamples[2.0]) 524 assert.EqualValues(t, 1, histograms["test.bat"].valueSamples[3.0]) 525 assert.EqualValues(t, 1, histograms["test.qux"].valueSamples[math.MaxFloat64]) 526 assert.EqualValues(t, 1, histograms["test.quux"].valueSamples[110.0]) 527 assert.EqualValues(t, 1, histograms["test2.quux"].valueSamples[110.0]) 528 529 r = newTestStatsReporter() 530 s.reportLoopRun() 531 532 counters = r.getCounters() 533 gauges = r.getGauges() 534 timers = r.getTimers() 535 histograms = r.getHistograms() 536 537 assert.Nil(t, counters["bar"]) 538 assert.Nil(t, gauges["zed"]) 539 assert.Nil(t, timers["ticky"]) 540 assert.Nil(t, histograms["baz"]) 541 assert.Nil(t, histograms["bat"]) 542 assert.Nil(t, histograms["test.qux"]) 543 } 544 545 func TestHistogramSharedBucketMetrics(t *testing.T) { 546 var ( 547 r = newTestStatsReporter() 548 scope = newRootScope(ScopeOptions{ 549 Prefix: "", 550 Tags: nil, 551 CachedReporter: r, 552 OmitCardinalityMetrics: true, 553 }, 0) 554 builder = func(s Scope) func(map[string]string) { 555 buckets := MustMakeLinearValueBuckets(10, 10, 3) 556 return func(tags map[string]string) { 557 s.Tagged(tags).Histogram("hist", buckets).RecordValue(19.0) 558 } 559 } 560 ) 561 562 var ( 563 wg = &sync.WaitGroup{} 564 record = builder(scope) 565 ) 566 567 r.hg.Add(4) 568 for i := 0; i < 10000; i++ { 569 i := i 570 wg.Add(1) 571 go func() { 572 defer wg.Done() 573 574 val := strconv.Itoa(i % 4) 575 record( 576 map[string]string{ 577 "key": val, 578 }, 579 ) 580 581 time.Sleep(time.Duration(rand.Float64() * float64(time.Second))) 582 }() 583 } 584 585 wg.Wait() 586 scope.reportRegistry() 587 r.WaitAll() 588 589 unseen := map[string]struct{}{ 590 "0": {}, 591 "1": {}, 592 "2": {}, 593 "3": {}, 594 } 595 596 require.Equal(t, len(unseen), len(r.histograms)) 597 598 for name, value := range r.histograms { 599 if !strings.HasPrefix(name, "hist+") { 600 continue 601 } 602 603 count, ok := value.valueSamples[20.0] 604 require.True(t, ok) 605 require.Equal(t, 2500, count) 606 607 delete(unseen, value.tags["key"]) 608 } 609 610 require.Equal(t, 0, len(unseen), fmt.Sprintf("%v", unseen)) 611 } 612 613 func TestConcurrentUpdates(t *testing.T) { 614 var ( 615 r = newTestStatsReporter() 616 wg = &sync.WaitGroup{} 617 workerCount = 20 618 scopeCount = 4 619 countersPerScope = 4 620 counterIncrs = 5000 621 rs = newRootScope( 622 ScopeOptions{ 623 Prefix: "", 624 Tags: nil, 625 CachedReporter: r, 626 OmitCardinalityMetrics: true, 627 }, 0, 628 ) 629 scopes = []Scope{rs} 630 counters []Counter 631 ) 632 633 // Instantiate Subscopes. 634 for i := 1; i < scopeCount; i++ { 635 scopes = append(scopes, rs.SubScope(fmt.Sprintf("subscope_%d", i))) 636 } 637 638 // Instantiate Counters. 639 for sNum, s := range scopes { 640 for cNum := 0; cNum < countersPerScope; cNum++ { 641 counters = append(counters, s.Counter(fmt.Sprintf("scope_%d_counter_%d", sNum, cNum))) 642 } 643 } 644 645 // Instantiate workers. 646 r.cg.Add(scopeCount * countersPerScope) 647 for worker := 0; worker < workerCount; worker++ { 648 wg.Add(1) 649 go func() { 650 defer wg.Done() 651 // Counter should have counterIncrs * workerCount. 652 for i := 0; i < counterIncrs*len(counters); i++ { 653 counters[i%len(counters)].Inc(1) 654 } 655 }() 656 } 657 658 wg.Wait() 659 rs.reportRegistry() 660 r.WaitAll() 661 662 wantVal := int64(workerCount * counterIncrs) 663 for _, gotCounter := range r.getCounters() { 664 assert.Equal(t, gotCounter.val, wantVal) 665 } 666 } 667 668 func TestCounterSanitized(t *testing.T) { 669 r := newTestStatsReporter() 670 671 root, closer := NewRootScope(ScopeOptions{ 672 Reporter: r, 673 SanitizeOptions: &alphanumericSanitizerOpts, 674 OmitCardinalityMetrics: true, 675 }, 0) 676 defer closer.Close() 677 678 s := root.(*scope) 679 680 r.cg.Add(1) 681 s.Counter("how?").Inc(1) 682 r.gg.Add(1) 683 s.Gauge("does!").Update(1) 684 r.tg.Add(1) 685 s.Timer("this!").Record(time.Millisecond * 175) 686 r.hg.Add(1) 687 s.Histogram("work1!?", MustMakeLinearValueBuckets(0, 10, 10)). 688 RecordValue(42.42) 689 690 s.report(r) 691 r.WaitAll() 692 693 var ( 694 counters = r.getCounters() 695 gauges = r.getGauges() 696 timers = r.getTimers() 697 histograms = r.getHistograms() 698 ) 699 700 assert.Nil(t, counters["how?"]) 701 assert.EqualValues(t, 1, counters["how_"].val) 702 assert.Nil(t, gauges["does!"]) 703 assert.EqualValues(t, 1, gauges["does_"].val) 704 assert.Nil(t, timers["this!"]) 705 assert.EqualValues(t, time.Millisecond*175, timers["this_"].val) 706 assert.Nil(t, histograms["work1!?"]) 707 assert.EqualValues(t, 1, histograms["work1__"].valueSamples[50.0]) 708 709 r = newTestStatsReporter() 710 s.report(r) 711 712 counters = r.getCounters() 713 gauges = r.getGauges() 714 timers = r.getTimers() 715 histograms = r.getHistograms() 716 717 assert.Nil(t, counters["how?"]) 718 assert.Nil(t, counters["how_"]) 719 assert.Nil(t, gauges["does!"]) 720 assert.Nil(t, gauges["does_"]) 721 assert.Nil(t, timers["this!"]) 722 assert.Nil(t, timers["this_"]) 723 assert.Nil(t, histograms["work1!?"]) 724 assert.Nil(t, histograms["work1__"]) 725 } 726 727 func TestCachedReporter(t *testing.T) { 728 r := newTestStatsReporter() 729 730 root, closer := NewRootScope(ScopeOptions{CachedReporter: r, OmitCardinalityMetrics: true}, 0) 731 defer closer.Close() 732 733 s := root.(*scope) 734 735 r.cg.Add(1) 736 s.Counter("bar").Inc(1) 737 r.gg.Add(1) 738 s.Gauge("zed").Update(1) 739 r.tg.Add(1) 740 s.Timer("ticky").Record(time.Millisecond * 175) 741 r.hg.Add(2) 742 s.Histogram("baz", MustMakeLinearValueBuckets(0, 10, 10)). 743 RecordValue(42.42) 744 s.Histogram("qux", MustMakeLinearDurationBuckets(0, 10*time.Millisecond, 10)). 745 RecordDuration(42 * time.Millisecond) 746 747 s.cachedReport() 748 r.WaitAll() 749 750 var ( 751 counters = r.getCounters() 752 gauges = r.getGauges() 753 timers = r.getTimers() 754 histograms = r.getHistograms() 755 ) 756 757 assert.EqualValues(t, 1, counters["bar"].val) 758 assert.EqualValues(t, 1, gauges["zed"].val) 759 assert.EqualValues(t, time.Millisecond*175, timers["ticky"].val) 760 assert.EqualValues(t, 1, histograms["baz"].valueSamples[50.0]) 761 assert.EqualValues(t, 1, histograms["qux"].durationSamples[50*time.Millisecond]) 762 } 763 764 func TestRootScopeWithoutPrefix(t *testing.T) { 765 r := newTestStatsReporter() 766 767 root, closer := NewRootScope(ScopeOptions{Reporter: r, OmitCardinalityMetrics: true}, 0) 768 defer closer.Close() 769 770 s := root.(*scope) 771 r.cg.Add(1) 772 s.Counter("bar").Inc(1) 773 s.Counter("bar").Inc(20) 774 r.gg.Add(1) 775 s.Gauge("zed").Update(1) 776 r.tg.Add(1) 777 s.Timer("blork").Record(time.Millisecond * 175) 778 r.hg.Add(1) 779 s.Histogram("baz", MustMakeLinearValueBuckets(0, 10, 10)). 780 RecordValue(42.42) 781 782 s.report(r) 783 r.WaitAll() 784 785 var ( 786 counters = r.getCounters() 787 gauges = r.getGauges() 788 timers = r.getTimers() 789 histograms = r.getHistograms() 790 ) 791 792 assert.EqualValues(t, 21, counters["bar"].val) 793 assert.EqualValues(t, 1, gauges["zed"].val) 794 assert.EqualValues(t, time.Millisecond*175, timers["blork"].val) 795 assert.EqualValues(t, 1, histograms["baz"].valueSamples[50.0]) 796 } 797 798 func TestRootScopeWithPrefix(t *testing.T) { 799 r := newTestStatsReporter() 800 801 root, closer := NewRootScope( 802 ScopeOptions{Prefix: "foo", Reporter: r, OmitCardinalityMetrics: true}, 0, 803 ) 804 defer closer.Close() 805 806 s := root.(*scope) 807 r.cg.Add(1) 808 s.Counter("bar").Inc(1) 809 s.Counter("bar").Inc(20) 810 r.gg.Add(1) 811 s.Gauge("zed").Update(1) 812 r.tg.Add(1) 813 s.Timer("blork").Record(time.Millisecond * 175) 814 r.hg.Add(1) 815 s.Histogram("baz", MustMakeLinearValueBuckets(0, 10, 10)). 816 RecordValue(42.42) 817 818 s.report(r) 819 r.WaitAll() 820 821 var ( 822 counters = r.getCounters() 823 gauges = r.getGauges() 824 timers = r.getTimers() 825 histograms = r.getHistograms() 826 ) 827 828 assert.EqualValues(t, 21, counters["foo.bar"].val) 829 assert.EqualValues(t, 1, gauges["foo.zed"].val) 830 assert.EqualValues(t, time.Millisecond*175, timers["foo.blork"].val) 831 assert.EqualValues(t, 1, histograms["foo.baz"].valueSamples[50.0]) 832 } 833 834 func TestRootScopeWithDifferentSeparator(t *testing.T) { 835 r := newTestStatsReporter() 836 837 root, closer := NewRootScope( 838 ScopeOptions{ 839 Prefix: "foo", Separator: "_", Reporter: r, OmitCardinalityMetrics: true, 840 }, 0, 841 ) 842 defer closer.Close() 843 844 s := root.(*scope) 845 r.cg.Add(1) 846 s.Counter("bar").Inc(1) 847 s.Counter("bar").Inc(20) 848 r.gg.Add(1) 849 s.Gauge("zed").Update(1) 850 r.tg.Add(1) 851 s.Timer("blork").Record(time.Millisecond * 175) 852 r.hg.Add(1) 853 s.Histogram("baz", MustMakeLinearValueBuckets(0, 10, 10)). 854 RecordValue(42.42) 855 856 s.report(r) 857 r.WaitAll() 858 859 var ( 860 counters = r.getCounters() 861 gauges = r.getGauges() 862 timers = r.getTimers() 863 histograms = r.getHistograms() 864 ) 865 866 assert.EqualValues(t, 21, counters["foo_bar"].val) 867 assert.EqualValues(t, 1, gauges["foo_zed"].val) 868 assert.EqualValues(t, time.Millisecond*175, timers["foo_blork"].val) 869 assert.EqualValues(t, 1, histograms["foo_baz"].valueSamples[50.0]) 870 } 871 872 func TestSubScope(t *testing.T) { 873 r := newTestStatsReporter() 874 875 root, closer := NewRootScope( 876 ScopeOptions{Prefix: "foo", Reporter: r, OmitCardinalityMetrics: true}, 0, 877 ) 878 defer closer.Close() 879 880 tags := map[string]string{"foo": "bar"} 881 s := root.Tagged(tags).SubScope("mork").(*scope) 882 r.cg.Add(1) 883 s.Counter("bar").Inc(1) 884 s.Counter("bar").Inc(20) 885 r.gg.Add(1) 886 s.Gauge("zed").Update(1) 887 r.tg.Add(1) 888 s.Timer("blork").Record(time.Millisecond * 175) 889 r.hg.Add(1) 890 s.Histogram("baz", MustMakeLinearValueBuckets(0, 10, 10)). 891 RecordValue(42.42) 892 893 s.report(r) 894 r.WaitAll() 895 896 var ( 897 counters = r.getCounters() 898 gauges = r.getGauges() 899 timers = r.getTimers() 900 histograms = r.getHistograms() 901 ) 902 903 // Assert prefixed correctly 904 assert.EqualValues(t, 21, counters["foo.mork.bar"].val) 905 assert.EqualValues(t, 1, gauges["foo.mork.zed"].val) 906 assert.EqualValues(t, time.Millisecond*175, timers["foo.mork.blork"].val) 907 assert.EqualValues(t, 1, histograms["foo.mork.baz"].valueSamples[50.0]) 908 909 // Assert tags inherited 910 assert.Equal(t, tags, counters["foo.mork.bar"].tags) 911 assert.Equal(t, tags, gauges["foo.mork.zed"].tags) 912 assert.Equal(t, tags, timers["foo.mork.blork"].tags) 913 assert.Equal(t, tags, histograms["foo.mork.baz"].tags) 914 } 915 916 func TestSubScopeClose(t *testing.T) { 917 r := newTestStatsReporter() 918 919 rs, closer := NewRootScope(ScopeOptions{Prefix: "foo", Reporter: r, OmitCardinalityMetrics: true}, 0) 920 // defer closer.Close() 921 _ = closer 922 923 var ( 924 root = rs.(*scope) 925 s = root.SubScope("mork").(*scope) 926 rootCounter = root.Counter("foo") 927 subCounter = s.Counter("foo") 928 ) 929 930 // Emit a metric from both scopes. 931 r.cg.Add(1) 932 rootCounter.Inc(1) 933 r.cg.Add(1) 934 subCounter.Inc(1) 935 936 // Verify that we got both metrics. 937 root.reportRegistry() 938 r.WaitAll() 939 counters := r.getCounters() 940 require.EqualValues(t, 1, counters["foo.foo"].val) 941 require.EqualValues(t, 1, counters["foo.mork.foo"].val) 942 943 // Close the subscope. We expect both metrics to still be reported, because 944 // we won't have reported the registry before we update the metrics. 945 require.NoError(t, s.Close()) 946 947 // Create a subscope from the now-closed scope; it should nop. 948 ns := s.SubScope("foobar") 949 require.Equal(t, NoopScope, ns) 950 951 // Emit a metric from all scopes. 952 r.cg.Add(1) 953 rootCounter.Inc(2) 954 r.cg.Add(1) 955 subCounter.Inc(2) 956 957 // Verify that we still got both metrics. 958 root.reportLoopRun() 959 r.WaitAll() 960 counters = r.getCounters() 961 require.EqualValues(t, 2, counters["foo.foo"].val) 962 require.EqualValues(t, 2, counters["foo.mork.foo"].val) 963 964 // Emit a metric for both scopes. The root counter should succeed, and the 965 // subscope counter should not update what's in the reporter. 966 r.cg.Add(1) 967 rootCounter.Inc(3) 968 subCounter.Inc(3) 969 root.reportLoopRun() 970 r.WaitAll() 971 time.Sleep(time.Second) // since we can't wg.Add the non-reported counter 972 973 // We only expect the root scope counter; the subscope counter will be the 974 // value previously held by the reporter, because it has not been udpated. 975 counters = r.getCounters() 976 require.EqualValues(t, 3, counters["foo.foo"].val) 977 require.EqualValues(t, 2, counters["foo.mork.foo"].val) 978 979 // Ensure that we can double-close harmlessly. 980 require.NoError(t, s.Close()) 981 982 // Create one more scope so that we can ensure it's defunct once the root is 983 // closed. 984 ns = root.SubScope("newscope") 985 986 // Close the root scope. We should not be able to emit any more metrics, 987 // because the root scope reports the registry prior to closing. 988 require.NoError(t, closer.Close()) 989 990 ns.Counter("newcounter").Inc(1) 991 rootCounter.Inc(4) 992 root.registry.Report(r) 993 time.Sleep(time.Second) // since we can't wg.Add the non-reported counter 994 995 // We do not expect any updates. 996 counters = r.getCounters() 997 require.EqualValues(t, 3, counters["foo.foo"].val) 998 require.EqualValues(t, 2, counters["foo.mork.foo"].val) 999 _, found := counters["newscope.newcounter"] 1000 require.False(t, found) 1001 1002 // Ensure that we can double-close harmlessly. 1003 require.NoError(t, closer.Close()) 1004 } 1005 1006 func TestTaggedSubScope(t *testing.T) { 1007 r := newTestStatsReporter() 1008 1009 ts := map[string]string{"env": "test"} 1010 root, closer := NewRootScope( 1011 ScopeOptions{ 1012 Prefix: "foo", Tags: ts, Reporter: r, OmitCardinalityMetrics: true, 1013 }, 0, 1014 ) 1015 defer closer.Close() 1016 1017 s := root.(*scope) 1018 1019 tscope := root.Tagged(map[string]string{"service": "test"}).(*scope) 1020 scope := root 1021 1022 r.cg.Add(1) 1023 scope.Counter("beep").Inc(1) 1024 r.cg.Add(1) 1025 tscope.Counter("boop").Inc(1) 1026 r.hg.Add(1) 1027 scope.Histogram("baz", MustMakeLinearValueBuckets(0, 10, 10)). 1028 RecordValue(42.42) 1029 r.hg.Add(1) 1030 tscope.Histogram("bar", MustMakeLinearValueBuckets(0, 10, 10)). 1031 RecordValue(42.42) 1032 1033 s.report(r) 1034 tscope.report(r) 1035 r.cg.Wait() 1036 1037 var ( 1038 counters = r.getCounters() 1039 histograms = r.getHistograms() 1040 ) 1041 1042 assert.EqualValues(t, 1, counters["foo.beep"].val) 1043 assert.EqualValues(t, ts, counters["foo.beep"].tags) 1044 1045 assert.EqualValues(t, 1, counters["foo.boop"].val) 1046 assert.EqualValues( 1047 t, map[string]string{ 1048 "env": "test", 1049 "service": "test", 1050 }, counters["foo.boop"].tags, 1051 ) 1052 1053 assert.EqualValues(t, 1, histograms["foo.baz"].valueSamples[50.0]) 1054 assert.EqualValues(t, ts, histograms["foo.baz"].tags) 1055 1056 assert.EqualValues(t, 1, histograms["foo.bar"].valueSamples[50.0]) 1057 assert.EqualValues( 1058 t, map[string]string{ 1059 "env": "test", 1060 "service": "test", 1061 }, histograms["foo.bar"].tags, 1062 ) 1063 } 1064 1065 func TestTaggedSanitizedSubScope(t *testing.T) { 1066 r := newTestStatsReporter() 1067 1068 ts := map[string]string{"env": "test:env"} 1069 root, closer := NewRootScope(ScopeOptions{ 1070 Prefix: "foo", 1071 Tags: ts, 1072 Reporter: r, 1073 SanitizeOptions: &alphanumericSanitizerOpts, 1074 OmitCardinalityMetrics: true, 1075 }, 0) 1076 defer closer.Close() 1077 s := root.(*scope) 1078 1079 tscope := root.Tagged(map[string]string{"service": "test.service"}).(*scope) 1080 1081 r.cg.Add(1) 1082 tscope.Counter("beep").Inc(1) 1083 1084 s.report(r) 1085 tscope.report(r) 1086 r.cg.Wait() 1087 1088 counters := r.getCounters() 1089 assert.EqualValues(t, 1, counters["foo_beep"].val) 1090 assert.EqualValues( 1091 t, map[string]string{ 1092 "env": "test_env", 1093 "service": "test_service", 1094 }, counters["foo_beep"].tags, 1095 ) 1096 } 1097 1098 func TestTaggedExistingReturnsSameScope(t *testing.T) { 1099 r := newTestStatsReporter() 1100 1101 for _, initialTags := range []map[string]string{ 1102 nil, 1103 {"env": "test"}, 1104 } { 1105 root, closer := NewRootScope( 1106 ScopeOptions{ 1107 Prefix: "foo", Tags: initialTags, Reporter: r, OmitCardinalityMetrics: true, 1108 }, 0, 1109 ) 1110 defer closer.Close() 1111 1112 rootScope := root.(*scope) 1113 fooScope := root.Tagged(map[string]string{"foo": "bar"}).(*scope) 1114 1115 assert.NotEqual(t, rootScope, fooScope) 1116 assert.Equal(t, fooScope, fooScope.Tagged(nil)) 1117 1118 fooBarScope := fooScope.Tagged(map[string]string{"bar": "baz"}).(*scope) 1119 1120 assert.NotEqual(t, fooScope, fooBarScope) 1121 assert.Equal(t, fooBarScope, fooScope.Tagged(map[string]string{"bar": "baz"}).(*scope)) 1122 } 1123 } 1124 1125 func TestSnapshot(t *testing.T) { 1126 commonTags := map[string]string{"env": "test"} 1127 s := NewTestScope("foo", map[string]string{"env": "test"}) 1128 child := s.Tagged(map[string]string{"service": "test"}) 1129 s.Counter("beep").Inc(1) 1130 s.Gauge("bzzt").Update(2) 1131 s.Timer("brrr").Record(1 * time.Second) 1132 s.Timer("brrr").Record(2 * time.Second) 1133 s.Histogram("fizz", ValueBuckets{0, 2, 4}).RecordValue(1) 1134 s.Histogram("fizz", ValueBuckets{0, 2, 4}).RecordValue(5) 1135 s.Histogram("buzz", DurationBuckets{time.Second * 2, time.Second * 4}).RecordDuration(time.Second) 1136 child.Counter("boop").Inc(1) 1137 1138 // Should be able to call Snapshot any number of times and get same result. 1139 for i := 0; i < 3; i++ { 1140 t.Run( 1141 fmt.Sprintf("attempt %d", i), func(t *testing.T) { 1142 snap := s.Snapshot() 1143 counters, gauges, timers, histograms := 1144 snap.Counters(), snap.Gauges(), snap.Timers(), snap.Histograms() 1145 1146 assert.EqualValues(t, 1, counters["foo.beep+env=test"].Value()) 1147 assert.EqualValues(t, commonTags, counters["foo.beep+env=test"].Tags()) 1148 1149 assert.EqualValues(t, 2, gauges["foo.bzzt+env=test"].Value()) 1150 assert.EqualValues(t, commonTags, gauges["foo.bzzt+env=test"].Tags()) 1151 1152 assert.EqualValues( 1153 t, []time.Duration{ 1154 1 * time.Second, 1155 2 * time.Second, 1156 }, timers["foo.brrr+env=test"].Values(), 1157 ) 1158 assert.EqualValues(t, commonTags, timers["foo.brrr+env=test"].Tags()) 1159 1160 assert.EqualValues( 1161 t, map[float64]int64{ 1162 0: 0, 1163 2: 1, 1164 4: 0, 1165 math.MaxFloat64: 1, 1166 }, histograms["foo.fizz+env=test"].Values(), 1167 ) 1168 assert.EqualValues(t, map[time.Duration]int64(nil), histograms["foo.fizz+env=test"].Durations()) 1169 assert.EqualValues(t, commonTags, histograms["foo.fizz+env=test"].Tags()) 1170 1171 assert.EqualValues(t, map[float64]int64(nil), histograms["foo.buzz+env=test"].Values()) 1172 assert.EqualValues( 1173 t, map[time.Duration]int64{ 1174 time.Second * 2: 1, 1175 time.Second * 4: 0, 1176 math.MaxInt64: 0, 1177 }, histograms["foo.buzz+env=test"].Durations(), 1178 ) 1179 assert.EqualValues(t, commonTags, histograms["foo.buzz+env=test"].Tags()) 1180 1181 assert.EqualValues(t, 1, counters["foo.boop+env=test,service=test"].Value()) 1182 assert.EqualValues( 1183 t, map[string]string{ 1184 "env": "test", 1185 "service": "test", 1186 }, counters["foo.boop+env=test,service=test"].Tags(), 1187 ) 1188 }, 1189 ) 1190 } 1191 } 1192 1193 func TestSnapshotConcurrent(t *testing.T) { 1194 var ( 1195 scope = NewTestScope("", nil) 1196 quit = make(chan struct{}) 1197 done = make(chan struct{}) 1198 ) 1199 1200 go func() { 1201 defer close(done) 1202 for { 1203 select { 1204 case <-quit: 1205 return 1206 default: 1207 hello := scope.Tagged(map[string]string{"a": "b"}).Counter("hello") 1208 hello.Inc(1) 1209 } 1210 } 1211 }() 1212 var val CounterSnapshot 1213 for { 1214 val = scope.Snapshot().Counters()["hello+a=b"] 1215 if val != nil { 1216 quit <- struct{}{} 1217 break 1218 } 1219 } 1220 require.NotNil(t, val) 1221 1222 <-done 1223 } 1224 1225 func TestCapabilities(t *testing.T) { 1226 r := newTestStatsReporter() 1227 s, closer := NewRootScope(ScopeOptions{Reporter: r, OmitCardinalityMetrics: true}, 0) 1228 defer closer.Close() 1229 assert.True(t, s.Capabilities().Reporting()) 1230 assert.False(t, s.Capabilities().Tagging()) 1231 } 1232 1233 func TestCapabilitiesNoReporter(t *testing.T) { 1234 s, closer := NewRootScope(ScopeOptions{}, 0) 1235 defer closer.Close() 1236 assert.False(t, s.Capabilities().Reporting()) 1237 assert.False(t, s.Capabilities().Tagging()) 1238 } 1239 1240 func TestNilTagMerge(t *testing.T) { 1241 assert.Nil(t, nil, mergeRightTags(nil, nil)) 1242 } 1243 1244 func TestScopeDefaultBuckets(t *testing.T) { 1245 r := newTestStatsReporter() 1246 1247 root, closer := NewRootScope(ScopeOptions{ 1248 DefaultBuckets: DurationBuckets{ 1249 0 * time.Millisecond, 1250 30 * time.Millisecond, 1251 60 * time.Millisecond, 1252 90 * time.Millisecond, 1253 120 * time.Millisecond, 1254 }, 1255 Reporter: r, 1256 OmitCardinalityMetrics: true, 1257 }, 0) 1258 defer closer.Close() 1259 1260 s := root.(*scope) 1261 r.hg.Add(2) 1262 s.Histogram("baz", DefaultBuckets).RecordDuration(42 * time.Millisecond) 1263 s.Histogram("baz", DefaultBuckets).RecordDuration(84 * time.Millisecond) 1264 s.Histogram("baz", DefaultBuckets).RecordDuration(84 * time.Millisecond) 1265 1266 s.report(r) 1267 r.WaitAll() 1268 1269 histograms := r.getHistograms() 1270 assert.EqualValues(t, 1, histograms["baz"].durationSamples[60*time.Millisecond]) 1271 assert.EqualValues(t, 2, histograms["baz"].durationSamples[90*time.Millisecond]) 1272 } 1273 1274 type testMets struct { 1275 c Counter 1276 } 1277 1278 func newTestMets(scope Scope) testMets { 1279 return testMets{ 1280 c: scope.Counter("honk"), 1281 } 1282 } 1283 1284 func TestReturnByValue(t *testing.T) { 1285 r := newTestStatsReporter() 1286 1287 root, closer := NewRootScope(ScopeOptions{Reporter: r, OmitCardinalityMetrics: true}, 0) 1288 defer closer.Close() 1289 1290 s := root.(*scope) 1291 mets := newTestMets(s) 1292 1293 r.cg.Add(1) 1294 mets.c.Inc(3) 1295 s.report(r) 1296 r.cg.Wait() 1297 1298 counters := r.getCounters() 1299 assert.EqualValues(t, 3, counters["honk"].val) 1300 } 1301 1302 func TestScopeAvoidReportLoopRunOnClose(t *testing.T) { 1303 r := newTestStatsReporter() 1304 root, closer := NewRootScope(ScopeOptions{Reporter: r, OmitCardinalityMetrics: true}, 0) 1305 1306 s := root.(*scope) 1307 s.reportLoopRun() 1308 1309 assert.Equal(t, int32(1), atomic.LoadInt32(&r.flushes)) 1310 1311 assert.NoError(t, closer.Close()) 1312 1313 s.reportLoopRun() 1314 assert.Equal(t, int32(2), atomic.LoadInt32(&r.flushes)) 1315 } 1316 1317 func TestScopeFlushOnClose(t *testing.T) { 1318 r := newTestStatsReporter() 1319 root, closer := NewRootScope(ScopeOptions{Reporter: r, OmitCardinalityMetrics: true}, time.Hour) 1320 1321 r.cg.Add(1) 1322 root.Counter("foo").Inc(1) 1323 1324 counters := r.getCounters() 1325 assert.Nil(t, counters["foo"]) 1326 assert.NoError(t, closer.Close()) 1327 1328 counters = r.getCounters() 1329 assert.EqualValues(t, 1, counters["foo"].val) 1330 assert.NoError(t, closer.Close()) 1331 }