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