master
go 497 lines 15.8 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package prometheus
4
5 import (
6 "context"
7 "net/http"
8 "net/http/httptest"
9 "os"
10 "testing"
11
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14
15 "github.com/netdata/netdata/go/plugins/pkg/metrix"
16 "github.com/netdata/netdata/go/plugins/pkg/prometheus/selector"
17 "github.com/netdata/netdata/go/plugins/pkg/web"
18 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/prometheus/relabel"
19 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
20 )
21
22 var (
23 dataConfigJSON, _ = os.ReadFile("testdata/config.json")
24 dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
25 )
26
27 func Test_testDataIsValid(t *testing.T) {
28 for name, data := range map[string][]byte{
29 "dataConfigJSON": dataConfigJSON,
30 "dataConfigYAML": dataConfigYAML,
31 } {
32 require.NotNil(t, data, name)
33 }
34 }
35
36 func TestCollector_ConfigurationSerialize(t *testing.T) {
37 collecttest.TestConfigurationSerialize(t, &Collector{}, dataConfigJSON, dataConfigYAML)
38 }
39
40 func TestCollector_Init(t *testing.T) {
41 tests := map[string]struct {
42 config Config
43 wantFail bool
44 }{
45 "non empty URL": {
46 wantFail: false,
47 config: Config{HTTPConfig: web.HTTPConfig{RequestConfig: web.RequestConfig{URL: "http://127.0.0.1:9090/metric"}}},
48 },
49 "invalid selector syntax": {
50 wantFail: true,
51 config: Config{
52 HTTPConfig: web.HTTPConfig{RequestConfig: web.RequestConfig{URL: "http://127.0.0.1:9090/metric"}},
53 Selector: selector.Expr{Allow: []string{`name{label=#"value"}`}},
54 },
55 },
56 "default": {
57 wantFail: true,
58 config: New().Config,
59 },
60 }
61
62 for name, test := range tests {
63 t.Run(name, func(t *testing.T) {
64 collr := New()
65 collr.Config = test.config
66
67 if test.wantFail {
68 assert.Error(t, collr.Init(context.Background()))
69 } else {
70 assert.NoError(t, collr.Init(context.Background()))
71 }
72 })
73 }
74 }
75
76 func TestCollector_Cleanup(t *testing.T) {
77 assert.NotPanics(t, func() { New().Cleanup(context.Background()) })
78
79 collr := New()
80 collr.URL = "http://127.0.0.1"
81 require.NoError(t, collr.Init(context.Background()))
82 assert.NotPanics(t, func() { collr.Cleanup(context.Background()) })
83 }
84
85 func TestCollector_Check(t *testing.T) {
86 tests := map[string]struct {
87 prepare func() (collr *Collector, cleanup func())
88 wantFail bool
89 }{
90 "success if endpoint returns valid metrics in prometheus format": {
91 wantFail: false,
92 prepare: func() (collr *Collector, cleanup func()) {
93 srv := httptest.NewServer(http.HandlerFunc(
94 func(w http.ResponseWriter, r *http.Request) {
95 _, _ = w.Write([]byte(`test_counter_no_meta_metric_1_total{label1="value1"} 11`))
96 }))
97 collr = New()
98 collr.URL = srv.URL
99
100 return collr, srv.Close
101 },
102 },
103 "fail if the total num of metrics exceeds the limit": {
104 wantFail: true,
105 prepare: func() (collr *Collector, cleanup func()) {
106 srv := httptest.NewServer(http.HandlerFunc(
107 func(w http.ResponseWriter, r *http.Request) {
108 _, _ = w.Write([]byte(`
109 test_counter_no_meta_metric_1_total{label1="value1"} 11
110 test_counter_no_meta_metric_1_total{label1="value2"} 11
111 `))
112 }))
113 collr = New()
114 collr.URL = srv.URL
115 collr.MaxTS = 1
116
117 return collr, srv.Close
118 },
119 },
120 "fail if the num time series in the metric exceeds the limit": {
121 wantFail: true,
122 prepare: func() (collr *Collector, cleanup func()) {
123 srv := httptest.NewServer(http.HandlerFunc(
124 func(w http.ResponseWriter, r *http.Request) {
125 _, _ = w.Write([]byte(`
126 test_counter_no_meta_metric_1_total{label1="value1"} 11
127 test_counter_no_meta_metric_1_total{label1="value2"} 11
128 `))
129 }))
130 collr = New()
131 collr.URL = srv.URL
132 collr.MaxTSPerMetric = 1
133
134 return collr, srv.Close
135 },
136 },
137 "fail if metrics have no expected prefix": {
138 wantFail: true,
139 prepare: func() (collr *Collector, cleanup func()) {
140 srv := httptest.NewServer(http.HandlerFunc(
141 func(w http.ResponseWriter, r *http.Request) {
142 _, _ = w.Write([]byte(`test_counter_no_meta_metric_1_total{label1="value1"} 11`))
143 }))
144 collr = New()
145 collr.URL = srv.URL
146 collr.ExpectedPrefix = "prefix_"
147
148 return collr, srv.Close
149 },
150 },
151 "fail if endpoint returns data not in prometheus format": {
152 wantFail: true,
153 prepare: func() (collr *Collector, cleanup func()) {
154 srv := httptest.NewServer(http.HandlerFunc(
155 func(w http.ResponseWriter, r *http.Request) {
156 _, _ = w.Write([]byte("hello and\n goodbye"))
157 }))
158 collr = New()
159 collr.URL = srv.URL
160
161 return collr, srv.Close
162 },
163 },
164 "fail if endpoint exposes only non-writable metrics": {
165 wantFail: true,
166 prepare: func() (collr *Collector, cleanup func()) {
167 srv := httptest.NewServer(http.HandlerFunc(
168 func(w http.ResponseWriter, r *http.Request) {
169 _, _ = w.Write([]byte(`app_x_info{version="1.0"} 1`))
170 }))
171 collr = New()
172 collr.URL = srv.URL
173
174 return collr, srv.Close
175 },
176 },
177 "fail if endpoint returns an empty body (no metric families)": {
178 wantFail: true,
179 prepare: func() (collr *Collector, cleanup func()) {
180 srv := httptest.NewServer(http.HandlerFunc(
181 func(w http.ResponseWriter, r *http.Request) {
182 _, _ = w.Write([]byte(""))
183 }))
184 collr = New()
185 collr.URL = srv.URL
186
187 return collr, srv.Close
188 },
189 },
190 "fail if connection refused": {
191 wantFail: true,
192 prepare: func() (collr *Collector, cleanup func()) {
193 collr = New()
194 collr.URL = "http://127.0.0.1:38001/metrics"
195
196 return collr, func() {}
197 },
198 },
199 "fail if endpoint returns 404": {
200 wantFail: true,
201 prepare: func() (collr *Collector, cleanup func()) {
202 srv := httptest.NewServer(http.HandlerFunc(
203 func(w http.ResponseWriter, r *http.Request) {
204 w.WriteHeader(http.StatusNotFound)
205 }))
206 collr = New()
207 collr.URL = srv.URL
208
209 return collr, srv.Close
210 },
211 },
212 }
213
214 for name, test := range tests {
215 t.Run(name, func(t *testing.T) {
216 collr, cleanup := test.prepare()
217 defer cleanup()
218
219 require.NoError(t, collr.Init(context.Background()))
220
221 if test.wantFail {
222 assert.Error(t, collr.Check(context.Background()))
223 } else {
224 assert.NoError(t, collr.Check(context.Background()))
225 }
226 })
227 }
228 }
229
230 // TestCollector_Collect drives the real V2 collector (Init, then a framework-style store
231 // cycle around Collect) and asserts the metrics it wrote into the metrix store, by metric
232 // name + flattened labels. Per-type correctness is exercised exhaustively in writer_test.go;
233 // this checks the collector's end-to-end wiring (client/selector/fallback built in Init →
234 // scrape → writer → store) plus the config-driven behaviors.
235 func TestCollector_Collect(t *testing.T) {
236 tests := map[string]struct {
237 prepare func() *Collector
238 input string
239 want func(t *testing.T, fr metrix.Reader)
240 }{
241 "gauge and counter values": {
242 prepare: New,
243 input: `
244 # TYPE test_gauge_metric gauge
245 test_gauge_metric{label1="value1"} 11
246 test_gauge_metric{label1="value2"} 12.5
247 # TYPE test_counter_metric_total counter
248 test_counter_metric_total{label1="value1"} 11
249 `,
250 want: func(t *testing.T, fr metrix.Reader) {
251 assert.InDelta(t, 11, value(t, fr, "test_gauge_metric", metrix.Labels{"label1": "value1"}), 1e-9)
252 assert.InDelta(t, 12.5, value(t, fr, "test_gauge_metric", metrix.Labels{"label1": "value2"}), 1e-9)
253 assert.InDelta(t, 11, value(t, fr, "test_counter_metric_total", metrix.Labels{"label1": "value1"}), 1e-9)
254 },
255 },
256 "summary flattens to quantiles, sum and count": {
257 prepare: New,
258 input: `
259 # TYPE test_latency summary
260 test_latency{quantile="0.5"} 0.25
261 test_latency{quantile="0.99"} 0.5
262 test_latency_sum 12.5
263 test_latency_count 42
264 `,
265 want: func(t *testing.T, fr metrix.Reader) {
266 assert.InDelta(t, 0.25, value(t, fr, "test_latency", metrix.Labels{"quantile": "0.5"}), 1e-9)
267 assert.InDelta(t, 0.5, value(t, fr, "test_latency", metrix.Labels{"quantile": "0.99"}), 1e-9)
268 assert.InDelta(t, 12.5, value(t, fr, "test_latency_sum", nil), 1e-9)
269 assert.InDelta(t, 42, value(t, fr, "test_latency_count", nil), 1e-9)
270 },
271 },
272 "histogram flattens to buckets, sum and count": {
273 prepare: New,
274 input: `
275 # TYPE test_dur histogram
276 test_dur_bucket{le="0.1"} 4
277 test_dur_bucket{le="+Inf"} 6
278 test_dur_sum 2.5
279 test_dur_count 6
280 `,
281 want: func(t *testing.T, fr metrix.Reader) {
282 assert.InDelta(t, 4, value(t, fr, "test_dur_bucket", metrix.Labels{"le": "0.1"}), 1e-9)
283 assert.InDelta(t, 6, value(t, fr, "test_dur_bucket", metrix.Labels{"le": "+Inf"}), 1e-9)
284 assert.InDelta(t, 2.5, value(t, fr, "test_dur_sum", nil), 1e-9)
285 assert.InDelta(t, 6, value(t, fr, "test_dur_count", nil), 1e-9)
286 },
287 },
288 "untyped falls back to gauge and counter": {
289 prepare: func() *Collector {
290 c := New()
291 c.FallbackType.Gauge = []string{"test_fallback_gauge"}
292 return c
293 },
294 input: `
295 test_fallback_gauge{label1="value1"} 7
296 test_things_total{label1="value1"} 5
297 test_untyped_dropped{label1="value1"} 9
298 `,
299 want: func(t *testing.T, fr metrix.Reader) {
300 assert.InDelta(t, 7, value(t, fr, "test_fallback_gauge", metrix.Labels{"label1": "value1"}), 1e-9)
301 assert.InDelta(t, 5, value(t, fr, "test_things_total", metrix.Labels{"label1": "value1"}), 1e-9)
302 _, ok := fr.Value("test_untyped_dropped", metrix.Labels{"label1": "value1"})
303 assert.False(t, ok, "an untyped metric with no fallback and no _total suffix must be dropped")
304 },
305 },
306 "selector drops non-matching metrics": {
307 prepare: func() *Collector {
308 c := New()
309 c.Selector = selector.Expr{Allow: []string{"test_keep"}}
310 return c
311 },
312 input: `
313 # TYPE test_keep gauge
314 test_keep{label1="value1"} 11
315 # TYPE test_drop gauge
316 test_drop{label1="value1"} 22
317 `,
318 want: func(t *testing.T, fr metrix.Reader) {
319 assert.InDelta(t, 11, value(t, fr, "test_keep", metrix.Labels{"label1": "value1"}), 1e-9)
320 _, ok := fr.Value("test_drop", metrix.Labels{"label1": "value1"})
321 assert.False(t, ok, "a metric not matched by the selector must be dropped")
322 },
323 },
324 "_info family is skipped": {
325 prepare: New,
326 input: `
327 # TYPE test_metric gauge
328 test_metric{label1="value1"} 11
329 # TYPE test_metric_info gauge
330 test_metric_info{version="1.2.3"} 1
331 `,
332 want: func(t *testing.T, fr metrix.Reader) {
333 assert.InDelta(t, 11, value(t, fr, "test_metric", metrix.Labels{"label1": "value1"}), 1e-9)
334 _, ok := fr.Value("test_metric_info", metrix.Labels{"version": "1.2.3"})
335 assert.False(t, ok, "an _info family must be skipped")
336 },
337 },
338 "per-metric series limit skips the family": {
339 prepare: func() *Collector {
340 c := New()
341 c.MaxTSPerMetric = 1
342 return c
343 },
344 input: `
345 # TYPE test_gauge_metric gauge
346 test_gauge_metric{label1="value1"} 11
347 test_gauge_metric{label1="value2"} 12
348 `,
349 want: func(t *testing.T, fr metrix.Reader) {
350 _, ok := fr.Value("test_gauge_metric", metrix.Labels{"label1": "value1"})
351 assert.False(t, ok, "a family over the per-metric series limit must be skipped entirely")
352 },
353 },
354 "relabel applies before assembly (drop + rename via __name__)": {
355 prepare: func() *Collector {
356 c := New()
357 c.relabelConfigs = []relabel.Config{
358 {
359 SourceLabels: []string{"__name__"},
360 Regex: relabel.MustNewRegexp("test_drop_me"),
361 Action: relabel.Drop,
362 },
363 {
364 SourceLabels: []string{"__name__"},
365 Regex: relabel.MustNewRegexp("test_(.+)"),
366 TargetLabel: "__name__",
367 Replacement: "renamed_${1}",
368 Action: relabel.Replace,
369 },
370 }
371 return c
372 },
373 input: `
374 # TYPE test_keep gauge
375 test_keep{label1="value1"} 11
376 # TYPE test_drop_me gauge
377 test_drop_me{label1="value1"} 22
378 `,
379 want: func(t *testing.T, fr metrix.Reader) {
380 assert.InDelta(t, 11, value(t, fr, "renamed_keep", metrix.Labels{"label1": "value1"}), 1e-9)
381 _, ok := fr.Value("test_keep", metrix.Labels{"label1": "value1"})
382 assert.False(t, ok, "the renamed metric must not appear under its original name")
383 _, ok = fr.Value("test_drop_me", metrix.Labels{"label1": "value1"})
384 assert.False(t, ok, "the drop rule must drop test_drop_me before assembly")
385 _, ok = fr.Value("renamed_drop_me", metrix.Labels{"label1": "value1"})
386 assert.False(t, ok, "a dropped sample must not be renamed or assembled")
387 },
388 },
389 "relabel rewrites a regular label (copy via Replace)": {
390 prepare: func() *Collector {
391 c := New()
392 c.relabelConfigs = []relabel.Config{
393 {
394 SourceLabels: []string{"method"},
395 Regex: relabel.MustNewRegexp("(.+)"),
396 TargetLabel: "verb",
397 Replacement: "${1}",
398 Action: relabel.Replace,
399 },
400 }
401 return c
402 },
403 input: `
404 # TYPE test_requests_total counter
405 test_requests_total{method="get"} 5
406 `,
407 want: func(t *testing.T, fr metrix.Reader) {
408 // Replace copies method -> verb; the series carries both labels.
409 assert.InDelta(t, 5, value(t, fr, "test_requests_total", metrix.Labels{"method": "get", "verb": "get"}), 1e-9)
410 },
411 },
412 }
413
414 for name, tc := range tests {
415 t.Run(name, func(t *testing.T) {
416 srv := httptest.NewServer(http.HandlerFunc(
417 func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(tc.input)) }))
418 defer srv.Close()
419
420 collr := tc.prepare()
421 collr.URL = srv.URL
422 require.NoError(t, collr.Init(context.Background()))
423
424 // Drive Collect exactly as the framework does: one store cycle around it.
425 cc := cycle(t, collr.MetricStore())
426 cc.BeginCycle()
427 require.NoError(t, collr.Collect(context.Background()))
428 require.NoError(t, cc.CommitCycleSuccess())
429
430 tc.want(t, collr.MetricStore().Read(metrix.ReadRaw(), metrix.ReadFlatten()))
431 })
432 }
433 }
434
435 // TestCollector_ChartCoverage verifies the collector's own ChartTemplateYAML() (the per-job
436 // autogen template built in Init from the configured app) plus the collected store materialize
437 // the expected chart contexts and dimensions. Unlike the manifest parity test (which builds the
438 // template directly), this exercises the real CollectorV2.ChartTemplateYAML() method and the
439 // "prometheus" / "prometheus.<app>" context namespace end-to-end via chartengine autogen.
440 func TestCollector_ChartCoverage(t *testing.T) {
441 tests := map[string]struct {
442 prepare func() *Collector
443 input string
444 want map[string][]string
445 }{
446 "default namespace, scalars and a summary split": {
447 prepare: New,
448 input: `
449 # TYPE test_gauge_metric gauge
450 test_gauge_metric{label1="value1"} 11
451 # TYPE test_counter_metric_total counter
452 test_counter_metric_total{label1="value1"} 11
453 # TYPE test_summary_duration_seconds summary
454 test_summary_duration_seconds{label1="value1",quantile="0.5"} 0.25
455 test_summary_duration_seconds{label1="value1",quantile="0.99"} 0.5
456 test_summary_duration_seconds_sum{label1="value1"} 12.5
457 test_summary_duration_seconds_count{label1="value1"} 42
458 `,
459 want: map[string][]string{
460 "prometheus.test_gauge_metric": {"test_gauge_metric"},
461 "prometheus.test_counter_metric_total": {"test_counter_metric_total"},
462 "prometheus.test_summary_duration_seconds": {"quantile_0.5", "quantile_0.99"},
463 "prometheus.test_summary_duration_seconds_sum": {"test_summary_duration_seconds_sum"},
464 "prometheus.test_summary_duration_seconds_count": {"test_summary_duration_seconds_count"},
465 },
466 },
467 "app namespace prefixes the context": {
468 prepare: func() *Collector { c := New(); c.Application = "myapp"; return c },
469 input: `
470 # TYPE test_gauge_metric gauge
471 test_gauge_metric{label1="value1"} 11
472 `,
473 want: map[string][]string{
474 "prometheus.myapp.test_gauge_metric": {"test_gauge_metric"},
475 },
476 },
477 }
478
479 for name, tc := range tests {
480 t.Run(name, func(t *testing.T) {
481 srv := httptest.NewServer(http.HandlerFunc(
482 func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(tc.input)) }))
483 defer srv.Close()
484
485 collr := tc.prepare()
486 collr.URL = srv.URL
487 require.NoError(t, collr.Init(context.Background()))
488
489 cc := cycle(t, collr.MetricStore())
490 cc.BeginCycle()
491 require.NoError(t, collr.Collect(context.Background()))
492 require.NoError(t, cc.CommitCycleSuccess())
493
494 collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{RequiredContexts: tc.want})
495 })
496 }
497 }