istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/monitoring/monitortest/test.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package monitortest
    16  
    17  import (
    18  	"fmt"
    19  	"strings"
    20  	"time"
    21  
    22  	"github.com/prometheus/client_golang/prometheus"
    23  	"github.com/prometheus/client_golang/prometheus/testutil/promlint"
    24  	dto "github.com/prometheus/client_model/go"
    25  	"go.opentelemetry.io/otel/attribute"
    26  
    27  	"istio.io/istio/pkg/lazy"
    28  	"istio.io/istio/pkg/maps"
    29  	"istio.io/istio/pkg/monitoring"
    30  	"istio.io/istio/pkg/test"
    31  	"istio.io/istio/pkg/test/util/retry"
    32  )
    33  
    34  type MetricsTest struct {
    35  	t      test.Failer
    36  	reg    prometheus.Gatherer
    37  	deltas map[metricKey]float64
    38  }
    39  
    40  type metricKey struct {
    41  	name  string
    42  	attrs attribute.Set
    43  }
    44  
    45  var reg = lazy.New(func() (prometheus.Gatherer, error) {
    46  	// TODO: do not use a global and/or add a way to reset (https://github.com/open-telemetry/opentelemetry-go/issues/4291)
    47  	reg := prometheus.NewRegistry()
    48  	_, err := monitoring.RegisterPrometheusExporter(reg, reg)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	return reg, nil
    53  })
    54  
    55  func TestRegistry(t test.Failer) prometheus.Gatherer {
    56  	r, err := reg.Get()
    57  	if err != nil {
    58  		t.Fatal(err)
    59  	}
    60  	return r
    61  }
    62  
    63  func New(t test.Failer) *MetricsTest {
    64  	r := TestRegistry(t)
    65  	mt := &MetricsTest{t: t, reg: r, deltas: computeDeltas(t, r)}
    66  	return mt
    67  }
    68  
    69  func computeDeltas(t test.Failer, reg prometheus.Gatherer) map[metricKey]float64 {
    70  	res := map[metricKey]float64{}
    71  	metrics, err := reg.Gather()
    72  	if err != nil {
    73  		t.Fatal(err)
    74  	}
    75  	for _, metric := range metrics {
    76  		for _, row := range metric.Metric {
    77  			if row.Counter == nil {
    78  				continue
    79  			}
    80  			key := toMetricKey(row, metric)
    81  			res[key] = *row.Counter.Value
    82  		}
    83  	}
    84  	return res
    85  }
    86  
    87  func toMetricKey(row *dto.Metric, metric *dto.MetricFamily) metricKey {
    88  	kvs := []attribute.KeyValue{}
    89  	for _, lv := range row.Label {
    90  		kvs = append(kvs, attribute.String(*lv.Name, *lv.Value))
    91  	}
    92  	key := metricKey{
    93  		name:  *metric.Name,
    94  		attrs: attribute.NewSet(kvs...),
    95  	}
    96  	return key
    97  }
    98  
    99  type Compare func(any) error
   100  
   101  func DoesNotExist(any) error {
   102  	// special case logic in the Assert
   103  	return nil
   104  }
   105  
   106  func Exactly(v float64) func(any) error {
   107  	return func(f any) error {
   108  		if v != toFloat(f) {
   109  			return fmt.Errorf("want %v, got %v", v, toFloat(f))
   110  		}
   111  		return nil
   112  	}
   113  }
   114  
   115  func Distribution(count uint64, sum float64) func(any) error {
   116  	return func(f any) error {
   117  		d := f.(*dto.Histogram)
   118  		if *d.SampleCount != count {
   119  			return fmt.Errorf("want %v samples, got %v", count, *d.SampleCount)
   120  		}
   121  		if *d.SampleSum != sum {
   122  			return fmt.Errorf("want %v sum, got %v", count, *d.SampleSum)
   123  		}
   124  		return nil
   125  	}
   126  }
   127  
   128  // Buckets asserts a distribution has the number of buckets
   129  func Buckets(count int) func(any) error {
   130  	return func(f any) error {
   131  		d := f.(*dto.Histogram)
   132  		if len(d.Bucket) != count {
   133  			return fmt.Errorf("want %v buckets, got %v", count, len(d.Bucket))
   134  		}
   135  		return nil
   136  	}
   137  }
   138  
   139  func AtLeast(want float64) func(any) error {
   140  	return func(got any) error {
   141  		if want > toFloat(got) {
   142  			return fmt.Errorf("want %v <= %v (got %v)", want, toFloat(got), want)
   143  		}
   144  		return nil
   145  	}
   146  }
   147  
   148  func (m *MetricsTest) Assert(name string, tags map[string]string, compare Compare, opts ...retry.Option) {
   149  	m.t.Helper()
   150  	opt := []retry.Option{retry.Timeout(time.Second * 5), retry.Message("metric not found")}
   151  	opt = append(opt, opts...)
   152  	err := retry.UntilSuccess(func() error {
   153  		res, err := m.reg.Gather()
   154  		if err != nil {
   155  			return err
   156  		}
   157  		if fmt.Sprintf("%p", compare) == fmt.Sprintf("%p", DoesNotExist) {
   158  			for _, metric := range res {
   159  				if *metric.Name == name {
   160  					return fmt.Errorf("metric was found when it should not have been")
   161  				}
   162  			}
   163  			return nil
   164  		}
   165  		for _, metric := range res {
   166  			if *metric.Name != name {
   167  				continue
   168  			}
   169  			for _, row := range metric.Metric {
   170  				want := maps.Clone(tags)
   171  				for _, lv := range row.Label {
   172  					k, v := *lv.Name, *lv.Value
   173  					if want[k] == v {
   174  						delete(want, k)
   175  					} else {
   176  						m.t.Logf("skip metric: want %v=%v, got %v=%v", k, want[k], k, v)
   177  					}
   178  				}
   179  				if len(want) > 0 {
   180  					// Not a match
   181  					m.t.Logf("skip metric: missing labels: %+v", want)
   182  					continue
   183  				}
   184  				var v any
   185  				if row.Counter != nil {
   186  					cv := *row.Counter.Value
   187  					key := toMetricKey(row, metric)
   188  					if delta, f := m.deltas[key]; f {
   189  						cv -= delta
   190  					}
   191  					v = cv
   192  				} else if row.Gauge != nil {
   193  					v = *row.Gauge.Value
   194  				} else if row.Histogram != nil {
   195  					v = row.Histogram
   196  				}
   197  				err := compare(v)
   198  				if err != nil {
   199  					return fmt.Errorf("got unexpected val %v: %v", v, err)
   200  				}
   201  				return nil
   202  			}
   203  		}
   204  		return fmt.Errorf("no matching rows found")
   205  	}, opt...)
   206  	if err != nil {
   207  		m.t.Logf("Metric %v/%v not matched (%v); Dumping known metrics:", name, tags, err)
   208  		m.Dump()
   209  		m.t.Fatal(err)
   210  	}
   211  
   212  	// Run through linter. For now this is warning, maybe allow opt-in to strict
   213  	res, err := m.reg.Gather()
   214  	if err != nil {
   215  		m.t.Fatal(err)
   216  	}
   217  	problems, err := promlint.NewWithMetricFamilies(res).Lint()
   218  	if err != nil {
   219  		m.t.Fatal(err)
   220  	}
   221  	if len(problems) > 0 {
   222  		m.t.Logf("WARNING: Prometheus linter issue: %v", problems)
   223  	}
   224  }
   225  
   226  func toFloat(r interface{}) float64 {
   227  	switch v := r.(type) {
   228  	default:
   229  		panic(fmt.Sprintf("unknown type %T", r))
   230  	case int64:
   231  		return float64(v)
   232  	case float64:
   233  		return v
   234  	}
   235  }
   236  
   237  // Metrics returns the full list of known metrics. Usually Assert should be used
   238  func (m *MetricsTest) Metrics() []Metric {
   239  	m.t.Helper()
   240  	res, err := m.reg.Gather()
   241  	if err != nil {
   242  		m.t.Fatal(err)
   243  	}
   244  	metrics := []Metric{}
   245  	for _, metric := range res {
   246  		if len(metric.Metric) == 0 {
   247  			m.t.Logf("%v: no rows", *metric.Name)
   248  		}
   249  		for _, row := range metric.Metric {
   250  			m := Metric{Name: *metric.Name, Labels: map[string]string{}, Value: display(row)}
   251  			for _, kv := range row.Label {
   252  				k, v := *kv.Name, *kv.Value
   253  				m.Labels[k] = v
   254  			}
   255  			metrics = append(metrics, m)
   256  		}
   257  	}
   258  	return metrics
   259  }
   260  
   261  type Metric struct {
   262  	Name   string
   263  	Labels map[string]string
   264  	Value  string
   265  }
   266  
   267  func (m *MetricsTest) Dump() {
   268  	m.t.Helper()
   269  	res, err := m.reg.Gather()
   270  	if err != nil {
   271  		m.t.Fatal(err)
   272  	}
   273  	for _, metric := range res {
   274  		if len(metric.Metric) == 0 {
   275  			m.t.Logf("%v: no rows", *metric.Name)
   276  		}
   277  		for _, row := range metric.Metric {
   278  			kvs := []string{}
   279  			for _, kv := range row.Label {
   280  				k, v := *kv.Name, *kv.Value
   281  				kvs = append(kvs, k+"="+v)
   282  			}
   283  			tags := strings.Join(kvs, ",")
   284  			m.t.Logf(" %v{%v} %v", *metric.Name, tags, display(row))
   285  		}
   286  	}
   287  }
   288  
   289  func display(row *dto.Metric) string {
   290  	if row.Counter != nil {
   291  		return fmt.Sprint(*row.Counter.Value)
   292  	} else if row.Gauge != nil {
   293  		return fmt.Sprint(*row.Gauge.Value)
   294  	} else if row.Histogram != nil {
   295  		return fmt.Sprintf("histogram{count=%v,sum=%v}", *row.Histogram.SampleCount, *row.Histogram.SampleSum)
   296  	} else if row.Summary != nil {
   297  		return fmt.Sprintf("summary{count=%v,sum=%v}", *row.Summary.SampleCount, *row.Summary.SampleSum)
   298  	}
   299  	return "?"
   300  }