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