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  }