| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package prometheus |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "compress/gzip" |
| 8 | "context" |
| 9 | "errors" |
| 10 | "net/http" |
| 11 | "net/http/httptest" |
| 12 | "os" |
| 13 | "strings" |
| 14 | "testing" |
| 15 | |
| 16 | "github.com/stretchr/testify/assert" |
| 17 | "github.com/stretchr/testify/require" |
| 18 | |
| 19 | "github.com/netdata/netdata/go/plugins/pkg/prometheus/selector" |
| 20 | "github.com/netdata/netdata/go/plugins/pkg/web" |
| 21 | ) |
| 22 | |
| 23 | var ( |
| 24 | testData, _ = os.ReadFile("testdata/testdata.txt") |
| 25 | testDataNoMeta, _ = os.ReadFile("testdata/testdata.nometa.txt") |
| 26 | ) |
| 27 | |
| 28 | func Test_testClientDataIsValid(t *testing.T) { |
| 29 | for name, data := range map[string][]byte{ |
| 30 | "testData": testData, |
| 31 | } { |
| 32 | require.NotNilf(t, data, name) |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | func TestPrometheus404(t *testing.T) { |
| 37 | tsMux := http.NewServeMux() |
| 38 | tsMux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { |
| 39 | w.WriteHeader(404) |
| 40 | }) |
| 41 | ts := httptest.NewServer(tsMux) |
| 42 | defer ts.Close() |
| 43 | |
| 44 | req := web.RequestConfig{URL: ts.URL + "/metrics"} |
| 45 | prom := New(http.DefaultClient, req) |
| 46 | res, err := prom.ScrapeSeries() |
| 47 | |
| 48 | assert.Error(t, err) |
| 49 | assert.Nil(t, res) |
| 50 | } |
| 51 | |
| 52 | func TestPrometheusPlain(t *testing.T) { |
| 53 | tsMux := http.NewServeMux() |
| 54 | tsMux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { |
| 55 | _, _ = w.Write(testData) |
| 56 | }) |
| 57 | ts := httptest.NewServer(tsMux) |
| 58 | defer ts.Close() |
| 59 | |
| 60 | req := web.RequestConfig{URL: ts.URL + "/metrics"} |
| 61 | prom := New(http.DefaultClient, req) |
| 62 | res, err := prom.ScrapeSeries() |
| 63 | |
| 64 | assert.NoError(t, err) |
| 65 | verifyTestData(t, res) |
| 66 | } |
| 67 | |
| 68 | func TestPrometheusPlainWithSelector(t *testing.T) { |
| 69 | tsMux := http.NewServeMux() |
| 70 | tsMux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { |
| 71 | _, _ = w.Write(testData) |
| 72 | }) |
| 73 | ts := httptest.NewServer(tsMux) |
| 74 | defer ts.Close() |
| 75 | |
| 76 | req := web.RequestConfig{URL: ts.URL + "/metrics"} |
| 77 | sr, err := selector.Parse("go_gc*") |
| 78 | require.NoError(t, err) |
| 79 | prom := NewWithSelector(http.DefaultClient, req, sr) |
| 80 | |
| 81 | res, err := prom.ScrapeSeries() |
| 82 | require.NoError(t, err) |
| 83 | |
| 84 | for _, v := range res { |
| 85 | assert.Truef(t, strings.HasPrefix(v.Name(), "go_gc"), v.Name()) |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | func TestPrometheusGzip(t *testing.T) { |
| 90 | counter := 0 |
| 91 | rawTestData := [][]byte{testData, testDataNoMeta} |
| 92 | tsMux := http.NewServeMux() |
| 93 | tsMux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { |
| 94 | w.Header().Set("Content-Encoding", "gzip") |
| 95 | w.WriteHeader(200) |
| 96 | gz := new(bytes.Buffer) |
| 97 | ww := gzip.NewWriter(gz) |
| 98 | _, _ = ww.Write(rawTestData[counter]) |
| 99 | _ = ww.Close() |
| 100 | _, _ = gz.WriteTo(w) |
| 101 | counter++ |
| 102 | }) |
| 103 | ts := httptest.NewServer(tsMux) |
| 104 | defer ts.Close() |
| 105 | |
| 106 | req := web.RequestConfig{URL: ts.URL + "/metrics"} |
| 107 | prom := New(http.DefaultClient, req) |
| 108 | |
| 109 | for range 2 { |
| 110 | res, err := prom.ScrapeSeries() |
| 111 | assert.NoError(t, err) |
| 112 | verifyTestData(t, res) |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | func TestPrometheusReadFromFile(t *testing.T) { |
| 117 | req := web.RequestConfig{URL: "file://testdata/testdata.txt"} |
| 118 | |
| 119 | prom := NewWithSelector(http.DefaultClient, req, nil) |
| 120 | |
| 121 | for range 2 { |
| 122 | res, err := prom.ScrapeSeries() |
| 123 | assert.NoError(t, err) |
| 124 | verifyTestData(t, res) |
| 125 | } |
| 126 | |
| 127 | prom = New(http.DefaultClient, req) |
| 128 | |
| 129 | for range 2 { |
| 130 | res, err := prom.ScrapeSeries() |
| 131 | assert.NoError(t, err) |
| 132 | verifyTestData(t, res) |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | func TestPrometheusScrapeWithTransform(t *testing.T) { |
| 137 | errBoom := errors.New("boom") |
| 138 | |
| 139 | tests := map[string]struct { |
| 140 | transform func(seen *int) SampleTransform |
| 141 | wantErr error |
| 142 | check func(t *testing.T, mfs MetricFamilies, seen int) |
| 143 | }{ |
| 144 | "nil transform assembles like Scrape": { |
| 145 | transform: func(*int) SampleTransform { return nil }, |
| 146 | check: func(t *testing.T, mfs MetricFamilies, seen int) { |
| 147 | assert.Positive(t, mfs.Len()) |
| 148 | assert.Zero(t, seen, "nil transform must not be invoked") |
| 149 | }, |
| 150 | }, |
| 151 | "transform sees every sample and keeps them": { |
| 152 | transform: func(seen *int) SampleTransform { |
| 153 | return func(s Sample) (Sample, bool, error) { *seen++; return s, true, nil } |
| 154 | }, |
| 155 | check: func(t *testing.T, mfs MetricFamilies, seen int) { |
| 156 | assert.Positive(t, seen) |
| 157 | assert.Positive(t, mfs.Len()) |
| 158 | }, |
| 159 | }, |
| 160 | "dropping every sample yields no families": { |
| 161 | transform: func(seen *int) SampleTransform { |
| 162 | return func(s Sample) (Sample, bool, error) { *seen++; return s, false, nil } |
| 163 | }, |
| 164 | check: func(t *testing.T, mfs MetricFamilies, seen int) { |
| 165 | assert.Positive(t, seen) |
| 166 | assert.Zero(t, mfs.Len()) |
| 167 | }, |
| 168 | }, |
| 169 | "transform error aborts the scrape": { |
| 170 | transform: func(*int) SampleTransform { |
| 171 | return func(s Sample) (Sample, bool, error) { return s, false, errBoom } |
| 172 | }, |
| 173 | wantErr: errBoom, |
| 174 | }, |
| 175 | } |
| 176 | |
| 177 | for name, tc := range tests { |
| 178 | t.Run(name, func(t *testing.T) { |
| 179 | prom := New(http.DefaultClient, web.RequestConfig{URL: "file://testdata/testdata.txt"}) |
| 180 | |
| 181 | var seen int |
| 182 | mfs, err := prom.ScrapeWithTransform(context.Background(), tc.transform(&seen)) |
| 183 | |
| 184 | if tc.wantErr != nil { |
| 185 | assert.ErrorIs(t, err, tc.wantErr) |
| 186 | return |
| 187 | } |
| 188 | require.NoError(t, err) |
| 189 | tc.check(t, mfs, seen) |
| 190 | }) |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | func verifyTestData(t *testing.T, ms Series) { |
| 195 | assert.Equal(t, 410, len(ms)) |
| 196 | assert.Equal(t, "go_gc_duration_seconds", ms[0].Labels.Get("__name__")) |
| 197 | assert.Equal(t, "0.25", ms[0].Labels.Get("quantile")) |
| 198 | assert.InDelta(t, 4.9351e-05, ms[0].Value, 0.0001) |
| 199 | |
| 200 | notExistYet := ms.FindByName("not_exist_yet") |
| 201 | assert.NotNil(t, notExistYet) |
| 202 | assert.Len(t, notExistYet, 0) |
| 203 | |
| 204 | targetInterval := ms.FindByName("prometheus_target_interval_length_seconds") |
| 205 | assert.Len(t, targetInterval, 5) |
| 206 | } |