...

Source file src/k8s.io/component-base/metrics/testutil/metrics.go

Documentation: k8s.io/component-base/metrics/testutil

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package testutil
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"math"
    23  	"reflect"
    24  	"sort"
    25  	"strings"
    26  
    27  	dto "github.com/prometheus/client_model/go"
    28  	"github.com/prometheus/common/expfmt"
    29  	"github.com/prometheus/common/model"
    30  
    31  	"k8s.io/component-base/metrics"
    32  )
    33  
    34  var (
    35  	// MetricNameLabel is label under which model.Sample stores metric name
    36  	MetricNameLabel model.LabelName = model.MetricNameLabel
    37  	// QuantileLabel is label under which model.Sample stores latency quantile value
    38  	QuantileLabel model.LabelName = model.QuantileLabel
    39  )
    40  
    41  // Metrics is generic metrics for other specific metrics
    42  type Metrics map[string]model.Samples
    43  
    44  // Equal returns true if all metrics are the same as the arguments.
    45  func (m *Metrics) Equal(o Metrics) bool {
    46  	var leftKeySet []string
    47  	var rightKeySet []string
    48  	for k := range *m {
    49  		leftKeySet = append(leftKeySet, k)
    50  	}
    51  	for k := range o {
    52  		rightKeySet = append(rightKeySet, k)
    53  	}
    54  	if !reflect.DeepEqual(leftKeySet, rightKeySet) {
    55  		return false
    56  	}
    57  	for _, k := range leftKeySet {
    58  		if !(*m)[k].Equal(o[k]) {
    59  			return false
    60  		}
    61  	}
    62  	return true
    63  }
    64  
    65  // NewMetrics returns new metrics which are initialized.
    66  func NewMetrics() Metrics {
    67  	result := make(Metrics)
    68  	return result
    69  }
    70  
    71  // ParseMetrics parses Metrics from data returned from prometheus endpoint
    72  func ParseMetrics(data string, output *Metrics) error {
    73  	dec := expfmt.NewDecoder(strings.NewReader(data), expfmt.FmtText)
    74  	decoder := expfmt.SampleDecoder{
    75  		Dec:  dec,
    76  		Opts: &expfmt.DecodeOptions{},
    77  	}
    78  
    79  	for {
    80  		var v model.Vector
    81  		if err := decoder.Decode(&v); err != nil {
    82  			if err == io.EOF {
    83  				// Expected loop termination condition.
    84  				return nil
    85  			}
    86  			continue
    87  		}
    88  		for _, metric := range v {
    89  			name := string(metric.Metric[MetricNameLabel])
    90  			(*output)[name] = append((*output)[name], metric)
    91  		}
    92  	}
    93  }
    94  
    95  // TextToMetricFamilies reads 'in' as the simple and flat text-based exchange
    96  // format and creates MetricFamily proto messages. It returns the MetricFamily
    97  // proto messages in a map where the metric names are the keys, along with any
    98  // error encountered.
    99  func TextToMetricFamilies(in io.Reader) (map[string]*dto.MetricFamily, error) {
   100  	var textParser expfmt.TextParser
   101  	return textParser.TextToMetricFamilies(in)
   102  }
   103  
   104  // PrintSample returns formatted representation of metric Sample
   105  func PrintSample(sample *model.Sample) string {
   106  	buf := make([]string, 0)
   107  	// Id is a VERY special label. For 'normal' container it's useless, but it's necessary
   108  	// for 'system' containers (e.g. /docker-daemon, /kubelet, etc.). We know if that's the
   109  	// case by checking if there's a label "kubernetes_container_name" present. It's hacky
   110  	// but it works...
   111  	_, normalContainer := sample.Metric["kubernetes_container_name"]
   112  	for k, v := range sample.Metric {
   113  		if strings.HasPrefix(string(k), "__") {
   114  			continue
   115  		}
   116  
   117  		if string(k) == "id" && normalContainer {
   118  			continue
   119  		}
   120  		buf = append(buf, fmt.Sprintf("%v=%v", string(k), v))
   121  	}
   122  	return fmt.Sprintf("[%v] = %v", strings.Join(buf, ","), sample.Value)
   123  }
   124  
   125  // ComputeHistogramDelta computes the change in histogram metric for a selected label.
   126  // Results are stored in after samples
   127  func ComputeHistogramDelta(before, after model.Samples, label model.LabelName) {
   128  	beforeSamplesMap := make(map[string]*model.Sample)
   129  	for _, bSample := range before {
   130  		beforeSamplesMap[makeKey(bSample.Metric[label], bSample.Metric["le"])] = bSample
   131  	}
   132  	for _, aSample := range after {
   133  		if bSample, found := beforeSamplesMap[makeKey(aSample.Metric[label], aSample.Metric["le"])]; found {
   134  			aSample.Value = aSample.Value - bSample.Value
   135  		}
   136  	}
   137  }
   138  
   139  func makeKey(a, b model.LabelValue) string {
   140  	return string(a) + "___" + string(b)
   141  }
   142  
   143  // GetMetricValuesForLabel returns value of metric for a given dimension
   144  func GetMetricValuesForLabel(ms Metrics, metricName, label string) map[string]int64 {
   145  	samples, found := ms[metricName]
   146  	result := make(map[string]int64, len(samples))
   147  	if !found {
   148  		return result
   149  	}
   150  	for _, sample := range samples {
   151  		count := int64(sample.Value)
   152  		dimensionName := string(sample.Metric[model.LabelName(label)])
   153  		result[dimensionName] = count
   154  	}
   155  	return result
   156  }
   157  
   158  // ValidateMetrics verifies if every sample of metric has all expected labels
   159  func ValidateMetrics(metrics Metrics, metricName string, expectedLabels ...string) error {
   160  	samples, ok := metrics[metricName]
   161  	if !ok {
   162  		return fmt.Errorf("metric %q was not found in metrics", metricName)
   163  	}
   164  	for _, sample := range samples {
   165  		for _, l := range expectedLabels {
   166  			if _, ok := sample.Metric[model.LabelName(l)]; !ok {
   167  				return fmt.Errorf("metric %q is missing label %q, sample: %q", metricName, l, sample.String())
   168  			}
   169  		}
   170  	}
   171  	return nil
   172  }
   173  
   174  // Histogram wraps prometheus histogram DTO (data transfer object)
   175  type Histogram struct {
   176  	*dto.Histogram
   177  }
   178  
   179  // HistogramVec wraps a slice of Histogram.
   180  // Note that each Histogram must have the same number of buckets.
   181  type HistogramVec []*Histogram
   182  
   183  // GetAggregatedSampleCount aggregates the sample count of each inner Histogram.
   184  func (vec HistogramVec) GetAggregatedSampleCount() uint64 {
   185  	var count uint64
   186  	for _, hist := range vec {
   187  		count += hist.GetSampleCount()
   188  	}
   189  	return count
   190  }
   191  
   192  // GetAggregatedSampleSum aggregates the sample sum of each inner Histogram.
   193  func (vec HistogramVec) GetAggregatedSampleSum() float64 {
   194  	var sum float64
   195  	for _, hist := range vec {
   196  		sum += hist.GetSampleSum()
   197  	}
   198  	return sum
   199  }
   200  
   201  // Quantile first aggregates inner buckets of each Histogram, and then
   202  // computes q-th quantile of a cumulative histogram.
   203  func (vec HistogramVec) Quantile(q float64) float64 {
   204  	var buckets []bucket
   205  
   206  	for i, hist := range vec {
   207  		for j, bckt := range hist.Bucket {
   208  			if i == 0 {
   209  				buckets = append(buckets, bucket{
   210  					count:      float64(bckt.GetCumulativeCount()),
   211  					upperBound: bckt.GetUpperBound(),
   212  				})
   213  			} else {
   214  				buckets[j].count += float64(bckt.GetCumulativeCount())
   215  			}
   216  		}
   217  	}
   218  
   219  	if len(buckets) == 0 || buckets[len(buckets)-1].upperBound != math.Inf(+1) {
   220  		// The list of buckets in dto.Histogram doesn't include the final +Inf bucket, so we
   221  		// add it here for the rest of the samples.
   222  		buckets = append(buckets, bucket{
   223  			count:      float64(vec.GetAggregatedSampleCount()),
   224  			upperBound: math.Inf(+1),
   225  		})
   226  	}
   227  
   228  	return bucketQuantile(q, buckets)
   229  }
   230  
   231  // Average computes wrapped histograms' average value.
   232  func (vec HistogramVec) Average() float64 {
   233  	return vec.GetAggregatedSampleSum() / float64(vec.GetAggregatedSampleCount())
   234  }
   235  
   236  // Validate makes sure the wrapped histograms have all necessary fields set and with valid values.
   237  func (vec HistogramVec) Validate() error {
   238  	bucketSize := 0
   239  	for i, hist := range vec {
   240  		if err := hist.Validate(); err != nil {
   241  			return err
   242  		}
   243  		if i == 0 {
   244  			bucketSize = len(hist.GetBucket())
   245  		} else if bucketSize != len(hist.GetBucket()) {
   246  			return fmt.Errorf("found different bucket size: expect %v, but got %v at index %v", bucketSize, len(hist.GetBucket()), i)
   247  		}
   248  	}
   249  	return nil
   250  }
   251  
   252  // GetHistogramVecFromGatherer collects a metric, that matches the input labelValue map,
   253  // from a gatherer implementing k8s.io/component-base/metrics.Gatherer interface.
   254  // Used only for testing purposes where we need to gather metrics directly from a running binary (without metrics endpoint).
   255  func GetHistogramVecFromGatherer(gatherer metrics.Gatherer, metricName string, lvMap map[string]string) (HistogramVec, error) {
   256  	var metricFamily *dto.MetricFamily
   257  	m, err := gatherer.Gather()
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  	for _, mFamily := range m {
   262  		if mFamily.GetName() == metricName {
   263  			metricFamily = mFamily
   264  			break
   265  		}
   266  	}
   267  
   268  	if metricFamily == nil {
   269  		return nil, fmt.Errorf("metric %q not found", metricName)
   270  	}
   271  
   272  	if len(metricFamily.GetMetric()) == 0 {
   273  		return nil, fmt.Errorf("metric %q is empty", metricName)
   274  	}
   275  
   276  	vec := make(HistogramVec, 0)
   277  	for _, metric := range metricFamily.GetMetric() {
   278  		if LabelsMatch(metric, lvMap) {
   279  			if hist := metric.GetHistogram(); hist != nil {
   280  				vec = append(vec, &Histogram{hist})
   281  			}
   282  		}
   283  	}
   284  	return vec, nil
   285  }
   286  
   287  func uint64Ptr(u uint64) *uint64 {
   288  	return &u
   289  }
   290  
   291  // Bucket of a histogram
   292  type bucket struct {
   293  	upperBound float64
   294  	count      float64
   295  }
   296  
   297  func bucketQuantile(q float64, buckets []bucket) float64 {
   298  	if q < 0 {
   299  		return math.Inf(-1)
   300  	}
   301  	if q > 1 {
   302  		return math.Inf(+1)
   303  	}
   304  
   305  	if len(buckets) < 2 {
   306  		return math.NaN()
   307  	}
   308  
   309  	rank := q * buckets[len(buckets)-1].count
   310  	b := sort.Search(len(buckets)-1, func(i int) bool { return buckets[i].count >= rank })
   311  
   312  	if b == 0 {
   313  		return buckets[0].upperBound * (rank / buckets[0].count)
   314  	}
   315  
   316  	if b == len(buckets)-1 && math.IsInf(buckets[b].upperBound, 1) {
   317  		return buckets[len(buckets)-2].upperBound
   318  	}
   319  
   320  	// linear approximation of b-th bucket
   321  	brank := rank - buckets[b-1].count
   322  	bSize := buckets[b].upperBound - buckets[b-1].upperBound
   323  	bCount := buckets[b].count - buckets[b-1].count
   324  
   325  	return buckets[b-1].upperBound + bSize*(brank/bCount)
   326  }
   327  
   328  // Quantile computes q-th quantile of a cumulative histogram.
   329  // It's expected the histogram is valid (by calling Validate)
   330  func (hist *Histogram) Quantile(q float64) float64 {
   331  	var buckets []bucket
   332  
   333  	for _, bckt := range hist.Bucket {
   334  		buckets = append(buckets, bucket{
   335  			count:      float64(bckt.GetCumulativeCount()),
   336  			upperBound: bckt.GetUpperBound(),
   337  		})
   338  	}
   339  
   340  	if len(buckets) == 0 || buckets[len(buckets)-1].upperBound != math.Inf(+1) {
   341  		// The list of buckets in dto.Histogram doesn't include the final +Inf bucket, so we
   342  		// add it here for the rest of the samples.
   343  		buckets = append(buckets, bucket{
   344  			count:      float64(hist.GetSampleCount()),
   345  			upperBound: math.Inf(+1),
   346  		})
   347  	}
   348  
   349  	return bucketQuantile(q, buckets)
   350  }
   351  
   352  // Average computes histogram's average value
   353  func (hist *Histogram) Average() float64 {
   354  	return hist.GetSampleSum() / float64(hist.GetSampleCount())
   355  }
   356  
   357  // Validate makes sure the wrapped histogram has all necessary fields set and with valid values.
   358  func (hist *Histogram) Validate() error {
   359  	if hist.SampleCount == nil || hist.GetSampleCount() == 0 {
   360  		return fmt.Errorf("nil or empty histogram SampleCount")
   361  	}
   362  
   363  	if hist.SampleSum == nil || hist.GetSampleSum() == 0 {
   364  		return fmt.Errorf("nil or empty histogram SampleSum")
   365  	}
   366  
   367  	for _, bckt := range hist.Bucket {
   368  		if bckt == nil {
   369  			return fmt.Errorf("empty histogram bucket")
   370  		}
   371  		if bckt.UpperBound == nil || bckt.GetUpperBound() < 0 {
   372  			return fmt.Errorf("nil or negative histogram bucket UpperBound")
   373  		}
   374  	}
   375  
   376  	return nil
   377  }
   378  
   379  // GetGaugeMetricValue extracts metric value from GaugeMetric
   380  func GetGaugeMetricValue(m metrics.GaugeMetric) (float64, error) {
   381  	metricProto := &dto.Metric{}
   382  	if err := m.Write(metricProto); err != nil {
   383  		return 0, fmt.Errorf("error writing m: %v", err)
   384  	}
   385  	return metricProto.Gauge.GetValue(), nil
   386  }
   387  
   388  // GetCounterMetricValue extracts metric value from CounterMetric
   389  func GetCounterMetricValue(m metrics.CounterMetric) (float64, error) {
   390  	metricProto := &dto.Metric{}
   391  	if err := m.(metrics.Metric).Write(metricProto); err != nil {
   392  		return 0, fmt.Errorf("error writing m: %v", err)
   393  	}
   394  	return metricProto.Counter.GetValue(), nil
   395  }
   396  
   397  // GetHistogramMetricValue extracts sum of all samples from ObserverMetric
   398  func GetHistogramMetricValue(m metrics.ObserverMetric) (float64, error) {
   399  	metricProto := &dto.Metric{}
   400  	if err := m.(metrics.Metric).Write(metricProto); err != nil {
   401  		return 0, fmt.Errorf("error writing m: %v", err)
   402  	}
   403  	return metricProto.Histogram.GetSampleSum(), nil
   404  }
   405  
   406  // GetHistogramMetricCount extracts count of all samples from ObserverMetric
   407  func GetHistogramMetricCount(m metrics.ObserverMetric) (uint64, error) {
   408  	metricProto := &dto.Metric{}
   409  	if err := m.(metrics.Metric).Write(metricProto); err != nil {
   410  		return 0, fmt.Errorf("error writing m: %v", err)
   411  	}
   412  	return metricProto.Histogram.GetSampleCount(), nil
   413  }
   414  
   415  // LabelsMatch returns true if metric has all expected labels otherwise false
   416  func LabelsMatch(metric *dto.Metric, labelFilter map[string]string) bool {
   417  	metricLabels := map[string]string{}
   418  
   419  	for _, labelPair := range metric.Label {
   420  		metricLabels[labelPair.GetName()] = labelPair.GetValue()
   421  	}
   422  
   423  	// length comparison then match key to values in the maps
   424  	if len(labelFilter) > len(metricLabels) {
   425  		return false
   426  	}
   427  
   428  	for labelName, labelValue := range labelFilter {
   429  		if value, ok := metricLabels[labelName]; !ok || value != labelValue {
   430  			return false
   431  		}
   432  	}
   433  
   434  	return true
   435  }
   436  

View as plain text