147 lines
4.1 KiB
Go
147 lines
4.1 KiB
Go
// Package promtest provides helpers for parsing and extracting prometheus metrics.
|
|
// These functions are only intended to be called from test files,
|
|
// as there is a dependency on the standard library testing package.
|
|
package promtest
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
dto "github.com/prometheus/client_model/go"
|
|
"github.com/prometheus/common/expfmt"
|
|
)
|
|
|
|
// FromHTTPResponse parses the prometheus metrics from the given *http.Response.
|
|
// It relies on properly set response headers to correctly parse.
|
|
// It will unconditionally close the response body.
|
|
//
|
|
// This is particularly helpful when testing the output of the /metrics endpoint of a service.
|
|
// However, for comprehensive testing of metrics, it usually makes more sense to
|
|
// add collectors to a registry and call Registry.Gather to get the metrics without involving HTTP.
|
|
func FromHTTPResponse(r *http.Response) ([]*dto.MetricFamily, error) {
|
|
defer r.Body.Close()
|
|
|
|
dec := expfmt.NewDecoder(r.Body, expfmt.ResponseFormat(r.Header))
|
|
var mfs []*dto.MetricFamily
|
|
for {
|
|
mf := new(dto.MetricFamily)
|
|
if err := dec.Decode(mf); err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
mfs = append(mfs, mf)
|
|
}
|
|
|
|
return mfs, nil
|
|
}
|
|
|
|
// FindMetric iterates through mfs to find the first metric family matching name.
|
|
// If a metric family matches, then the metrics inside the family are searched,
|
|
// and the first metric whose labels match the given labels are returned.
|
|
// If no matches are found, FindMetric returns nil.
|
|
//
|
|
// FindMetric assumes that the labels on the metric family are well formed,
|
|
// i.e. there are no duplicate label names, and the label values are not empty strings.
|
|
func FindMetric(mfs []*dto.MetricFamily, name string, labels map[string]string) *dto.Metric {
|
|
_, m := findMetric(mfs, name, labels)
|
|
return m
|
|
}
|
|
|
|
// MustFindMetric returns the matching metric, or if no matching metric could be found,
|
|
// it calls tb.Log with helpful output of what was actually available, before calling tb.FailNow.
|
|
func MustFindMetric(tb testing.TB, mfs []*dto.MetricFamily, name string, labels map[string]string) *dto.Metric {
|
|
tb.Helper()
|
|
|
|
fam, m := findMetric(mfs, name, labels)
|
|
if fam == nil {
|
|
tb.Logf("metric family with name %q not found", name)
|
|
tb.Log("available names:")
|
|
for _, mf := range mfs {
|
|
tb.Logf("\t%s", mf.GetName())
|
|
}
|
|
tb.FailNow()
|
|
return nil // Need an explicit return here for test.
|
|
}
|
|
|
|
if m == nil {
|
|
tb.Logf("found metric family with name %q, but metric with labels %v not found", name, labels)
|
|
tb.Logf("available labels on metric family %q:", name)
|
|
|
|
for _, m := range fam.Metric {
|
|
pairs := make([]string, len(m.Label))
|
|
for i, l := range m.Label {
|
|
pairs[i] = fmt.Sprintf("%q: %q", l.GetName(), l.GetValue())
|
|
}
|
|
tb.Logf("\t%s", strings.Join(pairs, ", "))
|
|
}
|
|
tb.FailNow()
|
|
return nil // Need an explicit return here for test.
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// findMetric is a helper that returns the matching family and the matching metric.
|
|
// The exported FindMetric function specifically only finds the metric, not the family,
|
|
// but for test it is more helpful to identify whether the family was matched.
|
|
func findMetric(mfs []*dto.MetricFamily, name string, labels map[string]string) (*dto.MetricFamily, *dto.Metric) {
|
|
var fam *dto.MetricFamily
|
|
|
|
for _, mf := range mfs {
|
|
if mf.GetName() == name {
|
|
fam = mf
|
|
break
|
|
}
|
|
}
|
|
|
|
if fam == nil {
|
|
// No family matching the name.
|
|
return nil, nil
|
|
}
|
|
|
|
for _, m := range fam.Metric {
|
|
if len(m.Label) != len(labels) {
|
|
continue
|
|
}
|
|
|
|
match := true
|
|
for _, l := range m.Label {
|
|
if labels[l.GetName()] != l.GetValue() {
|
|
match = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if !match {
|
|
continue
|
|
}
|
|
|
|
// All labels matched.
|
|
return fam, m
|
|
}
|
|
|
|
// Didn't find a metric whose labels all matched.
|
|
return fam, nil
|
|
}
|
|
|
|
// MustGather calls g.Gather and calls tb.Fatal if there was an error.
|
|
func MustGather(tb testing.TB, g prometheus.Gatherer) []*dto.MetricFamily {
|
|
tb.Helper()
|
|
|
|
mfs, err := g.Gather()
|
|
if err != nil {
|
|
tb.Fatalf("error while gathering metrics: %v", err)
|
|
return nil // Need an explicit return here for test.
|
|
}
|
|
|
|
return mfs
|
|
}
|