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 }