diff --git a/prometheus/testutil/testutil.go b/prometheus/testutil/testutil.go index 115979dc1..68c42b0db 100644 --- a/prometheus/testutil/testutil.go +++ b/prometheus/testutil/testutil.go @@ -40,13 +40,12 @@ package testutil import ( "bytes" "fmt" - "io" - "reflect" - "github.com/davecgh/go-spew/spew" - "github.com/prometheus/common/expfmt" - dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" + "io" + "net/http" + "reflect" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/internal" @@ -153,6 +152,33 @@ 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: %s", err) + } + + 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. @@ -182,17 +208,35 @@ func TransactionalGatherAndCompare(g prometheus.TransactionalGatherer, expected if err != nil { return fmt.Errorf("gathering metrics failed: %s", 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: %s", 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..a0f48367c 100644 --- a/prometheus/testutil/testutil_test.go +++ b/prometheus/testutil/testutil_test.go @@ -14,10 +14,12 @@ package testutil import ( + "fmt" + "github.com/prometheus/client_golang/prometheus" + "net/http" + "net/http/httptest" "strings" "testing" - - "github.com/prometheus/client_golang/prometheus" ) type untypedCollector struct{} @@ -308,6 +310,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{