github.com/lyft/flytestdlib@v0.3.12-0.20210213045714-8cdd111ecda1/promutils/scope.go (about)

     1  package promutils
     2  
     3  import (
     4  	"strings"
     5  	"time"
     6  
     7  	"k8s.io/apimachinery/pkg/util/rand"
     8  
     9  	"github.com/prometheus/client_golang/prometheus"
    10  )
    11  
    12  const defaultScopeDelimiterStr = ":"
    13  const defaultMetricDelimiterStr = "_"
    14  
    15  var (
    16  	defaultObjectives = map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}
    17  	defaultBuckets    = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}
    18  )
    19  
    20  func panicIfError(err error) {
    21  	if err != nil {
    22  		panic("Failed to register metrics. Error: " + err.Error())
    23  	}
    24  }
    25  
    26  // A Simple StopWatch that works with prometheus summary
    27  // It will scale the output to match the expected time scale (milliseconds, seconds etc)
    28  // NOTE: Do not create a StopWatch object by hand, use a Scope to get a new instance of the StopWatch object
    29  type StopWatch struct {
    30  	prometheus.Observer
    31  	outputScale time.Duration
    32  }
    33  
    34  // Start creates a new Instance of the StopWatch called a Timer that is closeable/stoppable.
    35  // Common pattern to time a scope would be
    36  // {
    37  //   timer := stopWatch.Start()
    38  //   defer timer.Stop()
    39  //   ....
    40  // }
    41  func (s StopWatch) Start() Timer {
    42  	return Timer{
    43  		start:       time.Now(),
    44  		outputScale: s.outputScale,
    45  		timer:       s.Observer,
    46  	}
    47  }
    48  
    49  // Observes specified duration between the start and end time
    50  func (s StopWatch) Observe(start, end time.Time) {
    51  	observed := end.Sub(start).Nanoseconds()
    52  	outputScaleDuration := s.outputScale.Nanoseconds()
    53  	if outputScaleDuration == 0 {
    54  		s.Observer.Observe(0)
    55  		return
    56  	}
    57  	scaled := float64(observed / outputScaleDuration)
    58  	s.Observer.Observe(scaled)
    59  }
    60  
    61  // Observes/records the time to execute the given function synchronously
    62  func (s StopWatch) Time(f func()) {
    63  	t := s.Start()
    64  	f()
    65  	t.Stop()
    66  }
    67  
    68  // A Simple StopWatch that works with prometheus summary
    69  // It will scale the output to match the expected time scale (milliseconds, seconds etc)
    70  // NOTE: Do not create a StopWatch object by hand, use a Scope to get a new instance of the StopWatch object
    71  type StopWatchVec struct {
    72  	*prometheus.SummaryVec
    73  	outputScale time.Duration
    74  }
    75  
    76  // Gets a concrete StopWatch instance that can be used to start a timer and record observations.
    77  func (s StopWatchVec) WithLabelValues(values ...string) StopWatch {
    78  	return StopWatch{
    79  		Observer:    s.SummaryVec.WithLabelValues(values...),
    80  		outputScale: s.outputScale,
    81  	}
    82  }
    83  
    84  func (s StopWatchVec) GetMetricWith(labels prometheus.Labels) (StopWatch, error) {
    85  	sVec, err := s.SummaryVec.GetMetricWith(labels)
    86  	if err != nil {
    87  		return StopWatch{}, err
    88  	}
    89  	return StopWatch{
    90  		Observer:    sVec,
    91  		outputScale: s.outputScale,
    92  	}, nil
    93  }
    94  
    95  // This is a stoppable instance of a StopWatch or a Timer
    96  // A Timer can only be stopped. On stopping it will output the elapsed duration to prometheus
    97  type Timer struct {
    98  	start       time.Time
    99  	outputScale time.Duration
   100  	timer       prometheus.Observer
   101  }
   102  
   103  // This method observes the elapsed duration since the creation of the timer. The timer is created using a StopWatch
   104  func (s Timer) Stop() float64 {
   105  	observed := time.Since(s.start).Nanoseconds()
   106  	outputScaleDuration := s.outputScale.Nanoseconds()
   107  	if outputScaleDuration == 0 {
   108  		s.timer.Observe(0)
   109  		return 0
   110  	}
   111  	scaled := float64(observed / outputScaleDuration)
   112  	s.timer.Observe(scaled)
   113  	return scaled
   114  }
   115  
   116  // A SummaryOptions represents a set of options that can be supplied when creating a new prometheus summary metric
   117  type SummaryOptions struct {
   118  	// An Objectives defines the quantile rank estimates with their respective absolute errors.
   119  	// Refer to https://godoc.org/github.com/prometheus/client_golang/prometheus#SummaryOpts for details
   120  	Objectives map[float64]float64
   121  }
   122  
   123  // A Scope represents a prefix in Prometheus. It is nestable, thus every metric that is published does not need to
   124  // provide a prefix, but just the name of the metric. As long as the Scope is used to create a new instance of the metric
   125  // The prefix (or scope) is automatically set.
   126  type Scope interface {
   127  	// Creates new prometheus.Gauge metric with the prefix as the CurrentScope
   128  	// Name is a string that follows prometheus conventions (mostly [_a-z])
   129  	// Refer to https://prometheus.io/docs/concepts/metric_types/ for more information
   130  	NewGauge(name, description string) (prometheus.Gauge, error)
   131  	MustNewGauge(name, description string) prometheus.Gauge
   132  
   133  	// Creates new prometheus.GaugeVec metric with the prefix as the CurrentScope
   134  	// Refer to https://prometheus.io/docs/concepts/metric_types/ for more information
   135  	NewGaugeVec(name, description string, labelNames ...string) (*prometheus.GaugeVec, error)
   136  	MustNewGaugeVec(name, description string, labelNames ...string) *prometheus.GaugeVec
   137  
   138  	// Creates new prometheus.Summary metric with the prefix as the CurrentScope
   139  	// Refer to https://prometheus.io/docs/concepts/metric_types/ for more information
   140  	NewSummary(name, description string) (prometheus.Summary, error)
   141  	MustNewSummary(name, description string) prometheus.Summary
   142  
   143  	// Creates new prometheus.Summary metric with custom options, such as a custom set of objectives (i.e., target quantiles).
   144  	// Refer to https://prometheus.io/docs/concepts/metric_types/ for more information
   145  	NewSummaryWithOptions(name, description string, options SummaryOptions) (prometheus.Summary, error)
   146  	MustNewSummaryWithOptions(name, description string, options SummaryOptions) prometheus.Summary
   147  
   148  	// Creates new prometheus.SummaryVec metric with the prefix as the CurrentScope
   149  	// Refer to https://prometheus.io/docs/concepts/metric_types/ for more information
   150  	NewSummaryVec(name, description string, labelNames ...string) (*prometheus.SummaryVec, error)
   151  	MustNewSummaryVec(name, description string, labelNames ...string) *prometheus.SummaryVec
   152  
   153  	// Creates new prometheus.Histogram metric with the prefix as the CurrentScope
   154  	// Refer to https://prometheus.io/docs/concepts/metric_types/ for more information
   155  	NewHistogram(name, description string) (prometheus.Histogram, error)
   156  	MustNewHistogram(name, description string) prometheus.Histogram
   157  
   158  	// Creates new prometheus.HistogramVec metric with the prefix as the CurrentScope
   159  	// Refer to https://prometheus.io/docs/concepts/metric_types/ for more information
   160  	NewHistogramVec(name, description string, labelNames ...string) (*prometheus.HistogramVec, error)
   161  	MustNewHistogramVec(name, description string, labelNames ...string) *prometheus.HistogramVec
   162  
   163  	// Creates new prometheus.Counter metric with the prefix as the CurrentScope
   164  	// Refer to https://prometheus.io/docs/concepts/metric_types/ for more information
   165  	// Important to note, counters are not like typical counters. These are ever increasing and cumulative.
   166  	// So if you want to observe counters within buckets use Summary/Histogram
   167  	NewCounter(name, description string) (prometheus.Counter, error)
   168  	MustNewCounter(name, description string) prometheus.Counter
   169  
   170  	// Creates new prometheus.GaugeVec metric with the prefix as the CurrentScope
   171  	// Refer to https://prometheus.io/docs/concepts/metric_types/ for more information
   172  	NewCounterVec(name, description string, labelNames ...string) (*prometheus.CounterVec, error)
   173  	MustNewCounterVec(name, description string, labelNames ...string) *prometheus.CounterVec
   174  
   175  	// This is a custom wrapper to create a StopWatch object in the current Scope.
   176  	// Duration is to specify the scale of the Timer. For example if you are measuring times in milliseconds
   177  	// pass scale=times.Millisecond
   178  	// https://golang.org/pkg/time/#Duration
   179  	// The metric name is auto-suffixed with the right scale. Refer to DurationToString to understand
   180  	NewStopWatch(name, description string, scale time.Duration) (StopWatch, error)
   181  	MustNewStopWatch(name, description string, scale time.Duration) StopWatch
   182  
   183  	// This is a custom wrapper to create a StopWatch object in the current Scope.
   184  	// Duration is to specify the scale of the Timer. For example if you are measuring times in milliseconds
   185  	// pass scale=times.Millisecond
   186  	// https://golang.org/pkg/time/#Duration
   187  	// The metric name is auto-suffixed with the right scale. Refer to DurationToString to understand
   188  	NewStopWatchVec(name, description string, scale time.Duration, labelNames ...string) (*StopWatchVec, error)
   189  	MustNewStopWatchVec(name, description string, scale time.Duration, labelNames ...string) *StopWatchVec
   190  
   191  	// In case nesting is desired for metrics, create a new subScope. This is generally useful in creating
   192  	// Scoped and SubScoped metrics
   193  	NewSubScope(name string) Scope
   194  
   195  	// Returns the current ScopeName. Use for creating your own metrics
   196  	CurrentScope() string
   197  
   198  	// Method that provides a scoped metric name. Can be used, if you want to directly create your own metric
   199  	NewScopedMetricName(name string) string
   200  }
   201  
   202  type metricsScope struct {
   203  	scope string
   204  }
   205  
   206  func (m metricsScope) NewGauge(name, description string) (prometheus.Gauge, error) {
   207  	g := prometheus.NewGauge(
   208  		prometheus.GaugeOpts{
   209  			Name: m.NewScopedMetricName(name),
   210  			Help: description,
   211  		},
   212  	)
   213  	return g, prometheus.Register(g)
   214  }
   215  
   216  func (m metricsScope) MustNewGauge(name, description string) prometheus.Gauge {
   217  	g, err := m.NewGauge(name, description)
   218  	panicIfError(err)
   219  	return g
   220  }
   221  
   222  func (m metricsScope) NewGaugeVec(name, description string, labelNames ...string) (*prometheus.GaugeVec, error) {
   223  	g := prometheus.NewGaugeVec(
   224  		prometheus.GaugeOpts{
   225  			Name: m.NewScopedMetricName(name),
   226  			Help: description,
   227  		},
   228  		labelNames,
   229  	)
   230  	return g, prometheus.Register(g)
   231  }
   232  
   233  func (m metricsScope) MustNewGaugeVec(name, description string, labelNames ...string) *prometheus.GaugeVec {
   234  	g, err := m.NewGaugeVec(name, description, labelNames...)
   235  	panicIfError(err)
   236  	return g
   237  }
   238  
   239  func (m metricsScope) NewSummary(name, description string) (prometheus.Summary, error) {
   240  	return m.NewSummaryWithOptions(name, description, SummaryOptions{Objectives: defaultObjectives})
   241  }
   242  
   243  func (m metricsScope) MustNewSummary(name, description string) prometheus.Summary {
   244  	s, err := m.NewSummary(name, description)
   245  	panicIfError(err)
   246  	return s
   247  }
   248  
   249  func (m metricsScope) NewSummaryWithOptions(name, description string, options SummaryOptions) (prometheus.Summary, error) {
   250  	s := prometheus.NewSummary(
   251  		prometheus.SummaryOpts{
   252  			Name:       m.NewScopedMetricName(name),
   253  			Help:       description,
   254  			Objectives: options.Objectives,
   255  		},
   256  	)
   257  
   258  	return s, prometheus.Register(s)
   259  }
   260  
   261  func (m metricsScope) MustNewSummaryWithOptions(name, description string, options SummaryOptions) prometheus.Summary {
   262  	s, err := m.NewSummaryWithOptions(name, description, options)
   263  	panicIfError(err)
   264  	return s
   265  }
   266  
   267  func (m metricsScope) NewSummaryVec(name, description string, labelNames ...string) (*prometheus.SummaryVec, error) {
   268  	s := prometheus.NewSummaryVec(
   269  		prometheus.SummaryOpts{
   270  			Name:       m.NewScopedMetricName(name),
   271  			Help:       description,
   272  			Objectives: defaultObjectives,
   273  		},
   274  		labelNames,
   275  	)
   276  
   277  	return s, prometheus.Register(s)
   278  }
   279  func (m metricsScope) MustNewSummaryVec(name, description string, labelNames ...string) *prometheus.SummaryVec {
   280  	s, err := m.NewSummaryVec(name, description, labelNames...)
   281  	panicIfError(err)
   282  	return s
   283  }
   284  
   285  func (m metricsScope) NewHistogram(name, description string) (prometheus.Histogram, error) {
   286  	h := prometheus.NewHistogram(
   287  		prometheus.HistogramOpts{
   288  			Name:    m.NewScopedMetricName(name),
   289  			Help:    description,
   290  			Buckets: defaultBuckets,
   291  		},
   292  	)
   293  	return h, prometheus.Register(h)
   294  }
   295  
   296  func (m metricsScope) MustNewHistogram(name, description string) prometheus.Histogram {
   297  	h, err := m.NewHistogram(name, description)
   298  	panicIfError(err)
   299  	return h
   300  }
   301  
   302  func (m metricsScope) NewHistogramVec(name, description string, labelNames ...string) (*prometheus.HistogramVec, error) {
   303  	h := prometheus.NewHistogramVec(
   304  		prometheus.HistogramOpts{
   305  			Name:    m.NewScopedMetricName(name),
   306  			Help:    description,
   307  			Buckets: defaultBuckets,
   308  		},
   309  		labelNames,
   310  	)
   311  	return h, prometheus.Register(h)
   312  }
   313  
   314  func (m metricsScope) MustNewHistogramVec(name, description string, labelNames ...string) *prometheus.HistogramVec {
   315  	h, err := m.NewHistogramVec(name, description, labelNames...)
   316  	panicIfError(err)
   317  	return h
   318  }
   319  
   320  func (m metricsScope) NewCounter(name, description string) (prometheus.Counter, error) {
   321  	c := prometheus.NewCounter(
   322  		prometheus.CounterOpts{
   323  			Name: m.NewScopedMetricName(name),
   324  			Help: description,
   325  		},
   326  	)
   327  	return c, prometheus.Register(c)
   328  }
   329  
   330  func (m metricsScope) MustNewCounter(name, description string) prometheus.Counter {
   331  	c, err := m.NewCounter(name, description)
   332  	panicIfError(err)
   333  	return c
   334  }
   335  
   336  func (m metricsScope) NewCounterVec(name, description string, labelNames ...string) (*prometheus.CounterVec, error) {
   337  	c := prometheus.NewCounterVec(
   338  		prometheus.CounterOpts{
   339  			Name: m.NewScopedMetricName(name),
   340  			Help: description,
   341  		},
   342  		labelNames,
   343  	)
   344  	return c, prometheus.Register(c)
   345  }
   346  
   347  func (m metricsScope) MustNewCounterVec(name, description string, labelNames ...string) *prometheus.CounterVec {
   348  	c, err := m.NewCounterVec(name, description, labelNames...)
   349  	panicIfError(err)
   350  	return c
   351  }
   352  
   353  func (m metricsScope) NewStopWatch(name, description string, scale time.Duration) (StopWatch, error) {
   354  	if !strings.HasSuffix(name, defaultMetricDelimiterStr) {
   355  		name += defaultMetricDelimiterStr
   356  	}
   357  	name += DurationToString(scale)
   358  	s, err := m.NewSummary(name, description)
   359  	if err != nil {
   360  		return StopWatch{}, err
   361  	}
   362  
   363  	return StopWatch{
   364  		Observer:    s,
   365  		outputScale: scale,
   366  	}, nil
   367  }
   368  
   369  func (m metricsScope) MustNewStopWatch(name, description string, scale time.Duration) StopWatch {
   370  	s, err := m.NewStopWatch(name, description, scale)
   371  	panicIfError(err)
   372  	return s
   373  }
   374  
   375  func (m metricsScope) NewStopWatchVec(name, description string, scale time.Duration, labelNames ...string) (*StopWatchVec, error) {
   376  	if !strings.HasSuffix(name, defaultMetricDelimiterStr) {
   377  		name += defaultMetricDelimiterStr
   378  	}
   379  	name += DurationToString(scale)
   380  	s, err := m.NewSummaryVec(name, description, labelNames...)
   381  	if err != nil {
   382  		return &StopWatchVec{}, err
   383  	}
   384  
   385  	return &StopWatchVec{
   386  		SummaryVec:  s,
   387  		outputScale: scale,
   388  	}, nil
   389  }
   390  
   391  func (m metricsScope) MustNewStopWatchVec(name, description string, scale time.Duration, labelNames ...string) *StopWatchVec {
   392  	s, err := m.NewStopWatchVec(name, description, scale, labelNames...)
   393  	panicIfError(err)
   394  	return s
   395  }
   396  
   397  func (m metricsScope) CurrentScope() string {
   398  	return m.scope
   399  }
   400  
   401  // Creates a metric name under the scope. Scope will always have a defaultScopeDelimiterRune as the last character
   402  func (m metricsScope) NewScopedMetricName(name string) string {
   403  	if name == "" {
   404  		panic("metric name cannot be an empty string")
   405  	}
   406  
   407  	return m.scope + name
   408  }
   409  
   410  func (m metricsScope) NewSubScope(subscopeName string) Scope {
   411  	if subscopeName == "" {
   412  		panic("scope name cannot be an empty string")
   413  	}
   414  
   415  	// If the last character of the new subscope is already a defaultScopeDelimiterRune, do not add anything
   416  	if !strings.HasSuffix(subscopeName, defaultScopeDelimiterStr) {
   417  		subscopeName += defaultScopeDelimiterStr
   418  	}
   419  
   420  	// Always add a new defaultScopeDelimiterRune to every scope name
   421  	return NewScope(m.scope + subscopeName)
   422  }
   423  
   424  // Creates a new scope in the format `name + defaultScopeDelimiterRune`
   425  // If the last character is already a defaultScopeDelimiterRune, then it does not add it to the scope name
   426  func NewScope(name string) Scope {
   427  	if name == "" {
   428  		panic("base scope for a metric cannot be an empty string")
   429  	}
   430  
   431  	// If the last character of the new subscope is already a defaultScopeDelimiterRune, do not add anything
   432  	if !strings.HasSuffix(name, defaultScopeDelimiterStr) {
   433  		name += defaultScopeDelimiterStr
   434  	}
   435  
   436  	return metricsScope{
   437  		scope: name,
   438  	}
   439  }
   440  
   441  // Returns a randomly-named scope for use in tests.
   442  // Prometheus requires that metric names begin with a single word, which is generated from the alphabetic testScopeNameCharset.
   443  func NewTestScope() Scope {
   444  	return NewScope("test" + rand.String(6))
   445  }
   446  
   447  // DurationToString converts the duration to a string suffix that indicates the scale of the timer.
   448  func DurationToString(duration time.Duration) string {
   449  	if duration >= time.Hour {
   450  		return "h"
   451  	}
   452  	if duration >= time.Minute {
   453  		return "m"
   454  	}
   455  	if duration >= time.Second {
   456  		return "s"
   457  	}
   458  	if duration >= time.Millisecond {
   459  		return "ms"
   460  	}
   461  	if duration >= time.Microsecond {
   462  		return "us"
   463  	}
   464  	return "ns"
   465  }