github.com/uber-go/tally/v4@v4.1.17/prometheus/reporter.go (about) 1 // Copyright (c) 2021 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 prometheus 22 23 import ( 24 "net/http" 25 "strings" 26 "sync" 27 "time" 28 29 "github.com/pkg/errors" 30 prom "github.com/prometheus/client_golang/prometheus" 31 "github.com/prometheus/client_golang/prometheus/promhttp" 32 tally "github.com/uber-go/tally/v4" 33 ) 34 35 const ( 36 // DefaultSeparator is the default separator that should be used with 37 // a tally scope for a prometheus reporter. 38 DefaultSeparator = "_" 39 ) 40 41 var ( 42 errUnknownTimerType = errors.New("unknown metric timer type") 43 ms = float64(time.Millisecond) / float64(time.Second) 44 ) 45 46 // DefaultHistogramBuckets is the default histogram buckets used when 47 // creating a new Histogram in the prometheus registry. 48 // See: https://godoc.org/github.com/prometheus/client_golang/prometheus#HistogramOpts 49 func DefaultHistogramBuckets() []float64 { 50 return []float64{ 51 ms, 52 2 * ms, 53 5 * ms, 54 10 * ms, 55 20 * ms, 56 50 * ms, 57 100 * ms, 58 200 * ms, 59 500 * ms, 60 1000 * ms, 61 2000 * ms, 62 5000 * ms, 63 10000 * ms, 64 } 65 } 66 67 // DefaultSummaryObjectives is the default objectives used when 68 // creating a new Summary in the prometheus registry. 69 // See: https://godoc.org/github.com/prometheus/client_golang/prometheus#SummaryOpts 70 func DefaultSummaryObjectives() map[float64]float64 { 71 return map[float64]float64{ 72 0.5: 0.01, 73 0.75: 0.001, 74 0.95: 0.001, 75 0.99: 0.001, 76 0.999: 0.0001, 77 } 78 } 79 80 // Reporter is a Prometheus backed tally reporter. 81 type Reporter interface { 82 tally.CachedStatsReporter 83 84 // HTTPHandler provides the Prometheus HTTP scrape handler. 85 HTTPHandler() http.Handler 86 87 // RegisterCounter is a helper method to initialize a counter 88 // in the Prometheus backend with a given help text. 89 // If not called explicitly, the Reporter will create one for 90 // you on first use, with a not super helpful HELP string. 91 RegisterCounter( 92 name string, 93 tagKeys []string, 94 desc string, 95 ) (*prom.CounterVec, error) 96 97 // RegisterGauge is a helper method to initialize a gauge 98 // in the prometheus backend with a given help text. 99 // If not called explicitly, the Reporter will create one for 100 // you on first use, with a not super helpful HELP string. 101 RegisterGauge( 102 name string, 103 tagKeys []string, 104 desc string, 105 ) (*prom.GaugeVec, error) 106 107 // RegisterTimer is a helper method to initialize a timer 108 // summary or histogram vector in the prometheus backend 109 // with a given help text. 110 // If not called explicitly, the Reporter will create one for 111 // you on first use, with a not super helpful HELP string. 112 // You may pass opts as nil to get the default timer type 113 // and objectives/buckets. 114 // You may also pass objectives/buckets as nil in opts to 115 // get the default objectives/buckets for the specified 116 // timer type. 117 RegisterTimer( 118 name string, 119 tagKeys []string, 120 desc string, 121 opts *RegisterTimerOptions, 122 ) (TimerUnion, error) 123 } 124 125 // RegisterTimerOptions provides options when registering a timer on demand. 126 // By default you can pass nil for the options to get the reporter defaults. 127 type RegisterTimerOptions struct { 128 TimerType TimerType 129 HistogramBuckets []float64 130 SummaryObjectives map[float64]float64 131 } 132 133 // TimerUnion is a representation of either a summary or a histogram 134 // described by the TimerType. 135 type TimerUnion struct { 136 TimerType TimerType 137 Histogram *prom.HistogramVec 138 Summary *prom.SummaryVec 139 } 140 141 type metricID string 142 143 type reporter struct { 144 sync.RWMutex 145 registerer prom.Registerer 146 gatherer prom.Gatherer 147 timerType TimerType 148 objectives map[float64]float64 149 buckets []float64 150 onRegisterError func(e error) 151 counters map[metricID]*prom.CounterVec 152 gauges map[metricID]*prom.GaugeVec 153 timers map[metricID]*promTimerVec 154 } 155 156 type promTimerVec struct { 157 summary *prom.SummaryVec 158 histogram *prom.HistogramVec 159 } 160 161 type cachedMetric struct { 162 counter prom.Counter 163 gauge prom.Gauge 164 reportTimer func(d time.Duration) 165 histogram prom.Observer 166 summary prom.Observer 167 } 168 169 func (m *cachedMetric) ReportCount(value int64) { 170 m.counter.Add(float64(value)) 171 } 172 173 func (m *cachedMetric) ReportGauge(value float64) { 174 m.gauge.Set(value) 175 } 176 177 func (m *cachedMetric) ReportTimer(interval time.Duration) { 178 m.reportTimer(interval) 179 } 180 181 func (m *cachedMetric) reportTimerHistogram(interval time.Duration) { 182 m.histogram.Observe(float64(interval) / float64(time.Second)) 183 } 184 185 func (m *cachedMetric) reportTimerSummary(interval time.Duration) { 186 m.summary.Observe(float64(interval) / float64(time.Second)) 187 } 188 189 func (m *cachedMetric) ValueBucket( 190 bucketLowerBound, bucketUpperBound float64, 191 ) tally.CachedHistogramBucket { 192 return cachedHistogramBucket{m, bucketUpperBound} 193 } 194 195 func (m *cachedMetric) DurationBucket( 196 bucketLowerBound, bucketUpperBound time.Duration, 197 ) tally.CachedHistogramBucket { 198 upperBound := float64(bucketUpperBound) / float64(time.Second) 199 return cachedHistogramBucket{m, upperBound} 200 } 201 202 type cachedHistogramBucket struct { 203 metric *cachedMetric 204 upperBound float64 205 } 206 207 func (b cachedHistogramBucket) ReportSamples(value int64) { 208 for i := int64(0); i < value; i++ { 209 b.metric.histogram.Observe(b.upperBound) 210 } 211 } 212 213 type noopMetric struct{} 214 215 func (m noopMetric) ReportCount(value int64) {} 216 func (m noopMetric) ReportGauge(value float64) {} 217 func (m noopMetric) ReportTimer(interval time.Duration) {} 218 func (m noopMetric) ReportSamples(value int64) {} 219 func (m noopMetric) ValueBucket(lower, upper float64) tally.CachedHistogramBucket { 220 return m 221 } 222 223 func (m noopMetric) DurationBucket(lower, upper time.Duration) tally.CachedHistogramBucket { 224 return m 225 } 226 227 func (r *reporter) HTTPHandler() http.Handler { 228 return promhttp.HandlerFor(r.gatherer, promhttp.HandlerOpts{}) 229 } 230 231 // TimerType describes a type of timer 232 type TimerType int 233 234 const ( 235 // SummaryTimerType is a timer type that reports into a summary 236 SummaryTimerType TimerType = iota 237 238 // HistogramTimerType is a timer type that reports into a histogram 239 HistogramTimerType 240 ) 241 242 // Options is a set of options for the tally reporter. 243 type Options struct { 244 // Registerer is the prometheus registerer to register 245 // metrics with. Use nil to specify the default registerer. 246 Registerer prom.Registerer 247 248 // Gatherer is the prometheus gatherer to gather 249 // metrics with. Use nil to specify the default gatherer. 250 Gatherer prom.Gatherer 251 252 // DefaultTimerType is the default type timer type to create 253 // when using timers. It's default value is a summary timer type. 254 DefaultTimerType TimerType 255 256 // DefaultHistogramBuckets is the default histogram buckets 257 // to use. Use nil to specify the default histogram buckets. 258 DefaultHistogramBuckets []float64 259 260 // DefaultSummaryObjectives is the default summary objectives 261 // to use. Use nil to specify the default summary objectives. 262 DefaultSummaryObjectives map[float64]float64 263 264 // OnRegisterError defines a method to call to when registering 265 // a metric with the registerer fails. Use nil to specify 266 // to panic by default when registering fails. 267 OnRegisterError func(err error) 268 } 269 270 // NewReporter returns a new Reporter for Prometheus client backed metrics 271 // objectives is the objectives used when creating a new Summary histogram for Timers. See 272 // https://godoc.org/github.com/prometheus/client_golang/prometheus#SummaryOpts for more details. 273 func NewReporter(opts Options) Reporter { 274 if opts.Registerer == nil { 275 opts.Registerer = prom.DefaultRegisterer 276 } else { 277 // A specific registerer was set, check if it's a registry and if 278 // no gatherer was set, then use that as the gatherer 279 if reg, ok := opts.Registerer.(*prom.Registry); ok && opts.Gatherer == nil { 280 opts.Gatherer = reg 281 } 282 } 283 if opts.Gatherer == nil { 284 opts.Gatherer = prom.DefaultGatherer 285 } 286 if opts.DefaultHistogramBuckets == nil { 287 opts.DefaultHistogramBuckets = DefaultHistogramBuckets() 288 } 289 if opts.DefaultSummaryObjectives == nil { 290 opts.DefaultSummaryObjectives = DefaultSummaryObjectives() 291 } 292 if opts.OnRegisterError == nil { 293 opts.OnRegisterError = func(err error) { 294 // n.b. Because our forked Prometheus client does not actually emit 295 // this message as a concrete error type (it uses fmt.Errorf), 296 // we need to check the error message. 297 if strings.Contains(err.Error(), "previously registered") { 298 err = errors.WithMessagef( 299 err, 300 "potential tally.Scope() vs Prometheus usage contract mismatch: "+ 301 "if this occurs after using Scope.Tagged(), different metric "+ 302 "names must be used than were registered with the parent scope", 303 ) 304 } 305 306 panic(err) 307 } 308 } 309 310 return &reporter{ 311 registerer: opts.Registerer, 312 gatherer: opts.Gatherer, 313 timerType: opts.DefaultTimerType, 314 buckets: opts.DefaultHistogramBuckets, 315 objectives: opts.DefaultSummaryObjectives, 316 onRegisterError: opts.OnRegisterError, 317 counters: make(map[metricID]*prom.CounterVec), 318 gauges: make(map[metricID]*prom.GaugeVec), 319 timers: make(map[metricID]*promTimerVec), 320 } 321 } 322 323 func (r *reporter) RegisterCounter( 324 name string, 325 tagKeys []string, 326 desc string, 327 ) (*prom.CounterVec, error) { 328 return r.counterVec(name, tagKeys, desc) 329 } 330 331 func (r *reporter) counterVec( 332 name string, 333 tagKeys []string, 334 desc string, 335 ) (*prom.CounterVec, error) { 336 id := canonicalMetricID(name, tagKeys) 337 338 r.Lock() 339 defer r.Unlock() 340 341 if ctr, ok := r.counters[id]; ok { 342 return ctr, nil 343 } 344 345 ctr := prom.NewCounterVec( 346 prom.CounterOpts{ 347 Name: name, 348 Help: desc, 349 }, 350 tagKeys, 351 ) 352 353 if err := r.registerer.Register(ctr); err != nil { 354 return nil, err 355 } 356 357 r.counters[id] = ctr 358 return ctr, nil 359 } 360 361 // AllocateCounter implements tally.CachedStatsReporter. 362 func (r *reporter) AllocateCounter(name string, tags map[string]string) tally.CachedCount { 363 tagKeys := keysFromMap(tags) 364 counterVec, err := r.counterVec(name, tagKeys, name+" counter") 365 if err != nil { 366 r.onRegisterError(err) 367 return noopMetric{} 368 } 369 return &cachedMetric{counter: counterVec.With(tags)} 370 } 371 372 func (r *reporter) RegisterGauge( 373 name string, 374 tagKeys []string, 375 desc string, 376 ) (*prom.GaugeVec, error) { 377 return r.gaugeVec(name, tagKeys, desc) 378 } 379 380 func (r *reporter) gaugeVec( 381 name string, 382 tagKeys []string, 383 desc string, 384 ) (*prom.GaugeVec, error) { 385 id := canonicalMetricID(name, tagKeys) 386 387 r.Lock() 388 defer r.Unlock() 389 390 if g, ok := r.gauges[id]; ok { 391 return g, nil 392 } 393 394 g := prom.NewGaugeVec( 395 prom.GaugeOpts{ 396 Name: name, 397 Help: desc, 398 }, 399 tagKeys, 400 ) 401 402 if err := r.registerer.Register(g); err != nil { 403 return nil, err 404 } 405 406 r.gauges[id] = g 407 return g, nil 408 } 409 410 // AllocateGauge implements tally.CachedStatsReporter. 411 func (r *reporter) AllocateGauge(name string, tags map[string]string) tally.CachedGauge { 412 tagKeys := keysFromMap(tags) 413 gaugeVec, err := r.gaugeVec(name, tagKeys, name+" gauge") 414 if err != nil { 415 r.onRegisterError(err) 416 return noopMetric{} 417 } 418 return &cachedMetric{gauge: gaugeVec.With(tags)} 419 } 420 421 func (r *reporter) RegisterTimer( 422 name string, 423 tagKeys []string, 424 desc string, 425 opts *RegisterTimerOptions, 426 ) (TimerUnion, error) { 427 timerType, buckets, objectives := r.timerConfig(opts) 428 switch timerType { 429 case HistogramTimerType: 430 h, err := r.histogramVec(name, tagKeys, desc, buckets) 431 return TimerUnion{TimerType: timerType, Histogram: h}, err 432 case SummaryTimerType: 433 s, err := r.summaryVec(name, tagKeys, desc, objectives) 434 return TimerUnion{TimerType: timerType, Summary: s}, err 435 } 436 return TimerUnion{}, errUnknownTimerType 437 } 438 439 func (r *reporter) timerConfig( 440 opts *RegisterTimerOptions, 441 ) ( 442 timerType TimerType, 443 buckets []float64, 444 objectives map[float64]float64, 445 ) { 446 timerType = r.timerType 447 objectives = r.objectives 448 buckets = r.buckets 449 if opts != nil { 450 timerType = opts.TimerType 451 if opts.SummaryObjectives != nil { 452 objectives = opts.SummaryObjectives 453 } 454 if opts.HistogramBuckets != nil { 455 buckets = opts.HistogramBuckets 456 } 457 } 458 return 459 } 460 461 func (r *reporter) summaryVec( 462 name string, 463 tagKeys []string, 464 desc string, 465 objectives map[float64]float64, 466 ) (*prom.SummaryVec, error) { 467 id := canonicalMetricID(name, tagKeys) 468 469 r.Lock() 470 defer r.Unlock() 471 472 if s, ok := r.timers[id]; ok { 473 return s.summary, nil 474 } 475 476 s := prom.NewSummaryVec( 477 prom.SummaryOpts{ 478 Name: name, 479 Help: desc, 480 Objectives: objectives, 481 }, 482 tagKeys, 483 ) 484 485 if err := r.registerer.Register(s); err != nil { 486 return nil, err 487 } 488 489 r.timers[id] = &promTimerVec{summary: s} 490 return s, nil 491 } 492 493 func (r *reporter) histogramVec( 494 name string, 495 tagKeys []string, 496 desc string, 497 buckets []float64, 498 ) (*prom.HistogramVec, error) { 499 id := canonicalMetricID(name, tagKeys) 500 501 r.Lock() 502 defer r.Unlock() 503 504 if h, ok := r.timers[id]; ok { 505 return h.histogram, nil 506 } 507 508 h := prom.NewHistogramVec( 509 prom.HistogramOpts{ 510 Name: name, 511 Help: desc, 512 Buckets: buckets, 513 }, 514 tagKeys, 515 ) 516 517 if err := r.registerer.Register(h); err != nil { 518 return nil, err 519 } 520 521 r.timers[id] = &promTimerVec{histogram: h} 522 return h, nil 523 } 524 525 // AllocateTimer implements tally.CachedStatsReporter. 526 func (r *reporter) AllocateTimer(name string, tags map[string]string) tally.CachedTimer { 527 var ( 528 timer tally.CachedTimer 529 err error 530 ) 531 tagKeys := keysFromMap(tags) 532 timerType, buckets, objectives := r.timerConfig(nil) 533 switch timerType { 534 case HistogramTimerType: 535 var histogramVec *prom.HistogramVec 536 histogramVec, err = r.histogramVec(name, tagKeys, name+" histogram", buckets) 537 if err == nil { 538 t := &cachedMetric{histogram: histogramVec.With(tags)} 539 t.reportTimer = t.reportTimerHistogram 540 timer = t 541 } 542 case SummaryTimerType: 543 var summaryVec *prom.SummaryVec 544 summaryVec, err = r.summaryVec(name, tagKeys, name+" summary", objectives) 545 if err == nil { 546 t := &cachedMetric{summary: summaryVec.With(tags)} 547 t.reportTimer = t.reportTimerSummary 548 timer = t 549 } 550 default: 551 err = errUnknownTimerType 552 } 553 if err != nil { 554 r.onRegisterError(err) 555 return noopMetric{} 556 } 557 return timer 558 } 559 560 func (r *reporter) AllocateHistogram( 561 name string, 562 tags map[string]string, 563 buckets tally.Buckets, 564 ) tally.CachedHistogram { 565 tagKeys := keysFromMap(tags) 566 histogramVec, err := r.histogramVec(name, tagKeys, name+" histogram", buckets.AsValues()) 567 if err != nil { 568 r.onRegisterError(err) 569 return noopMetric{} 570 } 571 return &cachedMetric{histogram: histogramVec.With(tags)} 572 } 573 574 func (r *reporter) Capabilities() tally.Capabilities { 575 return r 576 } 577 578 func (r *reporter) Reporting() bool { 579 return true 580 } 581 582 func (r *reporter) Tagging() bool { 583 return true 584 } 585 586 // Flush does nothing for prometheus 587 func (r *reporter) Flush() {} 588 589 var metricIDKeyValue = "1" 590 591 // NOTE: this generates a canonical MetricID for a given name+label keys, 592 // not values. This omits label values, as we track metrics as 593 // Vectors in order to support on-the-fly label changes. 594 func canonicalMetricID(name string, tagKeys []string) metricID { 595 keySet := make(map[string]string, len(tagKeys)) 596 for _, key := range tagKeys { 597 keySet[key] = metricIDKeyValue 598 } 599 return metricID(tally.KeyForPrefixedStringMap(name, keySet)) 600 } 601 602 func keysFromMap(m map[string]string) []string { 603 labelKeys := make([]string, len(m)) 604 i := 0 605 for k := range m { 606 labelKeys[i] = k 607 i++ 608 } 609 return labelKeys 610 }