...

Source file src/edge-infra.dev/pkg/lib/promassert/assert.go

Documentation: edge-infra.dev/pkg/lib/promassert

     1  package promassert
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"math"
     7  	"regexp"
     8  	"testing"
     9  
    10  	promi "github.com/prometheus/client_model/go"
    11  	"github.com/prometheus/common/expfmt"
    12  )
    13  
    14  // Verbose is provided as a var so that logging can be turned on/off explicitly.
    15  var Verbose = true
    16  
    17  // ErrEmptyMetric is returned whenever an function expects a non-empty metric.
    18  var ErrEmptyMetric = errors.New("metric is empty")
    19  
    20  // ParsedMetrics contains all the combinations of metric labels/values associated with a named Counter or Gauge.
    21  type ParsedMetrics []ParsedMetric
    22  
    23  // ParsedMetric represents an individual line within the prometheus metrics exposition format for Counters and Gauges.
    24  type ParsedMetric struct {
    25  	Value  float64
    26  	Labels map[string]string
    27  }
    28  
    29  // Counter parses the prometheus exposition format scraped from promhttp.Handler(),
    30  // and gathers all of the metrics data that matches the provided name.
    31  //
    32  // This function panics if the metric is not a Counter. Your test will fail, so this is a good thing.
    33  //
    34  // Counter(...) returns an empty slice if the metric is not present in the scrape data.
    35  //
    36  // NOTE: Counter metrics must be set using the `Add(...)`, or `Inc()` functions in order for it to be found.
    37  // Declaring a metric using `prometheus.NewCounter(...)` and `prometheus.NewCounterVec(...)` does not initialize the metric.
    38  func Counter(name string) ParsedMetrics {
    39  	return parseMetric(name, promi.MetricType_COUNTER)
    40  }
    41  
    42  // Gauge parses the prometheus exposition format scraped from promhttp.Handler(),
    43  // and gathers all of the metrics data that matches the provided name.
    44  //
    45  // This function panics if the metric is not a Gauge. Your test will fail, so this is a good thing.
    46  //
    47  // Gauge(...) returns an empty slice if the metric was not found.
    48  //
    49  // NOTE: Gauge metrics must be set using the `Add(...)`, `Set(...)` or `Inc()` functions in order for it to be found.
    50  // Declaring a metric using `prometheus.NewGauge(...)` and `prometheus.NewGaugeVec(...)` does not initialize the metric.
    51  func Gauge(name string) ParsedMetrics {
    52  	return parseMetric(name, promi.MetricType_GAUGE)
    53  }
    54  
    55  // parseMetric parses the prometheus exposition format scraped from promhttp.Handler(), and gathers all of the metrics data that matches the provided name.
    56  //
    57  // NOTE: This function only works with Counters and Gauges, and panics if the metric is any other metric type.
    58  func parseMetric(name string, mtype promi.MetricType) ParsedMetrics {
    59  	var ret []ParsedMetric
    60  
    61  	var prombody, err = ScrapePrometheusMetrics()
    62  	if err != nil {
    63  		panic(fmt.Sprintf("got error when scraping metrics: %v", err))
    64  	}
    65  
    66  	var tp expfmt.TextParser
    67  	mf, err := tp.TextToMetricFamilies(prombody)
    68  	if err != nil {
    69  		// Should not happen, but need to know if it does.
    70  		panic(fmt.Sprintf("could not parse metrics from prometheus handler: %v", err))
    71  	}
    72  
    73  	metf, found := mf[name]
    74  	if !found {
    75  		// Exit early and return the empty set.
    76  		// The assert will fail later, or pass if they are asserting empty!
    77  		return ret
    78  	} else if mtype != metf.GetType() {
    79  		panic(fmt.Sprintf("expected metric %q to be type %q but got type %q", name, mtype, metf.GetType()))
    80  	}
    81  
    82  	for _, met := range metf.GetMetric() {
    83  		var pm = ParsedMetric{Labels: make(map[string]string)}
    84  		switch mtype {
    85  		case promi.MetricType_COUNTER:
    86  			pm.Value = met.GetCounter().GetValue()
    87  		case promi.MetricType_GAUGE:
    88  			pm.Value = met.GetGauge().GetValue()
    89  		}
    90  		// metrics can omit labels
    91  		for _, lp := range met.GetLabel() {
    92  			pm.Labels[lp.GetName()] = lp.GetValue()
    93  		}
    94  		ret = append(ret, pm)
    95  	}
    96  	return ret
    97  }
    98  
    99  var ErrFoldedNaN = errors.New("folded float values cannot contain NaN")
   100  var ErrFoldedInf = errors.New("folded float values cannot contain Inf")
   101  
   102  // Fold combines the values of the parsed metric and returns the sum.
   103  //
   104  // If the metric is empty, then fold returns ErrEmptyMetric.
   105  //
   106  // If the metric contains NaN or Inf floats, then ErrFoldedNan or ErrFoldedInf is returned.
   107  //
   108  // Because float operations are imprecise, it's recommended to only fold metrics whose values represent integers.
   109  func (p ParsedMetrics) Fold() (float64, error) {
   110  	if len(p) == 0 {
   111  		return 0, ErrEmptyMetric
   112  	}
   113  	var sum float64
   114  	for _, pm := range p {
   115  		if math.IsNaN(pm.Value) {
   116  			return 0, ErrFoldedNaN
   117  		} else if math.IsInf(pm.Value, 0) {
   118  			return 0, ErrFoldedInf
   119  		}
   120  		sum += pm.Value
   121  	}
   122  	return sum, nil
   123  }
   124  
   125  // TryFold returns the folded value, or `0` if p.Fold returned an error.
   126  func (p ParsedMetrics) TryFold() float64 {
   127  	value, err := p.Fold()
   128  	if err != nil {
   129  		return 0
   130  	}
   131  	return value
   132  }
   133  
   134  // IsNaN asserts whether the values within `p` are NaN.
   135  //
   136  // This assert fails if `p` is empty.
   137  func (p ParsedMetrics) IsNaN(t *testing.T) bool {
   138  	if len(p) == 0 {
   139  		if t != nil {
   140  			t.Error(ErrEmptyMetric)
   141  		}
   142  		return false
   143  	}
   144  	var passed = true
   145  	for _, pm := range p {
   146  		if !math.IsNaN(pm.Value) {
   147  			passed = false
   148  			if t != nil {
   149  				t.Errorf("value %f is not NaN", pm.Value)
   150  			}
   151  		}
   152  	}
   153  	if t != nil && Verbose && passed {
   154  		t.Logf("assertion passed: the values of the metric are NaN")
   155  	}
   156  	return passed
   157  }
   158  
   159  // IsInf asserts whether the values within `p` are infinity, according to sign.
   160  // If sign > 0, IsInf asserts whether the values in `p` are positive infinity.
   161  // If sign < 0, IsInf asserts whether the values within `p` are negative infinity.
   162  // If sign == 0, IsInf asserts whether the values in `p` are either infinity.
   163  //
   164  // This assert fails if `p` is empty.
   165  func (p ParsedMetrics) IsInf(t *testing.T, sign int) bool {
   166  	if len(p) == 0 {
   167  		if t != nil {
   168  			t.Error(ErrEmptyMetric)
   169  		}
   170  		return false
   171  	}
   172  
   173  	// Used for pretty printing the sign.
   174  	var s string
   175  	if sign > 0 {
   176  		s = "+"
   177  	} else if sign < 0 {
   178  		s = "-"
   179  	}
   180  
   181  	var passed = true
   182  	for _, pm := range p {
   183  		if !math.IsInf(pm.Value, sign) {
   184  			passed = false
   185  			if t != nil {
   186  				t.Errorf("value %f is not %sInf", pm.Value, s)
   187  			}
   188  		}
   189  	}
   190  	if t != nil && Verbose && passed {
   191  		t.Logf("assertion passed: the values of the metric are %sInf", s)
   192  	}
   193  	return passed
   194  }
   195  
   196  // Exists asserts the ParsedMetrics are not empty, aka the metric exists, or the filtered labels were found.
   197  //
   198  //	// checks if the "foo_total" metric exists in the scraped prometheus exposition format.
   199  //	promassert.Metric("foo_total").Exists(t)
   200  //
   201  //	// checks if "foo_total" contains the label "bar" with the value "baz"
   202  //	var labels = map[string]string{"bar":"baz"}
   203  //	promassert.Metric("foo_total").With(labels).Exists(t)
   204  func (p ParsedMetrics) Exists(t *testing.T) bool {
   205  	if len(p) != 0 {
   206  		if t != nil && Verbose {
   207  			t.Logf("assertion passed. the metric exists")
   208  		}
   209  		return true
   210  	}
   211  
   212  	if t != nil {
   213  		t.Errorf("the metric does not exist")
   214  	}
   215  	return false
   216  }
   217  
   218  // NotExists asserts the ParsedMetrics are empty, aka the metric does not exist, or the filtered labels do not exist.
   219  func (p ParsedMetrics) NotExists(t *testing.T) bool {
   220  	if len(p) == 0 {
   221  		if t != nil && Verbose {
   222  			t.Logf("assertion passed: the metric does not exist")
   223  		}
   224  		return true
   225  	}
   226  
   227  	if t != nil {
   228  		t.Errorf("the metric exists")
   229  	}
   230  	return false
   231  }
   232  
   233  // Equals asserts the folded value of metrics within `p`. See the #Fold method for more information on folding.
   234  //
   235  // If the metrics are empty, then the assert fails
   236  func (p ParsedMetrics) Equals(t *testing.T, total float64) bool {
   237  	result, err := p.Fold()
   238  	if err != nil {
   239  		if t != nil {
   240  			t.Error(err)
   241  		}
   242  		return false
   243  	} else if result != total {
   244  		if t != nil {
   245  			t.Errorf("folded metric value %f does not equal expected total %f", result, total)
   246  		}
   247  		return false
   248  	}
   249  
   250  	if t != nil && Verbose {
   251  		t.Logf("assertion passed. folded metric value %f equals expected total %f", result, total)
   252  	}
   253  	return true
   254  }
   255  
   256  // NotEquals asserts the folded value of metrics within `p`. See the #Fold method for more information on folding.
   257  //
   258  // If the metrics are empty, then the assert fails
   259  func (p ParsedMetrics) NotEquals(t *testing.T, total float64) bool {
   260  	result, err := p.Fold()
   261  	if err != nil {
   262  		if t != nil {
   263  			t.Error(err)
   264  		}
   265  		return false
   266  	} else if result == total {
   267  		if t != nil {
   268  			t.Errorf("folded metric value %f equals provided total %f", result, total)
   269  		}
   270  		return false
   271  	}
   272  
   273  	if t != nil && Verbose {
   274  		t.Logf("assertion passed. folded metric value %f does not equal provided total %f", result, total)
   275  	}
   276  	return true
   277  }
   278  
   279  // GreaterThan asserts the folded value of metrics within `p`. See the #Fold method for more information on folding.
   280  //
   281  // If the metrics are empty, then the assert fails
   282  func (p ParsedMetrics) GreaterThan(t *testing.T, total float64) bool {
   283  	result, err := p.Fold()
   284  	if err != nil {
   285  		if t != nil {
   286  			t.Error(err)
   287  		}
   288  		return false
   289  	} else if result <= total {
   290  		if t != nil {
   291  			t.Errorf("folded metric value %f is not greater than expected total %f", result, total)
   292  		}
   293  		return false
   294  	}
   295  
   296  	if t != nil && Verbose {
   297  		t.Logf("assertion passed. folded metric value %f is greater than provided total %f", result, total)
   298  	}
   299  	return true
   300  }
   301  
   302  // GreaterThanOrEquals asserts the folded value of metrics within `p`. See the #Fold method for more information on folding.
   303  //
   304  // If the metrics are empty, then the assert fails
   305  func (p ParsedMetrics) GreaterThanOrEquals(t *testing.T, total float64) bool {
   306  	result, err := p.Fold()
   307  	if err != nil {
   308  		if t != nil {
   309  			t.Error(err)
   310  		}
   311  		return false
   312  	} else if result < total {
   313  		if t != nil {
   314  			t.Errorf("folded metric value %f is not greater than or equal to expected total %f", result, total)
   315  		}
   316  		return false
   317  	}
   318  
   319  	if t != nil && Verbose {
   320  		t.Logf("assertion passed. folded metric value %f is greater than or equal to provided total %f", result, total)
   321  	}
   322  	return true
   323  }
   324  
   325  // LessThan asserts the folded value of metrics within `p`. See the #Fold method for more information on folding.
   326  //
   327  // If the metrics are empty, then the assert fails
   328  func (p ParsedMetrics) LessThan(t *testing.T, total float64) bool {
   329  	result, err := p.Fold()
   330  	if err != nil {
   331  		if t != nil {
   332  			t.Error(err)
   333  		}
   334  		return false
   335  	} else if result >= total {
   336  		if t != nil {
   337  			t.Errorf("folded metric value %f is not less than expected total %f", result, total)
   338  		}
   339  		return false
   340  	}
   341  
   342  	if t != nil && Verbose {
   343  		t.Logf("assertion passed. folded metric value %f is less than provided total %f", result, total)
   344  	}
   345  	return true
   346  }
   347  
   348  // LessThanOrEquals asserts the folded value of metrics within `p`. See the #Fold method for more information on folding.
   349  //
   350  // If the metrics are empty, then the assert fails
   351  func (p ParsedMetrics) LessThanOrEquals(t *testing.T, total float64) bool {
   352  	result, err := p.Fold()
   353  	if err != nil {
   354  		if t != nil {
   355  			t.Error(err)
   356  		}
   357  		return false
   358  	} else if result > total {
   359  		if t != nil {
   360  			t.Errorf("folded metric value %f is not less than or equal to expected total %f", result, total)
   361  		}
   362  		return false
   363  	}
   364  
   365  	if t != nil && Verbose {
   366  		t.Logf("assertion passed. folded metric value %f is less than or equal to provided total %f", result, total)
   367  	}
   368  	return true
   369  }
   370  
   371  // LabelKeysExist is used to assert the metric contains each of the provided label names.
   372  func (p ParsedMetrics) LabelKeysExist(t *testing.T, keys ...string) bool {
   373  	if len(p) == 0 {
   374  		if t != nil {
   375  			t.Error(ErrEmptyMetric)
   376  		}
   377  		return false
   378  	}
   379  	var missing = make(map[string]bool)
   380  	for _, pm := range p {
   381  		for _, k := range keys {
   382  			if _, found := pm.Labels[k]; !found {
   383  				missing[k] = true
   384  			}
   385  		}
   386  	}
   387  	if len(missing) != 0 {
   388  		// mk is used to pretty print the missing keys.
   389  		var mk []string
   390  		for k := range missing {
   391  			mk = append(mk, k)
   392  		}
   393  		if t != nil {
   394  			t.Errorf("the metric is missing the following keys: %v", mk)
   395  		}
   396  	} else if t != nil && Verbose {
   397  		t.Logf("assertion passed: the keys %v are present in the metric", keys)
   398  	}
   399  	return len(missing) == 0
   400  }
   401  
   402  // WithRationalValues drops any parsed metric value containing Nan, +Inf, or -Inf.
   403  func (p ParsedMetrics) WithRationalValues() ParsedMetrics {
   404  	var filtered []ParsedMetric
   405  	for _, pm := range p {
   406  		if !math.IsNaN(pm.Value) && !math.IsInf(pm.Value, 0) {
   407  			filtered = append(filtered, pm)
   408  		}
   409  	}
   410  	return filtered
   411  }
   412  
   413  // With filters the metric by matching the keys/values provided in `l`.
   414  //
   415  // If none match, the empty set is returned.
   416  func (p ParsedMetrics) With(labels map[string]string) ParsedMetrics {
   417  	var filtered []ParsedMetric
   418  	for _, pm := range p {
   419  		var didNotMatch bool
   420  		for k, v := range labels {
   421  			if pmv, found := pm.Labels[k]; !found || pmv != v {
   422  				didNotMatch = true
   423  				break
   424  			}
   425  		}
   426  		if !didNotMatch {
   427  			filtered = append(filtered, pm)
   428  		}
   429  	}
   430  	return filtered
   431  }
   432  
   433  // WithValues is like With, except you can provide more than one label value to match against.
   434  //
   435  // WithValues panics when a label's value slice is empty.
   436  //
   437  // WithValues is useful when checking specific HTTP status codes, and/or methods
   438  //
   439  //	var lvs = map[string][]string{
   440  //	    "code":   []string{"200", "204"},
   441  //	    "method": []string{"GET", "POST"},
   442  //	}
   443  //	promassert.Metric("requests_total").WithValues(lvs).Equals(t, 42)
   444  func (p ParsedMetrics) WithValues(labelValues map[string][]string) ParsedMetrics {
   445  	var filtered []ParsedMetric
   446  
   447  	// Convert input to map for quicker asymptotic lookup.
   448  	var lm = make(map[string]map[string]bool)
   449  	for k, vs := range labelValues {
   450  		if 0 == len(vs) {
   451  			panic("the label values must not be an empty slice")
   452  		}
   453  		lm[k] = make(map[string]bool)
   454  		for _, v := range vs {
   455  			lm[k][v] = true
   456  		}
   457  	}
   458  
   459  	for _, pm := range p {
   460  		var didNotMatch bool
   461  		for k, vm := range lm {
   462  			if pmv, found := pm.Labels[k]; !found || !vm[pmv] {
   463  				didNotMatch = true
   464  				break
   465  			}
   466  		}
   467  		if !didNotMatch {
   468  			filtered = append(filtered, pm)
   469  		}
   470  	}
   471  	return filtered
   472  }
   473  
   474  // WithRegexp filters the metric by matching the key and then checking if the `lrxp[key].MatchString(...)` function passes.
   475  //
   476  // WithRegexp is useful when testing highly orthogonal labels, such as hostnames, clusters, pods, etc.
   477  //
   478  //	var mrxp = map[string]*regexp.Regexp{
   479  //	    "hostname": regexp.MustCompile("foo-[0-9]+[.]local"),
   480  //	}
   481  //	promassert.Metric("requests_total").WithRegexp(mrxp).Equals(t, 3)
   482  func (p ParsedMetrics) WithRegexp(lrxp map[string]*regexp.Regexp) ParsedMetrics {
   483  	var filtered []ParsedMetric
   484  	for _, pm := range p {
   485  		var didNotMatch bool
   486  		for k, rxpv := range lrxp {
   487  			if pmv, found := pm.Labels[k]; !found || !rxpv.MatchString(pmv) {
   488  				didNotMatch = true
   489  				break
   490  			}
   491  		}
   492  		if !didNotMatch {
   493  			filtered = append(filtered, pm)
   494  		}
   495  	}
   496  	return filtered
   497  }
   498  

View as plain text