github.com/uber-go/tally/v4@v4.1.17/scope_registry.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  	"hash/maphash"
    25  	"runtime"
    26  	"sync"
    27  	"unsafe"
    28  )
    29  
    30  var (
    31  	scopeRegistryKey = keyForPrefixedStringMaps
    32  
    33  	// Metrics related.
    34  	counterCardinalityName   = "tally.internal.counter_cardinality"
    35  	gaugeCardinalityName     = "tally.internal.gauge_cardinality"
    36  	histogramCardinalityName = "tally.internal.histogram_cardinality"
    37  	scopeCardinalityName     = "tally.internal.num_active_scopes"
    38  )
    39  
    40  const (
    41  	// DefaultTagRedactValue is the default tag value to use when redacting
    42  	DefaultTagRedactValue = "global"
    43  )
    44  
    45  type scopeRegistry struct {
    46  	seed maphash.Seed
    47  	root *scope
    48  	// We need a subscope per GOPROC so that we can take advantage of all the cpu available to the application.
    49  	subscopes []*scopeBucket
    50  	// Internal metrics related.
    51  	omitCardinalityMetrics            bool
    52  	cardinalityMetricsTags            map[string]string
    53  	sanitizedCounterCardinalityName   string
    54  	sanitizedGaugeCardinalityName     string
    55  	sanitizedHistogramCardinalityName string
    56  	sanitizedScopeCardinalityName     string
    57  	cachedCounterCardinalityGauge     CachedGauge
    58  	cachedGaugeCardinalityGauge       CachedGauge
    59  	cachedHistogramCardinalityGauge   CachedGauge
    60  	cachedScopeCardinalityGauge       CachedGauge
    61  }
    62  
    63  type scopeBucket struct {
    64  	mu sync.RWMutex
    65  	s  map[string]*scope
    66  }
    67  
    68  func newScopeRegistryWithShardCount(
    69  	root *scope,
    70  	shardCount uint,
    71  	omitCardinalityMetrics bool,
    72  	cardinalityMetricsTags map[string]string,
    73  ) *scopeRegistry {
    74  	if shardCount == 0 {
    75  		shardCount = uint(runtime.GOMAXPROCS(-1))
    76  	}
    77  
    78  	r := &scopeRegistry{
    79  		root:                              root,
    80  		subscopes:                         make([]*scopeBucket, shardCount),
    81  		seed:                              maphash.MakeSeed(),
    82  		omitCardinalityMetrics:            omitCardinalityMetrics,
    83  		sanitizedCounterCardinalityName:   root.sanitizer.Name(counterCardinalityName),
    84  		sanitizedGaugeCardinalityName:     root.sanitizer.Name(gaugeCardinalityName),
    85  		sanitizedHistogramCardinalityName: root.sanitizer.Name(histogramCardinalityName),
    86  		sanitizedScopeCardinalityName:     root.sanitizer.Name(scopeCardinalityName),
    87  		cardinalityMetricsTags: map[string]string{
    88  			"version":  Version,
    89  			"host":     DefaultTagRedactValue,
    90  			"instance": DefaultTagRedactValue,
    91  		},
    92  	}
    93  
    94  	for k, v := range cardinalityMetricsTags {
    95  		r.cardinalityMetricsTags[root.sanitizer.Key(k)] = root.sanitizer.Value(v)
    96  	}
    97  
    98  	for i := uint(0); i < shardCount; i++ {
    99  		r.subscopes[i] = &scopeBucket{
   100  			s: make(map[string]*scope),
   101  		}
   102  		r.subscopes[i].s[scopeRegistryKey(root.prefix, root.tags)] = root
   103  	}
   104  	if r.root.cachedReporter != nil && !omitCardinalityMetrics {
   105  		r.cachedCounterCardinalityGauge = r.root.cachedReporter.AllocateGauge(r.sanitizedCounterCardinalityName, r.cardinalityMetricsTags)
   106  		r.cachedGaugeCardinalityGauge = r.root.cachedReporter.AllocateGauge(r.sanitizedGaugeCardinalityName, r.cardinalityMetricsTags)
   107  		r.cachedHistogramCardinalityGauge = r.root.cachedReporter.AllocateGauge(r.sanitizedHistogramCardinalityName, r.cardinalityMetricsTags)
   108  		r.cachedScopeCardinalityGauge = r.root.cachedReporter.AllocateGauge(r.sanitizedScopeCardinalityName, r.cardinalityMetricsTags)
   109  	}
   110  	return r
   111  }
   112  
   113  func (r *scopeRegistry) Report(reporter StatsReporter) {
   114  	defer r.purgeIfRootClosed()
   115  	r.reportInternalMetrics()
   116  
   117  	for _, subscopeBucket := range r.subscopes {
   118  		subscopeBucket.mu.RLock()
   119  
   120  		for name, s := range subscopeBucket.s {
   121  			s.report(reporter)
   122  
   123  			if s.closed.Load() {
   124  				r.removeWithRLock(subscopeBucket, name)
   125  				s.clearMetrics()
   126  			}
   127  		}
   128  
   129  		subscopeBucket.mu.RUnlock()
   130  	}
   131  }
   132  
   133  func (r *scopeRegistry) CachedReport() {
   134  	defer r.purgeIfRootClosed()
   135  	r.reportInternalMetrics()
   136  
   137  	for _, subscopeBucket := range r.subscopes {
   138  		subscopeBucket.mu.RLock()
   139  
   140  		for name, s := range subscopeBucket.s {
   141  			s.cachedReport()
   142  
   143  			if s.closed.Load() {
   144  				r.removeWithRLock(subscopeBucket, name)
   145  				s.clearMetrics()
   146  			}
   147  		}
   148  
   149  		subscopeBucket.mu.RUnlock()
   150  	}
   151  }
   152  
   153  func (r *scopeRegistry) ForEachScope(f func(*scope)) {
   154  	for _, subscopeBucket := range r.subscopes {
   155  		subscopeBucket.mu.RLock()
   156  		for _, s := range subscopeBucket.s {
   157  			f(s)
   158  		}
   159  		subscopeBucket.mu.RUnlock()
   160  	}
   161  }
   162  
   163  func (r *scopeRegistry) Subscope(parent *scope, prefix string, tags map[string]string) *scope {
   164  	if r.root.closed.Load() || parent.closed.Load() {
   165  		return NoopScope.(*scope)
   166  	}
   167  
   168  	var (
   169  		buf = keyForPrefixedStringMapsAsKey(make([]byte, 0, 256), prefix, parent.tags, tags)
   170  		h   maphash.Hash
   171  	)
   172  
   173  	h.SetSeed(r.seed)
   174  	_, _ = h.Write(buf)
   175  	subscopeBucket := r.subscopes[h.Sum64()%uint64(len(r.subscopes))]
   176  
   177  	subscopeBucket.mu.RLock()
   178  	// buf is stack allocated and casting it to a string for lookup from the cache
   179  	// as the memory layout of []byte is a superset of string the below casting is safe and does not do any alloc
   180  	// However it cannot be used outside of the stack; a heap allocation is needed if that string needs to be stored
   181  	// in the map as a key
   182  	var (
   183  		unsanitizedKey = *(*string)(unsafe.Pointer(&buf))
   184  		sanitizedKey   string
   185  	)
   186  
   187  	s, ok := r.lockedLookup(subscopeBucket, unsanitizedKey)
   188  	if ok {
   189  		// If this subscope isn't closed or is a test scope, return it.
   190  		// Otherwise, report it immediately and delete it so that a new
   191  		// (functional) scope can be returned instead.
   192  		if !s.closed.Load() || s.testScope {
   193  			subscopeBucket.mu.RUnlock()
   194  			return s
   195  		}
   196  
   197  		switch {
   198  		case parent.reporter != nil:
   199  			s.report(parent.reporter)
   200  		case parent.cachedReporter != nil:
   201  			s.cachedReport()
   202  		}
   203  	}
   204  
   205  	tags = parent.copyAndSanitizeMap(tags)
   206  	sanitizedKey = scopeRegistryKey(prefix, parent.tags, tags)
   207  
   208  	// If a scope was found above but we didn't return, we need to remove the
   209  	// scope from both keys.
   210  	if ok {
   211  		r.removeWithRLock(subscopeBucket, unsanitizedKey)
   212  		r.removeWithRLock(subscopeBucket, sanitizedKey)
   213  		s.clearMetrics()
   214  	}
   215  
   216  	subscopeBucket.mu.RUnlock()
   217  
   218  	// Force-allocate the unsafe string as a safe string. Note that neither
   219  	// string(x) nor x+"" will have the desired effect (the former is a nop,
   220  	// and the latter will likely be elided), so append a new character and
   221  	// truncate instead.
   222  	//
   223  	// ref: https://go.dev/play/p/sxhExUKSxCw
   224  	unsanitizedKey = (unsanitizedKey + ".")[:len(unsanitizedKey)]
   225  
   226  	subscopeBucket.mu.Lock()
   227  	defer subscopeBucket.mu.Unlock()
   228  
   229  	if s, ok := r.lockedLookup(subscopeBucket, sanitizedKey); ok {
   230  		if _, ok = r.lockedLookup(subscopeBucket, unsanitizedKey); !ok {
   231  			subscopeBucket.s[unsanitizedKey] = s
   232  		}
   233  		return s
   234  	}
   235  
   236  	allTags := mergeRightTags(parent.tags, tags)
   237  	subscope := &scope{
   238  		separator: parent.separator,
   239  		prefix:    prefix,
   240  		// NB(prateek): don't need to copy the tags here,
   241  		// we assume the map provided is immutable.
   242  		tags:           allTags,
   243  		reporter:       parent.reporter,
   244  		cachedReporter: parent.cachedReporter,
   245  		baseReporter:   parent.baseReporter,
   246  		defaultBuckets: parent.defaultBuckets,
   247  		sanitizer:      parent.sanitizer,
   248  		registry:       parent.registry,
   249  
   250  		counters:        make(map[string]*counter),
   251  		countersSlice:   make([]*counter, 0, _defaultInitialSliceSize),
   252  		gauges:          make(map[string]*gauge),
   253  		gaugesSlice:     make([]*gauge, 0, _defaultInitialSliceSize),
   254  		histograms:      make(map[string]*histogram),
   255  		histogramsSlice: make([]*histogram, 0, _defaultInitialSliceSize),
   256  		timers:          make(map[string]*timer),
   257  		bucketCache:     parent.bucketCache,
   258  		done:            make(chan struct{}),
   259  		testScope:       parent.testScope,
   260  	}
   261  	subscopeBucket.s[sanitizedKey] = subscope
   262  	if _, ok := r.lockedLookup(subscopeBucket, unsanitizedKey); !ok {
   263  		subscopeBucket.s[unsanitizedKey] = subscope
   264  	}
   265  	return subscope
   266  }
   267  
   268  func (r *scopeRegistry) lockedLookup(subscopeBucket *scopeBucket, key string) (*scope, bool) {
   269  	ss, ok := subscopeBucket.s[key]
   270  	return ss, ok
   271  }
   272  
   273  func (r *scopeRegistry) purgeIfRootClosed() {
   274  	if !r.root.closed.Load() {
   275  		return
   276  	}
   277  
   278  	for _, subscopeBucket := range r.subscopes {
   279  		subscopeBucket.mu.Lock()
   280  		for k, s := range subscopeBucket.s {
   281  			_ = s.Close()
   282  			s.clearMetrics()
   283  			delete(subscopeBucket.s, k)
   284  		}
   285  		subscopeBucket.mu.Unlock()
   286  	}
   287  }
   288  
   289  func (r *scopeRegistry) removeWithRLock(subscopeBucket *scopeBucket, key string) {
   290  	// n.b. This function must lock the registry for writing and return it to an
   291  	//      RLocked state prior to exiting. Defer order is important (LIFO).
   292  	subscopeBucket.mu.RUnlock()
   293  	defer subscopeBucket.mu.RLock()
   294  	subscopeBucket.mu.Lock()
   295  	defer subscopeBucket.mu.Unlock()
   296  	delete(subscopeBucket.s, key)
   297  }
   298  
   299  // Records internal Metrics' cardinalities.
   300  func (r *scopeRegistry) reportInternalMetrics() {
   301  	if r.omitCardinalityMetrics {
   302  		return
   303  	}
   304  
   305  	var counters, gauges, histograms int64
   306  	var rootCounters, rootGauges, rootHistograms int64
   307  	scopes := 1 // Account for root scope.
   308  	r.ForEachScope(
   309  		func(ss *scope) {
   310  			ss.cm.RLock()
   311  			counterSliceLen := int64(len(ss.countersSlice))
   312  			ss.cm.RUnlock()
   313  
   314  			ss.gm.RLock()
   315  			gaugeSliceLen := int64(len(ss.gaugesSlice))
   316  			ss.gm.RUnlock()
   317  
   318  			ss.hm.RLock()
   319  			histogramSliceLen := int64(len(ss.histogramsSlice))
   320  			ss.hm.RUnlock()
   321  
   322  			if ss.root { // Root scope is referenced across all buckets.
   323  				rootCounters = counterSliceLen
   324  				rootGauges = gaugeSliceLen
   325  				rootHistograms = histogramSliceLen
   326  				return
   327  			}
   328  			counters += counterSliceLen
   329  			gauges += gaugeSliceLen
   330  			histograms += histogramSliceLen
   331  			scopes++
   332  		},
   333  	)
   334  
   335  	counters += rootCounters
   336  	gauges += rootGauges
   337  	histograms += rootHistograms
   338  	if r.root.reporter != nil {
   339  		r.root.reporter.ReportGauge(r.sanitizedCounterCardinalityName, r.cardinalityMetricsTags, float64(counters))
   340  		r.root.reporter.ReportGauge(r.sanitizedGaugeCardinalityName, r.cardinalityMetricsTags, float64(gauges))
   341  		r.root.reporter.ReportGauge(r.sanitizedHistogramCardinalityName, r.cardinalityMetricsTags, float64(histograms))
   342  		r.root.reporter.ReportGauge(r.sanitizedScopeCardinalityName, r.cardinalityMetricsTags, float64(scopes))
   343  	}
   344  
   345  	if r.root.cachedReporter != nil {
   346  		r.cachedCounterCardinalityGauge.ReportGauge(float64(counters))
   347  		r.cachedGaugeCardinalityGauge.ReportGauge(float64(gauges))
   348  		r.cachedHistogramCardinalityGauge.ReportGauge(float64(histograms))
   349  		r.cachedScopeCardinalityGauge.ReportGauge(float64(scopes))
   350  	}
   351  }