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 }