github.com/uber-go/tally/v4@v4.1.17/prometheus/reporter_test.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  	"fmt"
    25  	"testing"
    26  	"time"
    27  
    28  	prom "github.com/prometheus/client_golang/prometheus"
    29  	dto "github.com/prometheus/client_model/go"
    30  	"github.com/stretchr/testify/assert"
    31  	"github.com/stretchr/testify/require"
    32  	tally "github.com/uber-go/tally/v4"
    33  )
    34  
    35  // NB(r): If a test is failing, you can debug what is being
    36  // gathered from Prometheus by printing the following:
    37  // proto.MarshalTextString(gather(t, registry)[0])
    38  
    39  func TestCounter(t *testing.T) {
    40  	registry := prom.NewRegistry()
    41  	r := NewReporter(Options{Registerer: registry})
    42  	name := "test_counter"
    43  	tags := map[string]string{
    44  		"foo":  "bar",
    45  		"test": "everything",
    46  	}
    47  	tags2 := map[string]string{
    48  		"foo":  "baz",
    49  		"test": "something",
    50  	}
    51  
    52  	count := r.AllocateCounter(name, tags)
    53  	count.ReportCount(1)
    54  	count.ReportCount(2)
    55  
    56  	count = r.AllocateCounter(name, tags2)
    57  	count.ReportCount(2)
    58  
    59  	assertMetric(t, gather(t, registry), metric{
    60  		name:  name,
    61  		mtype: dto.MetricType_COUNTER,
    62  		instances: []instance{
    63  			{
    64  				labels:  tags,
    65  				counter: counterValue(3),
    66  			},
    67  			{
    68  				labels:  tags2,
    69  				counter: counterValue(2),
    70  			},
    71  		},
    72  	})
    73  }
    74  
    75  func TestGauge(t *testing.T) {
    76  	registry := prom.NewRegistry()
    77  	r := NewReporter(Options{Registerer: registry})
    78  	name := "test_gauge"
    79  	tags := map[string]string{
    80  		"foo":  "bar",
    81  		"test": "everything",
    82  	}
    83  
    84  	gauge := r.AllocateGauge(name, tags)
    85  	gauge.ReportGauge(15)
    86  	gauge.ReportGauge(30)
    87  
    88  	assertMetric(t, gather(t, registry), metric{
    89  		name:  name,
    90  		mtype: dto.MetricType_GAUGE,
    91  		instances: []instance{
    92  			{
    93  				labels: tags,
    94  				gauge:  gaugeValue(30),
    95  			},
    96  		},
    97  	})
    98  }
    99  
   100  func TestTimerHistogram(t *testing.T) {
   101  	registry := prom.NewRegistry()
   102  	r := NewReporter(Options{
   103  		Registerer:       registry,
   104  		DefaultTimerType: HistogramTimerType,
   105  		DefaultHistogramBuckets: []float64{
   106  			50 * ms,
   107  			250 * ms,
   108  			1000 * ms,
   109  			2500 * ms,
   110  			10000 * ms,
   111  		},
   112  	})
   113  
   114  	name := "test_timer"
   115  	tags := map[string]string{
   116  		"foo":  "bar",
   117  		"test": "everything",
   118  	}
   119  	tags2 := map[string]string{
   120  		"foo":  "baz",
   121  		"test": "something",
   122  	}
   123  	vals := []time.Duration{
   124  		23 * time.Millisecond,
   125  		223 * time.Millisecond,
   126  		320 * time.Millisecond,
   127  	}
   128  	vals2 := []time.Duration{
   129  		1742 * time.Millisecond,
   130  		3232 * time.Millisecond,
   131  	}
   132  
   133  	timer := r.AllocateTimer(name, tags)
   134  	for _, v := range vals {
   135  		timer.ReportTimer(v)
   136  	}
   137  
   138  	timer = r.AllocateTimer(name, tags2)
   139  	for _, v := range vals2 {
   140  		timer.ReportTimer(v)
   141  	}
   142  
   143  	assertMetric(t, gather(t, registry), metric{
   144  		name:  name,
   145  		mtype: dto.MetricType_HISTOGRAM,
   146  		instances: []instance{
   147  			{
   148  				labels: tags,
   149  				histogram: histogramValue(histogramVal{
   150  					sampleCount: uint64(len(vals)),
   151  					sampleSum:   durationFloatSum(vals),
   152  					buckets: []histogramValBucket{
   153  						{upperBound: 0.05, count: 1},
   154  						{upperBound: 0.25, count: 2},
   155  						{upperBound: 1.00, count: 3},
   156  						{upperBound: 2.50, count: 3},
   157  						{upperBound: 10.00, count: 3},
   158  					},
   159  				}),
   160  			},
   161  			{
   162  				labels: tags2,
   163  				histogram: histogramValue(histogramVal{
   164  					sampleCount: uint64(len(vals2)),
   165  					sampleSum:   durationFloatSum(vals2),
   166  					buckets: []histogramValBucket{
   167  						{upperBound: 0.05, count: 0},
   168  						{upperBound: 0.25, count: 0},
   169  						{upperBound: 1.00, count: 0},
   170  						{upperBound: 2.50, count: 1},
   171  						{upperBound: 10.00, count: 2},
   172  					},
   173  				}),
   174  			},
   175  		},
   176  	})
   177  }
   178  
   179  func TestTimerSummary(t *testing.T) {
   180  	registry := prom.NewRegistry()
   181  	r := NewReporter(Options{
   182  		Registerer:       registry,
   183  		DefaultTimerType: SummaryTimerType,
   184  		DefaultSummaryObjectives: map[float64]float64{
   185  			0.5:   0.01,
   186  			0.75:  0.001,
   187  			0.95:  0.001,
   188  			0.99:  0.001,
   189  			0.999: 0.0001,
   190  		},
   191  	})
   192  
   193  	name := "test_timer"
   194  	tags := map[string]string{
   195  		"foo":  "bar",
   196  		"test": "everything",
   197  	}
   198  	tags2 := map[string]string{
   199  		"foo":  "baz",
   200  		"test": "something",
   201  	}
   202  	vals := []time.Duration{
   203  		23 * time.Millisecond,
   204  		223 * time.Millisecond,
   205  		320 * time.Millisecond,
   206  	}
   207  	vals2 := []time.Duration{
   208  		1742 * time.Millisecond,
   209  		3232 * time.Millisecond,
   210  	}
   211  
   212  	timer := r.AllocateTimer(name, tags)
   213  	for _, v := range vals {
   214  		timer.ReportTimer(v)
   215  	}
   216  
   217  	timer = r.AllocateTimer(name, tags2)
   218  	for _, v := range vals2 {
   219  		timer.ReportTimer(v)
   220  	}
   221  
   222  	assertMetric(t, gather(t, registry), metric{
   223  		name:  name,
   224  		mtype: dto.MetricType_SUMMARY,
   225  		instances: []instance{
   226  			{
   227  				labels: tags,
   228  				summary: summaryValue(summaryVal{
   229  					sampleCount: uint64(len(vals)),
   230  					sampleSum:   durationFloatSum(vals),
   231  					quantiles: []summaryValQuantile{
   232  						{quantile: 0.50, value: 0.223},
   233  						{quantile: 0.75, value: 0.32},
   234  						{quantile: 0.95, value: 0.32},
   235  						{quantile: 0.99, value: 0.32},
   236  						{quantile: 0.999, value: 0.32},
   237  					},
   238  				}),
   239  			},
   240  			{
   241  				labels: tags2,
   242  				summary: summaryValue(summaryVal{
   243  					sampleCount: uint64(len(vals2)),
   244  					sampleSum:   durationFloatSum(vals2),
   245  					quantiles: []summaryValQuantile{
   246  						{quantile: 0.50, value: 1.742},
   247  						{quantile: 0.75, value: 3.232},
   248  						{quantile: 0.95, value: 3.232},
   249  						{quantile: 0.99, value: 3.232},
   250  						{quantile: 0.999, value: 3.232},
   251  					},
   252  				}),
   253  			},
   254  		},
   255  	})
   256  }
   257  
   258  func TestHistogramBucketValues(t *testing.T) {
   259  	registry := prom.NewRegistry()
   260  	r := NewReporter(Options{
   261  		Registerer: registry,
   262  	})
   263  
   264  	buckets := tally.DurationBuckets{
   265  		0 * time.Millisecond,
   266  		50 * time.Millisecond,
   267  		250 * time.Millisecond,
   268  		1000 * time.Millisecond,
   269  		2500 * time.Millisecond,
   270  		10000 * time.Millisecond,
   271  	}
   272  
   273  	name := "test_histogram"
   274  	tags := map[string]string{
   275  		"foo":  "bar",
   276  		"test": "everything",
   277  	}
   278  	tags2 := map[string]string{
   279  		"foo":  "baz",
   280  		"test": "something",
   281  	}
   282  	vals := []time.Duration{
   283  		23 * time.Millisecond,
   284  		223 * time.Millisecond,
   285  		320 * time.Millisecond,
   286  	}
   287  	vals2 := []time.Duration{
   288  		1742 * time.Millisecond,
   289  		3232 * time.Millisecond,
   290  	}
   291  
   292  	histogram := r.AllocateHistogram(name, tags, buckets)
   293  	histogram.DurationBucket(0, 50*time.Millisecond).ReportSamples(1)
   294  	histogram.DurationBucket(0, 250*time.Millisecond).ReportSamples(1)
   295  	histogram.DurationBucket(0, 1000*time.Millisecond).ReportSamples(1)
   296  
   297  	histogram = r.AllocateHistogram(name, tags2, buckets)
   298  	histogram.DurationBucket(0, 2500*time.Millisecond).ReportSamples(1)
   299  	histogram.DurationBucket(0, 10000*time.Millisecond).ReportSamples(1)
   300  
   301  	assertMetric(t, gather(t, registry), metric{
   302  		name:  name,
   303  		mtype: dto.MetricType_HISTOGRAM,
   304  		instances: []instance{
   305  			{
   306  				labels: tags,
   307  				histogram: histogramValue(histogramVal{
   308  					sampleCount: uint64(len(vals)),
   309  					sampleSum:   1.3,
   310  					buckets: []histogramValBucket{
   311  						{upperBound: 0.00, count: 0},
   312  						{upperBound: 0.05, count: 1},
   313  						{upperBound: 0.25, count: 2},
   314  						{upperBound: 1.00, count: 3},
   315  						{upperBound: 2.50, count: 3},
   316  						{upperBound: 10.00, count: 3},
   317  					},
   318  				}),
   319  			},
   320  			{
   321  				labels: tags2,
   322  				histogram: histogramValue(histogramVal{
   323  					sampleCount: uint64(len(vals2)),
   324  					sampleSum:   12.5,
   325  					buckets: []histogramValBucket{
   326  						{upperBound: 0.00, count: 0},
   327  						{upperBound: 0.05, count: 0},
   328  						{upperBound: 0.25, count: 0},
   329  						{upperBound: 1.00, count: 0},
   330  						{upperBound: 2.50, count: 1},
   331  						{upperBound: 10.00, count: 2},
   332  					},
   333  				}),
   334  			},
   335  		},
   336  	})
   337  }
   338  
   339  func TestOnRegisterError(t *testing.T) {
   340  	var captured []error
   341  
   342  	registry := prom.NewRegistry()
   343  	r := NewReporter(Options{
   344  		Registerer: registry,
   345  		OnRegisterError: func(err error) {
   346  			captured = append(captured, err)
   347  		},
   348  	})
   349  
   350  	c := r.AllocateCounter("bad-name", nil)
   351  	c.ReportCount(2)
   352  	c.ReportCount(4)
   353  	c = r.AllocateCounter("bad.name", nil)
   354  	c.ReportCount(42)
   355  	c.ReportCount(84)
   356  
   357  	assert.Equal(t, 2, len(captured))
   358  }
   359  
   360  func TestAlreadyRegisteredCounter(t *testing.T) {
   361  	var captured []error
   362  
   363  	registry := prom.NewRegistry()
   364  	r := NewReporter(Options{
   365  		Registerer: registry,
   366  		OnRegisterError: func(err error) {
   367  			captured = append(captured, err)
   368  		},
   369  	})
   370  
   371  	// n.b. Prometheus metrics are different from M3 metrics in that they are
   372  	//      uniquely identified as "metric_name+label_name+label_name+...";
   373  	//      additionally, given that Prometheus ingestion is pull-based, there
   374  	//      is only ever one reporter used regardless of tally.Scope hierarchy.
   375  	//
   376  	//      Because of this, for a given metric "foo", only the first-registered
   377  	//      permutation of metric and label names will succeed because the same
   378  	//      registry is being used, and because Prometheus asserts that a
   379  	//      registered metric name has the same corresponding label names.
   380  	//      Subsequent registrations - such as adding or removing tags - will
   381  	//      return an error.
   382  	//
   383  	//      This is a problem because Tally's API does not apply semantics or
   384  	//      restrictions to the combinatorics (or descendant mutations of)
   385  	//      metric tags. As such, we must assert that Tally's Prometheus
   386  	//      reporter does the right thing and indicates an error when this
   387  	//      happens.
   388  	//
   389  	// The first allocation call will succeed. This establishes the required
   390  	// label names (["foo"]) for metric "foo".
   391  	r.AllocateCounter("foo", map[string]string{"foo": "bar"})
   392  
   393  	// The second allocation call is okay, as it has the same label names (["foo"]).
   394  	r.AllocateCounter("foo", map[string]string{"foo": "baz"})
   395  
   396  	// The third allocation call fails, because it has different label names
   397  	// (["bar"], vs previously ["foo"]) for the same metric name "foo".
   398  	r.AllocateCounter("foo", map[string]string{"bar": "qux"})
   399  
   400  	// The fourth allocation call fails, because while it has one of the same
   401  	// label names ("foo") as was previously registered for metric "foo", it
   402  	// also has an additional label name (["foo", "zork"] != ["foo"]).
   403  	r.AllocateCounter("foo", map[string]string{
   404  		"foo":  "bar",
   405  		"zork": "derp",
   406  	})
   407  
   408  	// The fifth allocation call fails, because it has no label names for the
   409  	// metric "foo", which expects the label names it was originally registered
   410  	// with (["foo"]).
   411  	r.AllocateCounter("foo", nil)
   412  
   413  	require.Equal(t, 3, len(captured))
   414  	for _, err := range captured {
   415  		require.Contains(t, err.Error(), "same fully-qualified name")
   416  	}
   417  }
   418  
   419  func gather(t *testing.T, r prom.Gatherer) []*dto.MetricFamily {
   420  	metrics, err := r.Gather()
   421  	require.NoError(t, err)
   422  	return metrics
   423  }
   424  
   425  func counterValue(v float64) *dto.Counter {
   426  	return &dto.Counter{Value: &v}
   427  }
   428  
   429  func gaugeValue(v float64) *dto.Gauge {
   430  	return &dto.Gauge{Value: &v}
   431  }
   432  
   433  type histogramVal struct {
   434  	sampleCount uint64
   435  	sampleSum   float64
   436  	buckets     []histogramValBucket
   437  }
   438  
   439  type histogramValBucket struct {
   440  	count      uint64
   441  	upperBound float64
   442  }
   443  
   444  func histogramValue(v histogramVal) *dto.Histogram {
   445  	r := &dto.Histogram{
   446  		SampleCount: &v.sampleCount,
   447  		SampleSum:   &v.sampleSum,
   448  	}
   449  	for _, b := range v.buckets {
   450  		b := b // or else the addresses we take will be static
   451  		r.Bucket = append(r.Bucket, &dto.Bucket{
   452  			CumulativeCount: &b.count,
   453  			UpperBound:      &b.upperBound,
   454  		})
   455  	}
   456  	return r
   457  }
   458  
   459  type summaryVal struct {
   460  	sampleCount uint64
   461  	sampleSum   float64
   462  	quantiles   []summaryValQuantile
   463  }
   464  
   465  type summaryValQuantile struct {
   466  	quantile float64
   467  	value    float64
   468  }
   469  
   470  func summaryValue(v summaryVal) *dto.Summary {
   471  	r := &dto.Summary{
   472  		SampleCount: &v.sampleCount,
   473  		SampleSum:   &v.sampleSum,
   474  	}
   475  	for _, q := range v.quantiles {
   476  		q := q // or else the addresses we take will be static
   477  		r.Quantile = append(r.Quantile, &dto.Quantile{
   478  			Quantile: &q.quantile,
   479  			Value:    &q.value,
   480  		})
   481  	}
   482  	return r
   483  }
   484  
   485  func durationFloatSum(v []time.Duration) float64 {
   486  	var sum float64
   487  	for _, d := range v {
   488  		sum += durationFloat(d)
   489  	}
   490  	return sum
   491  }
   492  
   493  func durationFloat(d time.Duration) float64 {
   494  	return float64(d) / float64(time.Second)
   495  }
   496  
   497  type metric struct {
   498  	name      string
   499  	mtype     dto.MetricType
   500  	instances []instance
   501  }
   502  
   503  type instance struct {
   504  	labels    map[string]string
   505  	counter   *dto.Counter
   506  	gauge     *dto.Gauge
   507  	histogram *dto.Histogram
   508  	summary   *dto.Summary
   509  }
   510  
   511  func assertMetric(
   512  	t *testing.T,
   513  	metrics []*dto.MetricFamily,
   514  	query metric,
   515  ) {
   516  	q := query
   517  	msgFmt := func(msg string, v ...interface{}) string {
   518  		prefix := fmt.Sprintf("assert fail for metric name=%s, type=%s: ",
   519  			q.name, q.mtype.String())
   520  		return fmt.Sprintf(prefix+msg, v...)
   521  	}
   522  	for _, m := range metrics {
   523  		if m.GetName() != q.name || m.GetType() != q.mtype {
   524  			continue
   525  		}
   526  		if len(q.instances) == 0 {
   527  			require.Fail(t, msgFmt("no instances to assert"))
   528  		}
   529  		for _, i := range q.instances {
   530  			found := false
   531  			for _, j := range m.GetMetric() {
   532  				if len(i.labels) != len(j.GetLabel()) {
   533  					continue
   534  				}
   535  
   536  				notMatched := make(map[string]string, len(i.labels))
   537  				for k, v := range i.labels {
   538  					notMatched[k] = v
   539  				}
   540  
   541  				for _, pair := range j.GetLabel() {
   542  					notMatchedValue, matches := notMatched[pair.GetName()]
   543  					if matches && pair.GetValue() == notMatchedValue {
   544  						delete(notMatched, pair.GetName())
   545  					}
   546  				}
   547  
   548  				if len(notMatched) != 0 {
   549  					continue
   550  				}
   551  
   552  				found = true
   553  
   554  				switch {
   555  				case i.counter != nil:
   556  					require.NotNil(t, j.GetCounter())
   557  					assert.Equal(t, i.counter.GetValue(), j.GetCounter().GetValue())
   558  				case i.gauge != nil:
   559  					require.NotNil(t, j.GetGauge())
   560  					assert.Equal(t, i.gauge.GetValue(), j.GetGauge().GetValue())
   561  				case i.histogram != nil:
   562  					require.NotNil(t, j.GetHistogram())
   563  					assert.Equal(t, i.histogram.GetSampleCount(), j.GetHistogram().GetSampleCount())
   564  					assert.Equal(t, i.histogram.GetSampleSum(), j.GetHistogram().GetSampleSum())
   565  					require.Equal(t, len(i.histogram.GetBucket()), len(j.GetHistogram().GetBucket()))
   566  					for idx, b := range i.histogram.GetBucket() {
   567  						actual := j.GetHistogram().GetBucket()[idx]
   568  						assert.Equal(t, b.GetCumulativeCount(), actual.GetCumulativeCount())
   569  						assert.Equal(t, b.GetUpperBound(), actual.GetUpperBound())
   570  					}
   571  				case i.summary != nil:
   572  					require.NotNil(t, j.GetSummary())
   573  					assert.Equal(t, i.summary.GetSampleCount(), j.GetSummary().GetSampleCount())
   574  					assert.Equal(t, i.summary.GetSampleSum(), j.GetSummary().GetSampleSum())
   575  					require.Equal(t, len(i.summary.GetQuantile()), len(j.GetSummary().GetQuantile()))
   576  					for idx, q := range i.summary.GetQuantile() {
   577  						actual := j.GetSummary().GetQuantile()[idx]
   578  						assert.Equal(t, q.GetQuantile(), actual.GetQuantile())
   579  						assert.Equal(t, q.GetValue(), actual.GetValue())
   580  					}
   581  				}
   582  			}
   583  			if !found {
   584  				require.Fail(t, msgFmt("instance not found labels=%v", i.labels))
   585  			}
   586  		}
   587  		return
   588  	}
   589  	require.Fail(t, msgFmt("metric not found"))
   590  }