From 1638da9ae43be6ff5ad522ca402712e26b4a19c7 Mon Sep 17 00:00:00 2001 From: Soroosh Azary Marhabi Date: Fri, 5 Aug 2022 18:57:47 +0430 Subject: [PATCH] testutil: Add ScrapeAndCompare (#1043) * testutil: Add ScrapeAndCompare Signed-off-by: sazary * testutil: Use %w verb wherever we're using an error in fmt.Errorf Signed-off-by: sazary * Format Signed-off-by: Kemal Akkoyun Co-authored-by: Kemal Akkoyun Co-authored-by: Kemal Akkoyun --- prometheus/testutil/testutil.go | 62 ++++++++++++++++++++++++---- prometheus/testutil/testutil_test.go | 58 ++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 8 deletions(-) diff --git a/prometheus/testutil/testutil.go b/prometheus/testutil/testutil.go index cdf1802ca..91b83b528 100644 --- a/prometheus/testutil/testutil.go +++ b/prometheus/testutil/testutil.go @@ -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" @@ -155,6 +155,34 @@ 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 { + 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. @@ -184,17 +212,35 @@ func TransactionalGatherAndCompare(g prometheus.TransactionalGatherer, expected if err != nil { 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: %w", 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, diff --git a/prometheus/testutil/testutil_test.go b/prometheus/testutil/testutil_test.go index 96a6f3847..2a46e5dbb 100644 --- a/prometheus/testutil/testutil_test.go +++ b/prometheus/testutil/testutil_test.go @@ -14,6 +14,9 @@ package testutil import ( + "fmt" + "net/http" + "net/http/httptest" "strings" "testing" @@ -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{