internal/testhelper/prom.go (54 lines of code) (raw):
package testhelper
import (
"fmt"
"strings"
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
prom_model "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
)
// RequirePromMetrics is a test helper function that verifies if the provided Prometheus
// collector produces the expected metrics. The expected metrics are given as a string
// in Prometheus exposition format (the format Prometheus uses to scrape metrics).
//
// If a list of specific metrics is provided, only those metrics will be checked, and
// any others will be ignored.
//
// Example expected metrics string:
//
// # TYPE gitaly_concurrency_limiting_backoff_events_total counter
// gitaly_concurrency_limiting_backoff_events_total{watcher="testWatcher1"} 1
// gitaly_concurrency_limiting_backoff_events_total{watcher="testWatcher2"} 1
func RequirePromMetrics(t *testing.T, c prometheus.Collector, expected string, metrics ...string) {
require.NoError(t, ComparePromMetrics(t, c, expected, metrics...))
}
// RequireHistogramSampleCounts is a test helper function that ensures the provided Prometheus
// collector matches the expected sample counts of histogram metrics. In most cases, we use
// histogram to record elapsed time. It's not trivial to assert the elapsed time. So, this function
// provides a way to assert the sample counts of histogram, which are much more reliable.
// RequirePromMetrics is preferred when we need accurate assertion.
func RequireHistogramSampleCounts(t *testing.T, c prometheus.Collector, expected map[string]int) {
reg := prometheus.NewPedanticRegistry()
err := reg.Register(c)
require.NoError(t, err)
promMetrics, err := reg.Gather()
require.NoError(t, err)
actual := map[string]int{}
for _, actualFamily := range promMetrics {
if actualFamily.GetType() != prom_model.MetricType_HISTOGRAM {
continue
}
for _, actualMetric := range actualFamily.GetMetric() {
var labels []string
for _, actualLabel := range actualMetric.GetLabel() {
var label strings.Builder
label.WriteString(actualLabel.GetName())
label.WriteString("=")
label.WriteString(actualLabel.GetValue())
labels = append(labels, label.String())
}
value := int(actualMetric.GetHistogram().GetSampleCount())
if len(labels) == 0 {
actual[actualFamily.GetName()] = value
} else {
actual[fmt.Sprintf("%s{%s}", actualFamily.GetName(), strings.Join(labels, ","))] = value
}
}
}
require.Equal(t, expected, actual)
}
// ComparePromMetrics is a variant of RequirePromMetrics. It returns an error if the actual
// collected metrics don't match the expected ones.
func ComparePromMetrics(t *testing.T, c prometheus.Collector, expected string, metrics ...string) error {
var parser expfmt.TextParser
if len(metrics) == 0 {
family, err := parser.TextToMetricFamilies(strings.NewReader(expected))
require.NoErrorf(t, err, "fail to parse expected prometheus metrics: %s", err)
metrics = maps.Keys(family)
}
return testutil.CollectAndCompare(c, strings.NewReader(expected), metrics...)
}