github.com/uber-go/tally/v4@v4.1.17/scope.go (about) 1 // Copyright (c) 2024 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 "io" 25 "sync" 26 "time" 27 28 "go.uber.org/atomic" 29 ) 30 31 const ( 32 _defaultInitialSliceSize = 16 33 _defaultReportingInterval = 2 * time.Second 34 ) 35 36 var ( 37 // NoopScope is a scope that does nothing 38 NoopScope, _ = NewRootScope(ScopeOptions{Reporter: NullStatsReporter}, 0) 39 // DefaultSeparator is the default separator used to join nested scopes 40 DefaultSeparator = "." 41 42 globalNow = time.Now 43 44 defaultScopeBuckets = DurationBuckets{ 45 0 * time.Millisecond, 46 10 * time.Millisecond, 47 25 * time.Millisecond, 48 50 * time.Millisecond, 49 75 * time.Millisecond, 50 100 * time.Millisecond, 51 200 * time.Millisecond, 52 300 * time.Millisecond, 53 400 * time.Millisecond, 54 500 * time.Millisecond, 55 600 * time.Millisecond, 56 800 * time.Millisecond, 57 1 * time.Second, 58 2 * time.Second, 59 5 * time.Second, 60 } 61 ) 62 63 type scope struct { 64 separator string 65 prefix string 66 tags map[string]string 67 reporter StatsReporter 68 cachedReporter CachedStatsReporter 69 baseReporter BaseStatsReporter 70 defaultBuckets Buckets 71 sanitizer Sanitizer 72 73 registry *scopeRegistry 74 75 cm sync.RWMutex 76 gm sync.RWMutex 77 tm sync.RWMutex 78 hm sync.RWMutex 79 80 counters map[string]*counter 81 countersSlice []*counter 82 gauges map[string]*gauge 83 gaugesSlice []*gauge 84 histograms map[string]*histogram 85 histogramsSlice []*histogram 86 timers map[string]*timer 87 // nb: deliberately skipping timersSlice as we report timers immediately, 88 // no buffering is involved. 89 90 bucketCache *bucketCache 91 closed atomic.Bool 92 done chan struct{} 93 wg sync.WaitGroup 94 root bool 95 testScope bool 96 } 97 98 // ScopeOptions is a set of options to construct a scope. 99 type ScopeOptions struct { 100 Tags map[string]string 101 Prefix string 102 Reporter StatsReporter 103 CachedReporter CachedStatsReporter 104 Separator string 105 DefaultBuckets Buckets 106 SanitizeOptions *SanitizeOptions 107 OmitCardinalityMetrics bool 108 CardinalityMetricsTags map[string]string 109 110 testScope bool 111 registryShardCount uint 112 } 113 114 // NewRootScope creates a new root Scope with a set of options and 115 // a reporting interval. 116 // Must provide either a StatsReporter or a CachedStatsReporter. 117 func NewRootScope(opts ScopeOptions, interval time.Duration) (Scope, io.Closer) { 118 s := newRootScope(opts, interval) 119 return s, s 120 } 121 122 // NewRootScopeWithDefaultInterval invokes NewRootScope with the default 123 // reporting interval of 2s. 124 func NewRootScopeWithDefaultInterval(opts ScopeOptions) (Scope, io.Closer) { 125 return NewRootScope(opts, _defaultReportingInterval) 126 } 127 128 // NewTestScope creates a new Scope without a stats reporter with the 129 // given prefix and adds the ability to take snapshots of metrics emitted 130 // to it. 131 func NewTestScope( 132 prefix string, 133 tags map[string]string, 134 ) TestScope { 135 return newRootScope(ScopeOptions{ 136 Prefix: prefix, 137 Tags: tags, 138 testScope: true, 139 }, 0) 140 } 141 142 func newRootScope(opts ScopeOptions, interval time.Duration) *scope { 143 sanitizer := NewNoOpSanitizer() 144 if o := opts.SanitizeOptions; o != nil { 145 sanitizer = NewSanitizer(*o) 146 } 147 148 if opts.Tags == nil { 149 opts.Tags = make(map[string]string) 150 } 151 if opts.Separator == "" { 152 opts.Separator = DefaultSeparator 153 } 154 155 var baseReporter BaseStatsReporter 156 if opts.Reporter != nil { 157 baseReporter = opts.Reporter 158 } else if opts.CachedReporter != nil { 159 baseReporter = opts.CachedReporter 160 } 161 162 if opts.DefaultBuckets == nil || opts.DefaultBuckets.Len() < 1 { 163 opts.DefaultBuckets = defaultScopeBuckets 164 } 165 166 s := &scope{ 167 baseReporter: baseReporter, 168 bucketCache: newBucketCache(), 169 cachedReporter: opts.CachedReporter, 170 counters: make(map[string]*counter), 171 countersSlice: make([]*counter, 0, _defaultInitialSliceSize), 172 defaultBuckets: opts.DefaultBuckets, 173 done: make(chan struct{}), 174 gauges: make(map[string]*gauge), 175 gaugesSlice: make([]*gauge, 0, _defaultInitialSliceSize), 176 histograms: make(map[string]*histogram), 177 histogramsSlice: make([]*histogram, 0, _defaultInitialSliceSize), 178 prefix: sanitizer.Name(opts.Prefix), 179 reporter: opts.Reporter, 180 sanitizer: sanitizer, 181 separator: sanitizer.Name(opts.Separator), 182 timers: make(map[string]*timer), 183 root: true, 184 testScope: opts.testScope, 185 } 186 187 // NB(r): Take a copy of the tags on creation 188 // so that it cannot be modified after set. 189 s.tags = s.copyAndSanitizeMap(opts.Tags) 190 191 // Register the root scope 192 s.registry = newScopeRegistryWithShardCount(s, opts.registryShardCount, opts.OmitCardinalityMetrics, opts.CardinalityMetricsTags) 193 194 if interval > 0 { 195 s.wg.Add(1) 196 go func() { 197 defer s.wg.Done() 198 s.reportLoop(interval) 199 }() 200 } 201 202 return s 203 } 204 205 // report dumps all aggregated stats into the reporter. Should be called automatically by the root scope periodically. 206 func (s *scope) report(r StatsReporter) { 207 s.cm.RLock() 208 for name, counter := range s.counters { 209 counter.report(s.fullyQualifiedName(name), s.tags, r) 210 } 211 s.cm.RUnlock() 212 213 s.gm.RLock() 214 for name, gauge := range s.gauges { 215 gauge.report(s.fullyQualifiedName(name), s.tags, r) 216 } 217 s.gm.RUnlock() 218 219 // we do nothing for timers here because timers report directly to ths StatsReporter without buffering 220 221 s.hm.RLock() 222 for name, histogram := range s.histograms { 223 histogram.report(s.fullyQualifiedName(name), s.tags, r) 224 } 225 s.hm.RUnlock() 226 } 227 228 func (s *scope) cachedReport() { 229 s.cm.RLock() 230 for _, counter := range s.countersSlice { 231 counter.cachedReport() 232 } 233 s.cm.RUnlock() 234 235 s.gm.RLock() 236 for _, gauge := range s.gaugesSlice { 237 gauge.cachedReport() 238 } 239 s.gm.RUnlock() 240 241 // we do nothing for timers here because timers report directly to ths StatsReporter without buffering 242 243 s.hm.RLock() 244 for _, histogram := range s.histogramsSlice { 245 histogram.cachedReport() 246 } 247 s.hm.RUnlock() 248 } 249 250 // reportLoop is used by the root scope for periodic reporting 251 func (s *scope) reportLoop(interval time.Duration) { 252 ticker := time.NewTicker(interval) 253 defer ticker.Stop() 254 255 for { 256 select { 257 case <-ticker.C: 258 s.reportLoopRun() 259 case <-s.done: 260 return 261 } 262 } 263 } 264 265 func (s *scope) reportLoopRun() { 266 if s.closed.Load() { 267 return 268 } 269 270 s.reportRegistry() 271 } 272 273 func (s *scope) reportRegistry() { 274 if s.reporter != nil { 275 s.registry.Report(s.reporter) 276 s.reporter.Flush() 277 } else if s.cachedReporter != nil { 278 s.registry.CachedReport() 279 s.cachedReporter.Flush() 280 } 281 } 282 283 func (s *scope) Counter(name string) Counter { 284 name = s.sanitizer.Name(name) 285 if c, ok := s.counter(name); ok { 286 return c 287 } 288 289 s.cm.Lock() 290 defer s.cm.Unlock() 291 292 if c, ok := s.counters[name]; ok { 293 return c 294 } 295 296 var cachedCounter CachedCount 297 if s.cachedReporter != nil { 298 cachedCounter = s.cachedReporter.AllocateCounter( 299 s.fullyQualifiedName(name), 300 s.tags, 301 ) 302 } 303 304 c := newCounter(cachedCounter) 305 s.counters[name] = c 306 s.countersSlice = append(s.countersSlice, c) 307 308 return c 309 } 310 311 func (s *scope) counter(sanitizedName string) (Counter, bool) { 312 s.cm.RLock() 313 defer s.cm.RUnlock() 314 315 c, ok := s.counters[sanitizedName] 316 return c, ok 317 } 318 319 func (s *scope) Gauge(name string) Gauge { 320 name = s.sanitizer.Name(name) 321 if g, ok := s.gauge(name); ok { 322 return g 323 } 324 325 s.gm.Lock() 326 defer s.gm.Unlock() 327 328 if g, ok := s.gauges[name]; ok { 329 return g 330 } 331 332 var cachedGauge CachedGauge 333 if s.cachedReporter != nil { 334 cachedGauge = s.cachedReporter.AllocateGauge( 335 s.fullyQualifiedName(name), s.tags, 336 ) 337 } 338 339 g := newGauge(cachedGauge) 340 s.gauges[name] = g 341 s.gaugesSlice = append(s.gaugesSlice, g) 342 343 return g 344 } 345 346 func (s *scope) gauge(name string) (Gauge, bool) { 347 s.gm.RLock() 348 defer s.gm.RUnlock() 349 350 g, ok := s.gauges[name] 351 return g, ok 352 } 353 354 func (s *scope) Timer(name string) Timer { 355 name = s.sanitizer.Name(name) 356 if t, ok := s.timer(name); ok { 357 return t 358 } 359 360 s.tm.Lock() 361 defer s.tm.Unlock() 362 363 if t, ok := s.timers[name]; ok { 364 return t 365 } 366 367 var cachedTimer CachedTimer 368 if s.cachedReporter != nil { 369 cachedTimer = s.cachedReporter.AllocateTimer( 370 s.fullyQualifiedName(name), s.tags, 371 ) 372 } 373 374 t := newTimer( 375 s.fullyQualifiedName(name), s.tags, s.reporter, cachedTimer, 376 ) 377 s.timers[name] = t 378 379 return t 380 } 381 382 func (s *scope) timer(sanitizedName string) (Timer, bool) { 383 s.tm.RLock() 384 defer s.tm.RUnlock() 385 386 t, ok := s.timers[sanitizedName] 387 return t, ok 388 } 389 390 func (s *scope) Histogram(name string, b Buckets) Histogram { 391 name = s.sanitizer.Name(name) 392 if h, ok := s.histogram(name); ok { 393 return h 394 } 395 396 if b == nil { 397 b = s.defaultBuckets 398 } 399 400 htype := valueHistogramType 401 if _, ok := b.(DurationBuckets); ok { 402 htype = durationHistogramType 403 } 404 405 s.hm.Lock() 406 defer s.hm.Unlock() 407 408 if h, ok := s.histograms[name]; ok { 409 return h 410 } 411 412 var cachedHistogram CachedHistogram 413 if s.cachedReporter != nil { 414 cachedHistogram = s.cachedReporter.AllocateHistogram( 415 s.fullyQualifiedName(name), s.tags, b, 416 ) 417 } 418 419 h := newHistogram( 420 htype, 421 s.fullyQualifiedName(name), 422 s.tags, 423 s.reporter, 424 s.bucketCache.Get(htype, b), 425 cachedHistogram, 426 ) 427 s.histograms[name] = h 428 s.histogramsSlice = append(s.histogramsSlice, h) 429 430 return h 431 } 432 433 func (s *scope) histogram(sanitizedName string) (Histogram, bool) { 434 s.hm.RLock() 435 defer s.hm.RUnlock() 436 437 h, ok := s.histograms[sanitizedName] 438 return h, ok 439 } 440 441 func (s *scope) Tagged(tags map[string]string) Scope { 442 return s.subscope(s.prefix, tags) 443 } 444 445 func (s *scope) SubScope(prefix string) Scope { 446 prefix = s.sanitizer.Name(prefix) 447 return s.subscope(s.fullyQualifiedName(prefix), nil) 448 } 449 450 func (s *scope) subscope(prefix string, tags map[string]string) Scope { 451 return s.registry.Subscope(s, prefix, tags) 452 } 453 454 func (s *scope) Capabilities() Capabilities { 455 if s.baseReporter == nil { 456 return capabilitiesNone 457 } 458 return s.baseReporter.Capabilities() 459 } 460 461 func (s *scope) Snapshot() Snapshot { 462 snap := newSnapshot() 463 464 s.registry.ForEachScope(func(ss *scope) { 465 // NB(r): tags are immutable, no lock required to read. 466 tags := make(map[string]string, len(s.tags)) 467 for k, v := range ss.tags { 468 tags[k] = v 469 } 470 471 ss.cm.RLock() 472 for key, c := range ss.counters { 473 name := ss.fullyQualifiedName(key) 474 id := KeyForPrefixedStringMap(name, tags) 475 snap.counters[id] = &counterSnapshot{ 476 name: name, 477 tags: tags, 478 value: c.snapshot(), 479 } 480 } 481 ss.cm.RUnlock() 482 ss.gm.RLock() 483 for key, g := range ss.gauges { 484 name := ss.fullyQualifiedName(key) 485 id := KeyForPrefixedStringMap(name, tags) 486 snap.gauges[id] = &gaugeSnapshot{ 487 name: name, 488 tags: tags, 489 value: g.snapshot(), 490 } 491 } 492 ss.gm.RUnlock() 493 ss.tm.RLock() 494 for key, t := range ss.timers { 495 name := ss.fullyQualifiedName(key) 496 id := KeyForPrefixedStringMap(name, tags) 497 snap.timers[id] = &timerSnapshot{ 498 name: name, 499 tags: tags, 500 values: t.snapshot(), 501 } 502 } 503 ss.tm.RUnlock() 504 ss.hm.RLock() 505 for key, h := range ss.histograms { 506 name := ss.fullyQualifiedName(key) 507 id := KeyForPrefixedStringMap(name, tags) 508 snap.histograms[id] = &histogramSnapshot{ 509 name: name, 510 tags: tags, 511 values: h.snapshotValues(), 512 durations: h.snapshotDurations(), 513 } 514 } 515 ss.hm.RUnlock() 516 }) 517 518 return snap 519 } 520 521 func (s *scope) Close() error { 522 // n.b. Once this flag is set, the next scope report will remove it from 523 // the registry and clear its metrics. 524 if !s.closed.CAS(false, true) { 525 return nil 526 } 527 528 close(s.done) 529 530 if s.root { 531 s.reportRegistry() 532 if closer, ok := s.baseReporter.(io.Closer); ok { 533 return closer.Close() 534 } 535 } 536 537 return nil 538 } 539 540 func (s *scope) clearMetrics() { 541 s.cm.Lock() 542 s.gm.Lock() 543 s.tm.Lock() 544 s.hm.Lock() 545 defer s.cm.Unlock() 546 defer s.gm.Unlock() 547 defer s.tm.Unlock() 548 defer s.hm.Unlock() 549 550 for k := range s.counters { 551 delete(s.counters, k) 552 } 553 s.countersSlice = nil 554 555 for k := range s.gauges { 556 delete(s.gauges, k) 557 } 558 s.gaugesSlice = nil 559 560 for k := range s.timers { 561 delete(s.timers, k) 562 } 563 564 for k := range s.histograms { 565 delete(s.histograms, k) 566 } 567 s.histogramsSlice = nil 568 } 569 570 // NB(prateek): We assume concatenation of sanitized inputs is 571 // sanitized. If that stops being true, then we need to sanitize the 572 // output of this function. 573 func (s *scope) fullyQualifiedName(name string) string { 574 if len(s.prefix) == 0 { 575 return name 576 } 577 // NB: we don't need to sanitize the output of this function as we 578 // sanitize all the the inputs (prefix, separator, name); and the 579 // output we're creating is a concatenation of the sanitized inputs. 580 // If we change the concatenation to involve other inputs or characters, 581 // we'll need to sanitize them too. 582 return s.prefix + s.separator + name 583 } 584 585 func (s *scope) copyAndSanitizeMap(tags map[string]string) map[string]string { 586 result := make(map[string]string, len(tags)) 587 for k, v := range tags { 588 k = s.sanitizer.Key(k) 589 v = s.sanitizer.Value(v) 590 result[k] = v 591 } 592 return result 593 } 594 595 // TestScope is a metrics collector that has no reporting, ensuring that 596 // all emitted values have a given prefix or set of tags 597 type TestScope interface { 598 Scope 599 600 // Snapshot returns a copy of all values since the last report execution, 601 // this is an expensive operation and should only be use for testing purposes 602 Snapshot() Snapshot 603 } 604 605 // Snapshot is a snapshot of values since last report execution 606 type Snapshot interface { 607 // Counters returns a snapshot of all counter summations since last report execution 608 Counters() map[string]CounterSnapshot 609 610 // Gauges returns a snapshot of gauge last values since last report execution 611 Gauges() map[string]GaugeSnapshot 612 613 // Timers returns a snapshot of timer values since last report execution 614 Timers() map[string]TimerSnapshot 615 616 // Histograms returns a snapshot of histogram samples since last report execution 617 Histograms() map[string]HistogramSnapshot 618 } 619 620 // CounterSnapshot is a snapshot of a counter 621 type CounterSnapshot interface { 622 // Name returns the name 623 Name() string 624 625 // Tags returns the tags 626 Tags() map[string]string 627 628 // Value returns the value 629 Value() int64 630 } 631 632 // GaugeSnapshot is a snapshot of a gauge 633 type GaugeSnapshot interface { 634 // Name returns the name 635 Name() string 636 637 // Tags returns the tags 638 Tags() map[string]string 639 640 // Value returns the value 641 Value() float64 642 } 643 644 // TimerSnapshot is a snapshot of a timer 645 type TimerSnapshot interface { 646 // Name returns the name 647 Name() string 648 649 // Tags returns the tags 650 Tags() map[string]string 651 652 // Values returns the values 653 Values() []time.Duration 654 } 655 656 // HistogramSnapshot is a snapshot of a histogram 657 type HistogramSnapshot interface { 658 // Name returns the name 659 Name() string 660 661 // Tags returns the tags 662 Tags() map[string]string 663 664 // Values returns the sample values by upper bound for a valueHistogram 665 Values() map[float64]int64 666 667 // Durations returns the sample values by upper bound for a durationHistogram 668 Durations() map[time.Duration]int64 669 } 670 671 // mergeRightTags merges 2 sets of tags with the tags from tagsRight overriding values from tagsLeft 672 func mergeRightTags(tagsLeft, tagsRight map[string]string) map[string]string { 673 if tagsLeft == nil && tagsRight == nil { 674 return nil 675 } 676 if len(tagsRight) == 0 { 677 return tagsLeft 678 } 679 if len(tagsLeft) == 0 { 680 return tagsRight 681 } 682 683 result := make(map[string]string, len(tagsLeft)+len(tagsRight)) 684 for k, v := range tagsLeft { 685 result[k] = v 686 } 687 for k, v := range tagsRight { 688 result[k] = v 689 } 690 return result 691 } 692 693 type snapshot struct { 694 counters map[string]CounterSnapshot 695 gauges map[string]GaugeSnapshot 696 timers map[string]TimerSnapshot 697 histograms map[string]HistogramSnapshot 698 } 699 700 func newSnapshot() *snapshot { 701 return &snapshot{ 702 counters: make(map[string]CounterSnapshot), 703 gauges: make(map[string]GaugeSnapshot), 704 timers: make(map[string]TimerSnapshot), 705 histograms: make(map[string]HistogramSnapshot), 706 } 707 } 708 709 func (s *snapshot) Counters() map[string]CounterSnapshot { 710 return s.counters 711 } 712 713 func (s *snapshot) Gauges() map[string]GaugeSnapshot { 714 return s.gauges 715 } 716 717 func (s *snapshot) Timers() map[string]TimerSnapshot { 718 return s.timers 719 } 720 721 func (s *snapshot) Histograms() map[string]HistogramSnapshot { 722 return s.histograms 723 } 724 725 type counterSnapshot struct { 726 name string 727 tags map[string]string 728 value int64 729 } 730 731 func (s *counterSnapshot) Name() string { 732 return s.name 733 } 734 735 func (s *counterSnapshot) Tags() map[string]string { 736 return s.tags 737 } 738 739 func (s *counterSnapshot) Value() int64 { 740 return s.value 741 } 742 743 type gaugeSnapshot struct { 744 name string 745 tags map[string]string 746 value float64 747 } 748 749 func (s *gaugeSnapshot) Name() string { 750 return s.name 751 } 752 753 func (s *gaugeSnapshot) Tags() map[string]string { 754 return s.tags 755 } 756 757 func (s *gaugeSnapshot) Value() float64 { 758 return s.value 759 } 760 761 type timerSnapshot struct { 762 name string 763 tags map[string]string 764 values []time.Duration 765 } 766 767 func (s *timerSnapshot) Name() string { 768 return s.name 769 } 770 771 func (s *timerSnapshot) Tags() map[string]string { 772 return s.tags 773 } 774 775 func (s *timerSnapshot) Values() []time.Duration { 776 return s.values 777 } 778 779 type histogramSnapshot struct { 780 name string 781 tags map[string]string 782 values map[float64]int64 783 durations map[time.Duration]int64 784 } 785 786 func (s *histogramSnapshot) Name() string { 787 return s.name 788 } 789 790 func (s *histogramSnapshot) Tags() map[string]string { 791 return s.tags 792 } 793 794 func (s *histogramSnapshot) Values() map[float64]int64 { 795 return s.values 796 } 797 798 func (s *histogramSnapshot) Durations() map[time.Duration]int64 { 799 return s.durations 800 }