1
16
17 package testutil
18
19 import (
20 "fmt"
21 "math"
22 "reflect"
23 "testing"
24
25 "github.com/google/go-cmp/cmp"
26 "github.com/google/go-cmp/cmp/cmpopts"
27 dto "github.com/prometheus/client_model/go"
28 "k8s.io/component-base/metrics"
29 "k8s.io/utils/pointer"
30 )
31
32 func samples2Histogram(samples []float64, upperBounds []float64) Histogram {
33 histogram := dto.Histogram{
34 SampleCount: uint64Ptr(0),
35 SampleSum: pointer.Float64Ptr(0.0),
36 }
37
38 for _, ub := range upperBounds {
39 histogram.Bucket = append(histogram.Bucket, &dto.Bucket{
40 CumulativeCount: uint64Ptr(0),
41 UpperBound: pointer.Float64Ptr(ub),
42 })
43 }
44
45 for _, sample := range samples {
46 for i, bucket := range histogram.Bucket {
47 if sample < *bucket.UpperBound {
48 *histogram.Bucket[i].CumulativeCount++
49 }
50 }
51 *histogram.SampleCount++
52 *histogram.SampleSum += sample
53 }
54 return Histogram{
55 &histogram,
56 }
57 }
58
59 func TestHistogramQuantile(t *testing.T) {
60 tests := []struct {
61 name string
62 samples []float64
63 bounds []float64
64 q50 float64
65 q90 float64
66 q99 float64
67 }{
68 {
69 name: "Repeating numbers",
70 samples: []float64{0.5, 0.5, 0.5, 0.5, 1.5, 1.5, 1.5, 1.5, 3, 3, 3, 3, 6, 6, 6, 6},
71 bounds: []float64{1, 2, 4, 8},
72 q50: 2,
73 q90: 6.4,
74 q99: 7.84,
75 },
76 {
77 name: "Random numbers",
78 samples: []float64{11, 67, 61, 21, 40, 36, 52, 63, 8, 3, 67, 35, 61, 1, 36, 58},
79 bounds: []float64{10, 20, 40, 80},
80 q50: 40,
81 q90: 72,
82 q99: 79.2,
83 },
84 {
85 name: "The last bucket is empty",
86 samples: []float64{6, 34, 30, 10, 20, 18, 26, 31, 4, 2, 33, 17, 30, 1, 18, 29},
87 bounds: []float64{10, 20, 40, 80},
88 q50: 20,
89 q90: 36,
90 q99: 39.6,
91 },
92 {
93 name: "The last bucket has positive infinity upper bound",
94 samples: []float64{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 500},
95 bounds: []float64{10, 20, 40, math.Inf(1)},
96 q50: 5.3125,
97 q90: 9.5625,
98 q99: 40,
99 },
100 }
101
102 for _, test := range tests {
103 t.Run(test.name, func(t *testing.T) {
104 h := samples2Histogram(test.samples, test.bounds)
105 q50 := h.Quantile(0.5)
106 q90 := h.Quantile(0.9)
107 q99 := h.Quantile(0.99)
108 q999999 := h.Quantile(0.999999)
109
110 if q50 != test.q50 {
111 t.Errorf("Expected q50 to be %v, got %v instead", test.q50, q50)
112 }
113 if q90 != test.q90 {
114 t.Errorf("Expected q90 to be %v, got %v instead", test.q90, q90)
115 }
116 if q99 != test.q99 {
117 t.Errorf("Expected q99 to be %v, got %v instead", test.q99, q99)
118 }
119 lastUpperBound := test.bounds[len(test.bounds)-1]
120 if !(q999999 < lastUpperBound) {
121 t.Errorf("Expected q999999 to be less than %v, got %v instead", lastUpperBound, q999999)
122 }
123 })
124 }
125 }
126
127 func TestHistogramValidate(t *testing.T) {
128 tests := []struct {
129 name string
130 h Histogram
131 err error
132 }{
133 {
134 name: "nil SampleCount",
135 h: Histogram{
136 &dto.Histogram{},
137 },
138 err: fmt.Errorf("nil or empty histogram SampleCount"),
139 },
140 {
141 name: "empty SampleCount",
142 h: Histogram{
143 &dto.Histogram{
144 SampleCount: uint64Ptr(0),
145 },
146 },
147 err: fmt.Errorf("nil or empty histogram SampleCount"),
148 },
149 {
150 name: "nil SampleSum",
151 h: Histogram{
152 &dto.Histogram{
153 SampleCount: uint64Ptr(1),
154 },
155 },
156 err: fmt.Errorf("nil or empty histogram SampleSum"),
157 },
158 {
159 name: "empty SampleSum",
160 h: Histogram{
161 &dto.Histogram{
162 SampleCount: uint64Ptr(1),
163 SampleSum: pointer.Float64Ptr(0.0),
164 },
165 },
166 err: fmt.Errorf("nil or empty histogram SampleSum"),
167 },
168 {
169 name: "nil bucket",
170 h: Histogram{
171 &dto.Histogram{
172 SampleCount: uint64Ptr(1),
173 SampleSum: pointer.Float64Ptr(1.0),
174 Bucket: []*dto.Bucket{
175 nil,
176 },
177 },
178 },
179 err: fmt.Errorf("empty histogram bucket"),
180 },
181 {
182 name: "nil bucket UpperBound",
183 h: Histogram{
184 &dto.Histogram{
185 SampleCount: uint64Ptr(1),
186 SampleSum: pointer.Float64Ptr(1.0),
187 Bucket: []*dto.Bucket{
188 {},
189 },
190 },
191 },
192 err: fmt.Errorf("nil or negative histogram bucket UpperBound"),
193 },
194 {
195 name: "negative bucket UpperBound",
196 h: Histogram{
197 &dto.Histogram{
198 SampleCount: uint64Ptr(1),
199 SampleSum: pointer.Float64Ptr(1.0),
200 Bucket: []*dto.Bucket{
201 {UpperBound: pointer.Float64Ptr(-1.0)},
202 },
203 },
204 },
205 err: fmt.Errorf("nil or negative histogram bucket UpperBound"),
206 },
207 {
208 name: "valid histogram",
209 h: samples2Histogram(
210 []float64{0.5, 0.5, 0.5, 0.5, 1.5, 1.5, 1.5, 1.5, 3, 3, 3, 3, 6, 6, 6, 6},
211 []float64{1, 2, 4, 8},
212 ),
213 },
214 }
215
216 for _, test := range tests {
217 err := test.h.Validate()
218 if test.err != nil {
219 if err == nil || err.Error() != test.err.Error() {
220 t.Errorf("Expected %q error, got %q instead", test.err, err)
221 }
222 } else {
223 if err != nil {
224 t.Errorf("Expected error to be nil, got %q instead", err)
225 }
226 }
227 }
228 }
229
230 func TestLabelsMatch(t *testing.T) {
231 cases := []struct {
232 name string
233 metric *dto.Metric
234 labelFilter map[string]string
235 expectedMatch bool
236 }{
237 {name: "metric labels and labelFilter have the same labels and values", metric: &dto.Metric{
238 Label: []*dto.LabelPair{
239 {Name: pointer.StringPtr("a"), Value: pointer.StringPtr("1")},
240 {Name: pointer.StringPtr("b"), Value: pointer.StringPtr("2")},
241 {Name: pointer.StringPtr("c"), Value: pointer.StringPtr("3")},
242 }}, labelFilter: map[string]string{
243 "a": "1",
244 "b": "2",
245 "c": "3",
246 }, expectedMatch: true},
247 {name: "metric labels contain all labelFilter labels, and labelFilter is a subset of metric labels", metric: &dto.Metric{
248 Label: []*dto.LabelPair{
249 {Name: pointer.StringPtr("a"), Value: pointer.StringPtr("1")},
250 {Name: pointer.StringPtr("b"), Value: pointer.StringPtr("2")},
251 {Name: pointer.StringPtr("c"), Value: pointer.StringPtr("3")},
252 }}, labelFilter: map[string]string{
253 "a": "1",
254 "b": "2",
255 }, expectedMatch: true},
256 {name: "metric labels don't have all labelFilter labels and value", metric: &dto.Metric{
257 Label: []*dto.LabelPair{
258 {Name: pointer.StringPtr("a"), Value: pointer.StringPtr("1")},
259 {Name: pointer.StringPtr("b"), Value: pointer.StringPtr("2")},
260 }}, labelFilter: map[string]string{
261 "a": "1",
262 "b": "2",
263 "c": "3",
264 }, expectedMatch: false},
265 {name: "The intersection of metric labels and labelFilter labels is empty", metric: &dto.Metric{
266 Label: []*dto.LabelPair{
267 {Name: pointer.StringPtr("aa"), Value: pointer.StringPtr("11")},
268 {Name: pointer.StringPtr("bb"), Value: pointer.StringPtr("22")},
269 {Name: pointer.StringPtr("cc"), Value: pointer.StringPtr("33")},
270 }}, labelFilter: map[string]string{
271 "a": "1",
272 "b": "2",
273 "c": "3",
274 }, expectedMatch: false},
275 {name: "metric labels have the same labels names but different values with labelFilter labels and value", metric: &dto.Metric{
276 Label: []*dto.LabelPair{
277 {Name: pointer.StringPtr("a"), Value: pointer.StringPtr("1")},
278 {Name: pointer.StringPtr("b"), Value: pointer.StringPtr("2")},
279 {Name: pointer.StringPtr("c"), Value: pointer.StringPtr("3")},
280 }}, labelFilter: map[string]string{
281 "a": "11",
282 "b": "2",
283 "c": "3",
284 }, expectedMatch: false},
285 {name: "metric labels contain label name but different values with labelFilter labels and value", metric: &dto.Metric{
286 Label: []*dto.LabelPair{
287 {Name: pointer.StringPtr("a"), Value: pointer.StringPtr("1")},
288 {Name: pointer.StringPtr("b"), Value: pointer.StringPtr("2")},
289 {Name: pointer.StringPtr("c"), Value: pointer.StringPtr("33")},
290 {Name: pointer.StringPtr("d"), Value: pointer.StringPtr("4")},
291 }}, labelFilter: map[string]string{
292 "a": "1",
293 "b": "2",
294 "c": "3",
295 }, expectedMatch: false},
296 {name: "metric labels is empty and labelFilter is not empty", metric: &dto.Metric{
297 Label: []*dto.LabelPair{}}, labelFilter: map[string]string{
298 "a": "1",
299 "b": "2",
300 "c": "3",
301 }, expectedMatch: false},
302 {name: "metric labels is not empty and labelFilter is empty", metric: &dto.Metric{
303 Label: []*dto.LabelPair{
304 {Name: pointer.StringPtr("a"), Value: pointer.StringPtr("1")},
305 {Name: pointer.StringPtr("b"), Value: pointer.StringPtr("2")},
306 }}, labelFilter: map[string]string{}, expectedMatch: true},
307 }
308 for _, tt := range cases {
309 t.Run(tt.name, func(t *testing.T) {
310 got := LabelsMatch(tt.metric, tt.labelFilter)
311 if got != tt.expectedMatch {
312 t.Errorf("Expected %v, got %v instead", tt.expectedMatch, got)
313 }
314 })
315 }
316 }
317
318 func TestHistogramVec_GetAggregatedSampleCount(t *testing.T) {
319 tests := []struct {
320 name string
321 vec HistogramVec
322 want uint64
323 }{
324 {
325 name: "nil case",
326 want: 0,
327 },
328 {
329 name: "zero case",
330 vec: HistogramVec{
331 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(0), SampleSum: pointer.Float64Ptr(0.0)}},
332 },
333 want: 0,
334 },
335 {
336 name: "standard case",
337 vec: HistogramVec{
338 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(2.0)}},
339 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(2), SampleSum: pointer.Float64Ptr(4.0)}},
340 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(4), SampleSum: pointer.Float64Ptr(8.0)}},
341 },
342 want: 7,
343 },
344 {
345 name: "mixed case",
346 vec: HistogramVec{
347 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(2.0)}},
348 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(0), SampleSum: pointer.Float64Ptr(0.0)}},
349 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(2), SampleSum: pointer.Float64Ptr(4.0)}},
350 },
351 want: 3,
352 },
353 }
354
355 for _, tt := range tests {
356 t.Run(tt.name, func(t *testing.T) {
357 if got := tt.vec.GetAggregatedSampleCount(); got != tt.want {
358 t.Errorf("GetAggregatedSampleCount() = %v, want %v", got, tt.want)
359 }
360 })
361 }
362 }
363
364 func TestHistogramVec_GetAggregatedSampleSum(t *testing.T) {
365 tests := []struct {
366 name string
367 vec HistogramVec
368 want float64
369 }{
370 {
371 name: "nil case",
372 want: 0.0,
373 },
374 {
375 name: "zero case",
376 vec: HistogramVec{
377 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(0), SampleSum: pointer.Float64Ptr(0.0)}},
378 },
379 want: 0.0,
380 },
381 {
382 name: "standard case",
383 vec: HistogramVec{
384 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(2.0)}},
385 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(2), SampleSum: pointer.Float64Ptr(4.0)}},
386 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(4), SampleSum: pointer.Float64Ptr(8.0)}},
387 },
388 want: 14.0,
389 },
390 {
391 name: "mixed case",
392 vec: HistogramVec{
393 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(2.0)}},
394 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(0), SampleSum: pointer.Float64Ptr(0.0)}},
395 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(2), SampleSum: pointer.Float64Ptr(4.0)}},
396 },
397 want: 6.0,
398 },
399 }
400 for _, tt := range tests {
401 t.Run(tt.name, func(t *testing.T) {
402 if got := tt.vec.GetAggregatedSampleSum(); got != tt.want {
403 t.Errorf("GetAggregatedSampleSum() = %v, want %v", got, tt.want)
404 }
405 })
406 }
407 }
408
409 func TestHistogramVec_Quantile(t *testing.T) {
410 tests := []struct {
411 name string
412 samples [][]float64
413 bounds []float64
414 quantile float64
415 want []float64
416 }{
417 {
418 name: "duplicated histograms",
419 samples: [][]float64{
420 {0.5, 0.5, 0.5, 0.5, 1.5, 1.5, 1.5, 1.5, 3, 3, 3, 3, 6, 6, 6, 6},
421 {0.5, 0.5, 0.5, 0.5, 1.5, 1.5, 1.5, 1.5, 3, 3, 3, 3, 6, 6, 6, 6},
422 {0.5, 0.5, 0.5, 0.5, 1.5, 1.5, 1.5, 1.5, 3, 3, 3, 3, 6, 6, 6, 6},
423 },
424 bounds: []float64{1, 2, 4, 8},
425 want: []float64{2, 6.4, 7.2, 7.84},
426 },
427 {
428 name: "random numbers",
429 samples: [][]float64{
430 {8, 35, 47, 61, 56, 69, 66, 74, 35, 69, 5, 38, 58, 40, 36, 12},
431 {79, 44, 57, 46, 11, 8, 53, 77, 13, 35, 38, 47, 73, 16, 26, 29},
432 {51, 76, 22, 55, 20, 63, 59, 66, 34, 58, 64, 16, 79, 7, 58, 28},
433 },
434 bounds: []float64{10, 20, 40, 80},
435 want: []float64{44.44, 72.89, 76.44, 79.29},
436 },
437 {
438 name: "single histogram",
439 samples: [][]float64{
440 {6, 34, 30, 10, 20, 18, 26, 31, 4, 2, 33, 17, 30, 1, 18, 29},
441 },
442 bounds: []float64{10, 20, 40, 80},
443 want: []float64{20, 36, 38, 39.6},
444 },
445 }
446 for _, tt := range tests {
447 t.Run(tt.name, func(t *testing.T) {
448 var vec HistogramVec
449 for _, sample := range tt.samples {
450 histogram := samples2Histogram(sample, tt.bounds)
451 vec = append(vec, &histogram)
452 }
453 var got []float64
454 for _, q := range []float64{0.5, 0.9, 0.95, 0.99} {
455 got = append(got, math.Round(vec.Quantile(q)*100)/100)
456 }
457 if !reflect.DeepEqual(got, tt.want) {
458 t.Errorf("Quantile() = %v, want %v", got, tt.want)
459 }
460 })
461 }
462 }
463
464 func TestHistogramVec_Validate(t *testing.T) {
465 tests := []struct {
466 name string
467 vec HistogramVec
468 want error
469 }{
470 {
471 name: "nil SampleCount",
472 vec: HistogramVec{
473 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(1.0)}},
474 &Histogram{&dto.Histogram{SampleSum: pointer.Float64Ptr(2.0)}},
475 },
476 want: fmt.Errorf("nil or empty histogram SampleCount"),
477 },
478 {
479 name: "valid HistogramVec",
480 vec: HistogramVec{
481 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(1.0)}},
482 &Histogram{&dto.Histogram{SampleCount: uint64Ptr(2), SampleSum: pointer.Float64Ptr(2.0)}},
483 },
484 },
485 {
486 name: "different bucket size",
487 vec: HistogramVec{
488 &Histogram{&dto.Histogram{
489 SampleCount: uint64Ptr(4),
490 SampleSum: pointer.Float64Ptr(10.0),
491 Bucket: []*dto.Bucket{
492 {CumulativeCount: uint64Ptr(1), UpperBound: pointer.Float64Ptr(1)},
493 {CumulativeCount: uint64Ptr(2), UpperBound: pointer.Float64Ptr(2)},
494 {CumulativeCount: uint64Ptr(5), UpperBound: pointer.Float64Ptr(4)},
495 },
496 }},
497 &Histogram{&dto.Histogram{
498 SampleCount: uint64Ptr(3),
499 SampleSum: pointer.Float64Ptr(8.0),
500 Bucket: []*dto.Bucket{
501 {CumulativeCount: uint64Ptr(1), UpperBound: pointer.Float64Ptr(2)},
502 {CumulativeCount: uint64Ptr(3), UpperBound: pointer.Float64Ptr(4)},
503 },
504 }},
505 },
506 want: fmt.Errorf("found different bucket size: expect 3, but got 2 at index 1"),
507 },
508 }
509 for _, tt := range tests {
510 t.Run(tt.name, func(t *testing.T) {
511 if got := tt.vec.Validate(); fmt.Sprintf("%v", got) != fmt.Sprintf("%v", tt.want) {
512 t.Errorf("Validate() = %v, want %v", got, tt.want)
513 }
514 })
515 }
516 }
517
518 func TestGetHistogramVecFromGatherer(t *testing.T) {
519 tests := []struct {
520 name string
521 lvMap map[string]string
522 wantVec HistogramVec
523 }{
524 {
525 name: "filter with one label",
526 lvMap: map[string]string{"label1": "value1-0"},
527 wantVec: HistogramVec{
528 &Histogram{&dto.Histogram{
529 SampleCount: uint64Ptr(1),
530 SampleSum: pointer.Float64Ptr(1.5),
531 Bucket: []*dto.Bucket{
532 {CumulativeCount: uint64Ptr(0), UpperBound: pointer.Float64Ptr(0.5)},
533 {CumulativeCount: uint64Ptr(1), UpperBound: pointer.Float64Ptr(2.0)},
534 {CumulativeCount: uint64Ptr(1), UpperBound: pointer.Float64Ptr(5.0)},
535 },
536 }},
537 &Histogram{&dto.Histogram{
538 SampleCount: uint64Ptr(1),
539 SampleSum: pointer.Float64Ptr(2.5),
540 Bucket: []*dto.Bucket{
541 {CumulativeCount: uint64Ptr(0), UpperBound: pointer.Float64Ptr(0.5)},
542 {CumulativeCount: uint64Ptr(0), UpperBound: pointer.Float64Ptr(2.0)},
543 {CumulativeCount: uint64Ptr(1), UpperBound: pointer.Float64Ptr(5.0)},
544 },
545 }},
546 },
547 },
548 {
549 name: "filter with two labels",
550 lvMap: map[string]string{"label1": "value1-0", "label2": "value2-1"},
551 wantVec: HistogramVec{
552 &Histogram{&dto.Histogram{
553 SampleCount: uint64Ptr(1),
554 SampleSum: pointer.Float64Ptr(2.5),
555 Bucket: []*dto.Bucket{
556 {CumulativeCount: uint64Ptr(0), UpperBound: pointer.Float64Ptr(0.5)},
557 {CumulativeCount: uint64Ptr(0), UpperBound: pointer.Float64Ptr(2.0)},
558 {CumulativeCount: uint64Ptr(1), UpperBound: pointer.Float64Ptr(5.0)},
559 },
560 }},
561 },
562 },
563 }
564 for _, tt := range tests {
565 t.Run(tt.name, func(t *testing.T) {
566 buckets := []float64{.5, 2, 5}
567
568 labels := []string{"label1", "label2"}
569 HistogramOpts := &metrics.HistogramOpts{
570 Namespace: "namespace",
571 Name: "metric_test_name",
572 Subsystem: "subsystem",
573 Help: "histogram help message",
574 Buckets: buckets,
575 }
576 vec := metrics.NewHistogramVec(HistogramOpts, labels)
577
578 var registry = metrics.NewKubeRegistry()
579 var gather metrics.Gatherer = registry
580 registry.MustRegister(vec)
581
582 vec.WithLabelValues("value1-0", "value2-0").Observe(1.5)
583 vec.WithLabelValues("value1-0", "value2-1").Observe(2.5)
584 vec.WithLabelValues("value1-1", "value2-0").Observe(3.5)
585 vec.WithLabelValues("value1-1", "value2-1").Observe(4.5)
586 metricName := fmt.Sprintf("%s_%s_%s", HistogramOpts.Namespace, HistogramOpts.Subsystem, HistogramOpts.Name)
587 histogramVec, _ := GetHistogramVecFromGatherer(gather, metricName, tt.lvMap)
588 if diff := cmp.Diff(tt.wantVec, histogramVec, cmpopts.IgnoreFields(dto.Histogram{}, "state", "sizeCache", "unknownFields"), cmpopts.IgnoreFields(dto.Bucket{}, "state", "sizeCache", "unknownFields")); diff != "" {
589 t.Errorf("Got unexpected HistogramVec (-want +got):\n%s", diff)
590 }
591 })
592 }
593 }
594
View as plain text