Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

testutil: Add ScrapeAndCompare #1043

Merged
merged 4 commits into from Aug 5, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
74 changes: 60 additions & 14 deletions prometheus/testutil/testutil.go
Expand Up @@ -41,12 +41,12 @@ import (
"bytes"
"fmt"
"io"
"net/http"
"reflect"

"github.com/davecgh/go-spew/spew"
"github.com/prometheus/common/expfmt"

dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/internal"
Expand Down Expand Up @@ -124,7 +124,7 @@ func ToFloat64(c prometheus.Collector) float64 {
func CollectAndCount(c prometheus.Collector, metricNames ...string) int {
reg := prometheus.NewPedanticRegistry()
if err := reg.Register(c); err != nil {
panic(fmt.Errorf("registering collector failed: %s", err))
panic(fmt.Errorf("registering collector failed: %w", err))
}
result, err := GatherAndCount(reg, metricNames...)
if err != nil {
Expand All @@ -140,7 +140,7 @@ func CollectAndCount(c prometheus.Collector, metricNames ...string) int {
func GatherAndCount(g prometheus.Gatherer, metricNames ...string) (int, error) {
got, err := g.Gather()
if err != nil {
return 0, fmt.Errorf("gathering metrics failed: %s", err)
return 0, fmt.Errorf("gathering metrics failed: %w", err)
}
if metricNames != nil {
got = filterMetrics(got, metricNames)
Expand All @@ -153,13 +153,41 @@ func GatherAndCount(g prometheus.Gatherer, metricNames ...string) (int, error) {
return result, nil
}

// ScrapeAndCompare calls a remote exporter's endpoint which is expected to return some metrics in
// plain text format. Then it compares it with the results that the `expected` would return.
// If the `metricNames` is not empty it would filter the comparison only to the given metric names.
func ScrapeAndCompare(url string, expected io.Reader, metricNames ...string) error {
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("scraping metrics failed: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
kakkoyun marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("the scraping target returned a status code other than 200: %d",
resp.StatusCode)
}

scraped, err := convertReaderToMetricFamily(resp.Body)
if err != nil {
return err
}

wanted, err := convertReaderToMetricFamily(expected)
if err != nil {
return err
}

return compareMetricFamilies(scraped, wanted, metricNames...)
}

// CollectAndCompare registers the provided Collector with a newly created
// pedantic Registry. It then calls GatherAndCompare with that Registry and with
// the provided metricNames.
func CollectAndCompare(c prometheus.Collector, expected io.Reader, metricNames ...string) error {
reg := prometheus.NewPedanticRegistry()
if err := reg.Register(c); err != nil {
return fmt.Errorf("registering collector failed: %s", err)
return fmt.Errorf("registering collector failed: %w", err)
}
return GatherAndCompare(reg, expected, metricNames...)
}
Expand All @@ -180,19 +208,37 @@ func TransactionalGatherAndCompare(g prometheus.TransactionalGatherer, expected
got, done, err := g.Gather()
defer done()
if err != nil {
return fmt.Errorf("gathering metrics failed: %s", err)
return fmt.Errorf("gathering metrics failed: %w", err)
}
if metricNames != nil {
got = filterMetrics(got, metricNames)

wanted, err := convertReaderToMetricFamily(expected)
if err != nil {
return err
}

return compareMetricFamilies(got, wanted, metricNames...)
}

// convertReaderToMetricFamily would read from a io.Reader object and convert it to a slice of
// dto.MetricFamily.
func convertReaderToMetricFamily(reader io.Reader) ([]*dto.MetricFamily, error) {
var tp expfmt.TextParser
wantRaw, err := tp.TextToMetricFamilies(expected)
notNormalized, err := tp.TextToMetricFamilies(reader)
if err != nil {
return fmt.Errorf("parsing expected metrics failed: %s", err)
return nil, fmt.Errorf("converting reader to metric families failed: %w", err)
}

return internal.NormalizeMetricFamilies(notNormalized), nil
}

// compareMetricFamilies would compare 2 slices of metric families, and optionally filters both of
// them to the `metricNames` provided.
func compareMetricFamilies(got, expected []*dto.MetricFamily, metricNames ...string) error {
if metricNames != nil {
got = filterMetrics(got, metricNames)
}
want := internal.NormalizeMetricFamilies(wantRaw)

return compare(got, want)
return compare(got, expected)
}

// compare encodes both provided slices of metric families into the text format,
Expand All @@ -204,13 +250,13 @@ func compare(got, want []*dto.MetricFamily) error {
enc := expfmt.NewEncoder(&gotBuf, expfmt.FmtText)
for _, mf := range got {
if err := enc.Encode(mf); err != nil {
return fmt.Errorf("encoding gathered metrics failed: %s", err)
return fmt.Errorf("encoding gathered metrics failed: %w", err)
}
}
enc = expfmt.NewEncoder(&wantBuf, expfmt.FmtText)
for _, mf := range want {
if err := enc.Encode(mf); err != nil {
return fmt.Errorf("encoding expected metrics failed: %s", err)
return fmt.Errorf("encoding expected metrics failed: %w", err)
}
}
if diffErr := diff(wantBuf, gotBuf); diffErr != "" {
Expand Down
58 changes: 58 additions & 0 deletions prometheus/testutil/testutil_test.go
Expand Up @@ -14,6 +14,9 @@
package testutil

import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

Expand Down Expand Up @@ -308,6 +311,61 @@ Diff:
}
}

func TestScrapeAndCompare(t *testing.T) {
const expected = `
# HELP some_total A value that represents a counter.
# TYPE some_total counter

some_total{ label1 = "value1" } 1
`

expectedReader := strings.NewReader(expected)

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, expected)
}))
defer ts.Close()

if err := ScrapeAndCompare(ts.URL, expectedReader, "some_total"); err != nil {
t.Errorf("unexpected scraping result:\n%s", err)
}
}

func TestScrapeAndCompareFetchingFail(t *testing.T) {
err := ScrapeAndCompare("some_url", strings.NewReader("some expectation"), "some_total")
if err == nil {
t.Errorf("expected an error but got nil")
}
if !strings.HasPrefix(err.Error(), "scraping metrics failed") {
t.Errorf("unexpected error happened: %s", err)
}
}

func TestScrapeAndCompareBadStatusCode(t *testing.T) {
const expected = `
# HELP some_total A value that represents a counter.
# TYPE some_total counter

some_total{ label1 = "value1" } 1
`

expectedReader := strings.NewReader(expected)

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadGateway)
fmt.Fprintln(w, expected)
}))
defer ts.Close()

err := ScrapeAndCompare(ts.URL, expectedReader, "some_total")
if err == nil {
t.Errorf("expected an error but got nil")
}
if !strings.HasPrefix(err.Error(), "the scraping target returned a status code other than 200") {
t.Errorf("unexpected error happened: %s", err)
}
}

func TestCollectAndCount(t *testing.T) {
c := prometheus.NewCounterVec(
prometheus.CounterOpts{
Expand Down