...

Source file src/github.com/linkerd/linkerd2/testutil/prommatch/prommatch.go

Documentation: github.com/linkerd/linkerd2/testutil/prommatch

     1  // Package prommatch provides means of checking whether a prometheus metrics
     2  // contain a specific series.
     3  //
     4  // It tries to give a similar look and feel as time series in PromQL.
     5  // So where in PromQL one would write this:
     6  //
     7  //	request_total{direction="outbound", target_port=~"8\d\d\d"} 30
     8  //
     9  // In prommatch, one can write this:
    10  //
    11  //	portRE := regex.MustCompile(`^8\d\d\d$`)
    12  //	prommatch.NewMatcher("request_total", prommatch.Labels{
    13  //		"direction": prommatch.Equals("outbound"),
    14  //		"target_port": prommatch.Like(portRE),
    15  //	})
    16  package prommatch
    17  
    18  import (
    19  	"bytes"
    20  	"fmt"
    21  	"net"
    22  	"net/netip"
    23  	"regexp"
    24  
    25  	dto "github.com/prometheus/client_model/go"
    26  	"github.com/prometheus/common/expfmt"
    27  	"github.com/prometheus/common/model"
    28  )
    29  
    30  // Expression can match or reject one time series.
    31  type Expression interface {
    32  	matches(sp *model.Sample) bool
    33  }
    34  
    35  type funcMatcher func(sp *model.Sample) bool
    36  
    37  func (fc funcMatcher) matches(sp *model.Sample) bool {
    38  	return fc(sp)
    39  }
    40  
    41  // NewMatcher will match series name (exactly) and all the additional matchers.
    42  func NewMatcher(name string, ms ...Expression) *Matcher {
    43  	return &Matcher{
    44  		expressions: append([]Expression{hasName(name)}, ms...),
    45  	}
    46  }
    47  
    48  // Matcher contains a list of expressions, which will be checked against each series.
    49  type Matcher struct {
    50  	expressions []Expression
    51  }
    52  
    53  // HasMatchInString will return:
    54  // - true, if the provided metrics have a series which matches all expressions,
    55  // - false, if none of the series matches,
    56  // - error, if the provided string is not valid Prometheus metrics,
    57  func (e *Matcher) HasMatchInString(s string) (bool, error) {
    58  	v, err := extractSamplesVectorFromString(s)
    59  	if err != nil {
    60  		return false, fmt.Errorf("failed to parse input string as vector of samples: %w", err)
    61  	}
    62  	return e.hasMatchInVector(v), nil
    63  }
    64  
    65  func (e *Matcher) hasMatchInVector(v model.Vector) bool {
    66  	for _, s := range v {
    67  		if e.sampleMatches(s) {
    68  			return true
    69  		}
    70  	}
    71  	return false
    72  }
    73  
    74  func (e *Matcher) sampleMatches(s *model.Sample) bool {
    75  	for _, m := range e.expressions {
    76  		if !m.matches(s) {
    77  			return false
    78  		}
    79  	}
    80  	return true
    81  }
    82  
    83  // LabelMatcher can match or reject a label's value.
    84  type LabelMatcher func(string) bool
    85  
    86  // Labels is used for selecting series with matching labels.
    87  type Labels map[string]LabelMatcher
    88  
    89  // Make sure Labels implement Expression.
    90  var _ Expression = Labels{}
    91  
    92  func (l Labels) matches(s *model.Sample) bool {
    93  	for k, m := range l {
    94  		labelValue := s.Metric[model.LabelName(k)]
    95  		if !m(string(labelValue)) {
    96  			return false
    97  		}
    98  	}
    99  	return true
   100  }
   101  
   102  // Equals is when you want label value to have an exact value.
   103  func Equals(expected string) LabelMatcher {
   104  	return func(s string) bool {
   105  		return expected == s
   106  	}
   107  }
   108  
   109  // Like is when you want label value to match a regular expression.
   110  func Like(re *regexp.Regexp) LabelMatcher {
   111  	return func(s string) bool {
   112  		return re.MatchString(s)
   113  	}
   114  }
   115  
   116  // Absent is when you want to match series MISSING a specific label.
   117  func Absent() LabelMatcher {
   118  	return func(s string) bool {
   119  		return s == ""
   120  	}
   121  }
   122  
   123  // Any is when you want to select a series which has a certain label, but don't care about the value.
   124  func Any() LabelMatcher {
   125  	return func(s string) bool {
   126  		return s != ""
   127  	}
   128  }
   129  
   130  // HasValueLike is used for selecting time series based on value.
   131  func HasValueLike(f func(float64) bool) Expression {
   132  	return funcMatcher(func(sp *model.Sample) bool {
   133  		return f(float64(sp.Value))
   134  	})
   135  }
   136  
   137  // HasValueOf is used for selecting time series based on a specific value.
   138  func HasValueOf(f float64) Expression {
   139  	return funcMatcher(func(sp *model.Sample) bool {
   140  		return f == float64(sp.Value)
   141  	})
   142  }
   143  
   144  // HasPositiveValue is used to select time series with a positive value.
   145  func HasPositiveValue() Expression {
   146  	return HasValueLike(func(f float64) bool {
   147  		return f > 0
   148  	})
   149  }
   150  
   151  // IsAddr is used to check if the value is an IP:port combo, where IP can be
   152  // an IPv4 or an IPv6
   153  func IsAddr() LabelMatcher {
   154  	return func(s string) bool {
   155  		if _, err := netip.ParseAddrPort(s); err != nil {
   156  			return false
   157  		}
   158  		return true
   159  	}
   160  }
   161  
   162  // IsIP use used to check if the value is an IPv4 or IPv6
   163  func IsIP() LabelMatcher {
   164  	return func(s string) bool {
   165  		return net.ParseIP(s) != nil
   166  	}
   167  }
   168  
   169  func hasName(metricName string) Expression {
   170  	return funcMatcher(func(sp *model.Sample) bool {
   171  		return sp.Metric[model.MetricNameLabel] == model.LabelValue(metricName)
   172  	})
   173  }
   174  
   175  func extractSamplesVectorFromString(s string) (model.Vector, error) {
   176  	bb := bytes.NewBufferString(s)
   177  
   178  	p := &expfmt.TextParser{}
   179  	metricFamilies, err := p.TextToMetricFamilies(bb)
   180  	if err != nil {
   181  		return nil, fmt.Errorf("failed to parse input as metrics: %w", err)
   182  	}
   183  	var mfs []*dto.MetricFamily
   184  	for _, m := range metricFamilies {
   185  		mfs = append(mfs, m)
   186  	}
   187  	v, err := expfmt.ExtractSamples(&expfmt.DecodeOptions{}, mfs...)
   188  	if err != nil {
   189  		return nil, fmt.Errorf("failed to extract samples from input: %w", err)
   190  	}
   191  	return v, nil
   192  }
   193  

View as plain text