Skip to content

Commit df345c1

Browse files
authored
test: refactor prometheus_metrics_test.go (#7063)
Refactors the prometheus_metrics sequential e2e test to rely on polling with a conditional instead of sleeping. We've noticed flakes with this test where the magic sleep timings don't play well on certain machines and platforms. Also adds KedaEventually which acts like ginkgo.Eventually, which allows to poll a condition until it is true. Signed-off-by: Max Cao <macao@redhat.com>
1 parent 470aeb3 commit df345c1

2 files changed

Lines changed: 157 additions & 97 deletions

File tree

‎tests/helper/helper.go‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ const (
7070
StrimziNamespace = "strimzi"
7171
)
7272

73+
const (
74+
WaitShort = 20 * time.Second
75+
IntervalShort = 1 * time.Second
76+
)
77+
7378
const (
7479
caCrtPath = "/tmp/keda-e2e-ca.crt"
7580
caKeyPath = "/tmp/keda-e2e-ca.key"
@@ -1041,6 +1046,42 @@ func CheckKubectlGetResult(t *testing.T, kind string, name string, namespace str
10411046
assert.Equal(t, expected, unqoutedOutput)
10421047
}
10431048

1049+
// KedaEventually checks if the provided conditionFunc eventually returns true
1050+
// (and no error) within the context's deadline. It polls the conditionFunc
1051+
// at the given interval until the condition is met or the context times out.
1052+
func KedaEventually(ctx context.Context, conditionFunc wait.ConditionWithContextFunc, interval time.Duration) error {
1053+
if interval <= 0 {
1054+
return fmt.Errorf("polling interval must be positive, got %v", interval)
1055+
}
1056+
1057+
ok, err := conditionFunc(ctx)
1058+
if err != nil {
1059+
return fmt.Errorf("eventually check failed on initial check: %w", err)
1060+
}
1061+
if ok {
1062+
return nil
1063+
}
1064+
1065+
ticker := time.NewTicker(interval)
1066+
defer ticker.Stop()
1067+
1068+
for {
1069+
select {
1070+
case <-ctx.Done():
1071+
return fmt.Errorf("eventually check failed: context deadline exceeded before condition was met")
1072+
1073+
case <-ticker.C:
1074+
ok, err := conditionFunc(ctx)
1075+
if err != nil {
1076+
return fmt.Errorf("eventually check failed during polling: %w", err)
1077+
}
1078+
if ok {
1079+
return nil
1080+
}
1081+
}
1082+
}
1083+
}
1084+
10441085
// KedaConsistently checks if the provided conditionFunc consistently returns true
10451086
// (and no error) for the entire duration specified by the context's deadline.
10461087
// It polls the conditionFunc at the given interval.

‎tests/sequential/prometheus_metrics/prometheus_metrics_test.go‎

Lines changed: 116 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"os/exec"
1212
"strings"
1313
"testing"
14-
"time"
1514

1615
prommodel "github.com/prometheus/client_model/go"
1716
"github.com/prometheus/common/expfmt"
@@ -486,7 +485,7 @@ func getTemplateData() (templateData, []Template) {
486485
}
487486

488487
func fetchAndParsePrometheusMetrics(t *testing.T, cmd string) map[string]*prommodel.MetricFamily {
489-
out, _, err := ExecCommandOnSpecificPod(t, clientName, testNamespace, cmd)
488+
out, _, err := ExecCommandOnSpecificPodWithoutTTY(t, clientName, testNamespace, cmd)
490489
assert.NoErrorf(t, err, "cannot execute command - %s", err)
491490

492491
parser := expfmt.TextParser{}
@@ -498,6 +497,33 @@ func fetchAndParsePrometheusMetrics(t *testing.T, cmd string) map[string]*prommo
498497
return families
499498
}
500499

500+
// WaitForPrometheusMetric waits for a specific metric to appear in the Prometheus metrics endpoint
501+
// and validates that the MetricFamily it has certain conditions using the provided familyValidator function.
502+
// Returns the parsed MetricFamily.
503+
func WaitForPrometheusMetric(t *testing.T, metricToWaitFor string, familyValidator func(family *prommodel.MetricFamily) bool) map[string]*prommodel.MetricFamily {
504+
contextWithTimeout, cancel := context.WithTimeout(context.Background(), WaitShort)
505+
defer cancel()
506+
var family map[string]*prommodel.MetricFamily
507+
err := KedaEventually(contextWithTimeout, func(ctx context.Context) (bool, error) {
508+
t.Logf("Waiting for metric %s", metricToWaitFor)
509+
family = fetchAndParsePrometheusMetrics(t, fmt.Sprintf("curl --insecure %s", kedaOperatorPrometheusURL))
510+
511+
if _, ok := family[metricToWaitFor]; ok {
512+
if familyValidator(family[metricToWaitFor]) {
513+
return true, nil
514+
}
515+
return false, nil
516+
}
517+
return false, nil
518+
}, IntervalShort)
519+
520+
if err != nil {
521+
t.Errorf("error waiting for metric %s: %v", metricToWaitFor, err)
522+
}
523+
524+
return family
525+
}
526+
501527
func testScalerMetricValue(t *testing.T) {
502528
t.Log("--- testing scaler metric value ---")
503529

@@ -528,110 +554,78 @@ func testScaledObjectErrors(t *testing.T, data templateData) {
528554
t.Log("--- testing scaled object errors ---")
529555

530556
KubectlDeleteWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate)
531-
time.Sleep(2 * time.Second)
532557
KubectlApplyWithTemplate(t, data, "wrongScaledObjectTemplate", wrongScaledObjectTemplate)
533558

534-
// wait for 2 seconds as pollinginterval is 2
535-
time.Sleep(20 * time.Second)
559+
WaitForPrometheusMetric(t, "keda_scaled_object_errors_total", func(family *prommodel.MetricFamily) bool {
560+
errCounterVal1 := getErrorMetricsValue(family)
536561

537-
family := fetchAndParsePrometheusMetrics(t, fmt.Sprintf("curl --insecure %s", kedaOperatorPrometheusURL))
538-
val, ok := family["keda_scaled_object_errors_total"]
539-
assert.True(t, ok, "keda_scaled_object_errors_total not available")
540-
if ok {
541-
errCounterVal1 := getErrorMetricsValue(val)
562+
// in the nested wait, we are just waiting for errCounterVal2 to eventually be greater that the first errCounterVal1
563+
metrics2 := WaitForPrometheusMetric(t, "keda_scaled_object_errors_total", func(family *prommodel.MetricFamily) bool {
564+
errCounterVal2 := getErrorMetricsValue(family)
565+
return errCounterVal2 > errCounterVal1 && errCounterVal2 > 0
566+
})
542567

543-
// wait for 2 seconds as pollinginterval is 2
544-
time.Sleep(2 * time.Second)
568+
errCounterVal2 := getErrorMetricsValue(metrics2["keda_scaled_object_errors_total"])
545569

546-
family = fetchAndParsePrometheusMetrics(t, fmt.Sprintf("curl --insecure %s", kedaOperatorPrometheusURL))
547-
val, ok := family["keda_scaled_object_errors_total"]
548-
assert.True(t, ok, "keda_scaled_object_errors_total not available")
549-
if ok {
550-
errCounterVal2 := getErrorMetricsValue(val)
551-
assert.NotEqual(t, errCounterVal2, float64(0))
552-
assert.GreaterOrEqual(t, errCounterVal2, errCounterVal1)
553-
}
554-
}
570+
// we don't have to check again, but extra validation is fine
571+
return errCounterVal2 > errCounterVal1 && errCounterVal2 > 0
572+
})
555573

556574
KubectlDeleteWithTemplate(t, data, "wrongScaledObjectTemplate", wrongScaledObjectTemplate)
557-
time.Sleep(2 * time.Second)
558575
KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate)
559576
}
560577

561578
func testScaledJobErrors(t *testing.T, data templateData) {
562579
t.Log("--- testing scaled job errors ---")
563580

564581
KubectlDeleteWithTemplate(t, data, "scaledJobTemplate", scaledJobTemplate)
565-
time.Sleep(2 * time.Second)
566582
KubectlApplyWithTemplate(t, data, "wrongScaledJobTemplate", wrongScaledJobTemplate)
567583

568-
// wait for 2 seconds as pollinginterval is 2
569-
time.Sleep(20 * time.Second)
584+
WaitForPrometheusMetric(t, "keda_scaled_job_errors_total", func(family *prommodel.MetricFamily) bool {
585+
errCounterVal1 := getErrorMetricsValue(family)
570586

571-
family := fetchAndParsePrometheusMetrics(t, fmt.Sprintf("curl --insecure %s", kedaOperatorPrometheusURL))
572-
val, ok := family["keda_scaled_job_errors_total"]
573-
assert.True(t, ok, "keda_scaled_job_errors_total not available")
574-
if ok {
575-
errCounterVal1 := getErrorMetricsValue(val)
587+
// in the nested wait, we are just waiting for errCounterVal2 to eventually be greater that the first errCounterVal1
588+
metrics2 := WaitForPrometheusMetric(t, "keda_scaled_job_errors_total", func(family *prommodel.MetricFamily) bool {
589+
errCounterVal2 := getErrorMetricsValue(family)
590+
return errCounterVal2 > errCounterVal1 && errCounterVal2 > 0
591+
})
576592

577-
// wait for 2 seconds as pollinginterval is 2
578-
time.Sleep(2 * time.Second)
593+
errCounterVal2 := getErrorMetricsValue(metrics2["keda_scaled_job_errors_total"])
579594

580-
family = fetchAndParsePrometheusMetrics(t, fmt.Sprintf("curl --insecure %s", kedaOperatorPrometheusURL))
581-
val, ok := family["keda_scaled_job_errors_total"]
582-
assert.True(t, ok, "keda_scaled_job_errors_total not available")
583-
if ok {
584-
errCounterVal2 := getErrorMetricsValue(val)
585-
assert.NotEqual(t, errCounterVal2, float64(0))
586-
assert.GreaterOrEqual(t, errCounterVal2, errCounterVal1)
587-
} else {
588-
t.Errorf("metric keda_scaled_job_errors_total not available")
589-
}
590-
} else {
591-
t.Errorf("metric keda_scaled_job_errors_total not available")
592-
}
595+
// we don't have to check again, but extra validation is fine
596+
return errCounterVal2 > errCounterVal1 && errCounterVal2 > 0
597+
})
593598

594599
KubectlDeleteWithTemplate(t, data, "wrongScaledJobTemplate", wrongScaledJobTemplate)
595-
time.Sleep(2 * time.Second)
596600
KubectlApplyWithTemplate(t, data, "scaledJobTemplate", scaledJobTemplate)
597601
}
598602

599603
func testScalerErrors(t *testing.T, data templateData) {
600604
t.Log("--- testing scaler errors ---")
601605

602606
KubectlDeleteWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate)
603-
time.Sleep(2 * time.Second)
604607
KubectlApplyWithTemplate(t, data, "wrongScaledObjectTemplate", wrongScaledObjectTemplate)
605-
606608
KubectlDeleteWithTemplate(t, data, "scaledJobTemplate", scaledJobTemplate)
607-
time.Sleep(2 * time.Second)
608609
KubectlApplyWithTemplate(t, data, "wrongScaledJobTemplate", wrongScaledJobTemplate)
609610

610-
family := fetchAndParsePrometheusMetrics(t, fmt.Sprintf("curl --insecure %s", kedaOperatorPrometheusURL))
611-
val, ok := family["keda_scaler_detail_errors_total"]
612-
assert.True(t, ok, "keda_scaler_detail_errors_total not available")
613-
if ok {
614-
errCounterVal1 := getErrorMetricsValue(val)
611+
WaitForPrometheusMetric(t, "keda_scaler_detail_errors_total", func(family *prommodel.MetricFamily) bool {
612+
errCounterVal1 := getErrorMetricsValue(family)
615613

616-
// wait for 20 seconds to correctly fetch metrics.
617-
time.Sleep(20 * time.Second)
614+
// in the nested wait, we are just waiting for errCounterVal2 to eventually be greater that the first errCounterVal1
615+
metrics2 := WaitForPrometheusMetric(t, "keda_scaler_detail_errors_total", func(family *prommodel.MetricFamily) bool {
616+
errCounterVal2 := getErrorMetricsValue(family)
617+
return errCounterVal2 > errCounterVal1 && errCounterVal2 > 0
618+
})
618619

619-
family = fetchAndParsePrometheusMetrics(t, fmt.Sprintf("curl --insecure %s", kedaOperatorPrometheusURL))
620-
val, ok := family["keda_scaler_detail_errors_total"]
621-
assert.True(t, ok, "keda_scaler_detail_errors_total not available")
622-
if ok {
623-
errCounterVal2 := getErrorMetricsValue(val)
624-
assert.NotEqual(t, errCounterVal2, float64(0))
625-
assert.GreaterOrEqual(t, errCounterVal2, errCounterVal1)
626-
}
627-
}
620+
errCounterVal2 := getErrorMetricsValue(metrics2["keda_scaler_detail_errors_total"])
621+
622+
// we don't have to check again, but extra validation is fine
623+
return errCounterVal2 > errCounterVal1 && errCounterVal2 > 0
624+
})
628625

629626
KubectlDeleteWithTemplate(t, data, "wrongScaledJobTemplate", wrongScaledJobTemplate)
630-
time.Sleep(2 * time.Second)
631627
KubectlApplyWithTemplate(t, data, "scaledJobTemplate", scaledJobTemplate)
632-
633628
KubectlDeleteWithTemplate(t, data, "wrongScaledObjectTemplate", wrongScaledObjectTemplate)
634-
time.Sleep(2 * time.Second)
635629
KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate)
636630
}
637631

@@ -827,18 +821,41 @@ func testScaledObjectPausedMetric(t *testing.T, data templateData) {
827821

828822
// Pause the ScaledObject
829823
KubectlApplyWithTemplate(t, data, "scaledObjectPausedTemplate", scaledObjectPausedTemplate)
830-
time.Sleep(20 * time.Second)
831824

832825
// Check that the paused metric is now true
833-
families := fetchAndParsePrometheusMetrics(t, fmt.Sprintf("curl --insecure %s", kedaOperatorPrometheusURL))
826+
families := WaitForPrometheusMetric(t, "keda_scaled_object_paused", func(family *prommodel.MetricFamily) bool {
827+
metricValue := 0.0
828+
metrics := family.GetMetric()
829+
for _, metric := range metrics {
830+
labels := metric.GetLabel()
831+
for _, label := range labels {
832+
if *label.Name == labelScaledObject && *label.Value == scaledObjectName {
833+
metricValue = *metric.Gauge.Value
834+
}
835+
}
836+
}
837+
838+
return metricValue == float64(1)
839+
})
834840
assertScaledObjectPausedMetric(t, families, scaledObjectName, true)
835841

836842
// Unpause the ScaledObject
837843
KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate)
838-
time.Sleep(20 * time.Second)
839844

840845
// Check that the paused metric is back to false
841-
families = fetchAndParsePrometheusMetrics(t, fmt.Sprintf("curl --insecure %s", kedaOperatorPrometheusURL))
846+
families = WaitForPrometheusMetric(t, "keda_scaled_object_paused", func(family *prommodel.MetricFamily) bool {
847+
metricValue := 0.0
848+
metrics := family.GetMetric()
849+
for _, metric := range metrics {
850+
labels := metric.GetLabel()
851+
for _, label := range labels {
852+
if *label.Name == labelScaledObject && *label.Value == scaledObjectName {
853+
metricValue = *metric.Gauge.Value
854+
}
855+
}
856+
}
857+
return metricValue == float64(0)
858+
})
842859
assertScaledObjectPausedMetric(t, families, scaledObjectName, false)
843860
}
844861

@@ -1350,62 +1367,64 @@ func testCloudEventEmitted(t *testing.T, data templateData) {
13501367
t.Log("--- testing cloudevent emitted ---")
13511368

13521369
KubectlDeleteWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate)
1370+
13531371
KubectlApplyWithTemplate(t, data, "cloudEventSourceTemplate", cloudEventSourceTemplate)
13541372
KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate)
13551373

1356-
family := fetchAndParsePrometheusMetrics(t, fmt.Sprintf("curl --insecure %s", kedaOperatorPrometheusURL))
1374+
defer KubectlDeleteWithTemplate(t, data, "cloudEventSourceTemplate", cloudEventSourceTemplate)
13571375

1358-
if val, ok := family["keda_cloudeventsource_events_emitted_total"]; ok {
1359-
var found bool
1360-
metrics := val.GetMetric()
1361-
for _, metric := range metrics {
1376+
familyValidator := func(family *prommodel.MetricFamily) bool {
1377+
labels := family.GetMetric()
1378+
found := false
1379+
for _, metric := range labels {
13621380
labels := metric.GetLabel()
13631381
if len(labels) >= 4 &&
13641382
ExtractPrometheusLabelValue("cloudeventsource", labels) == "prometheus-metrics-test-ce" &&
13651383
ExtractPrometheusLabelValue("eventsink", labels) == "http" &&
13661384
ExtractPrometheusLabelValue("namespace", labels) == "prometheus-metrics-test-ns" &&
1367-
ExtractPrometheusLabelValue("state", labels) == "emitted" {
1368-
assert.GreaterOrEqual(t, *metric.Counter.Value, float64(1))
1385+
ExtractPrometheusLabelValue("state", labels) == "emitted" &&
1386+
metric.GetCounter().GetValue() >= 1 {
13691387
found = true
13701388
}
13711389
}
1372-
assert.Equal(t, true, found)
1373-
} else {
1374-
t.Errorf("metric not available")
1390+
return found
13751391
}
1392+
1393+
families := WaitForPrometheusMetric(t, "keda_cloudeventsource_events_emitted_total", familyValidator)
1394+
metric := families["keda_cloudeventsource_events_emitted_total"]
1395+
1396+
assert.True(t, familyValidator(metric))
13761397
}
13771398

13781399
func testCloudEventEmittedError(t *testing.T, data templateData) {
13791400
t.Log("--- testing cloudevent emitted error ---")
13801401

13811402
KubectlDeleteWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate)
1382-
KubectlDeleteWithTemplate(t, data, "cloudEventSourceTemplate", cloudEventSourceTemplate)
1403+
13831404
KubectlApplyWithTemplate(t, data, "wrongCloudEventSourceTemplate", wrongCloudEventSourceTemplate)
1384-
time.Sleep(1 * time.Second)
13851405
KubectlApplyWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate)
1386-
time.Sleep(5 * time.Second)
13871406

1388-
family := fetchAndParsePrometheusMetrics(t, fmt.Sprintf("curl --insecure %s", kedaOperatorPrometheusURL))
1407+
defer KubectlDeleteWithTemplate(t, data, "wrongCloudEventSourceTemplate", wrongCloudEventSourceTemplate)
13891408

1390-
if val, ok := family["keda_cloudeventsource_events_emitted_total"]; ok {
1391-
var found bool
1392-
metrics := val.GetMetric()
1393-
for _, metric := range metrics {
1409+
familyValidator := func(family *prommodel.MetricFamily) bool {
1410+
labels := family.GetMetric()
1411+
found := false
1412+
for _, metric := range labels {
13941413
labels := metric.GetLabel()
13951414
if len(labels) >= 4 &&
13961415
ExtractPrometheusLabelValue("cloudeventsource", labels) == "prometheus-metrics-test-ce-w" &&
13971416
ExtractPrometheusLabelValue("eventsink", labels) == "http" &&
13981417
ExtractPrometheusLabelValue("namespace", labels) == "prometheus-metrics-test-ns" &&
1399-
ExtractPrometheusLabelValue("state", labels) == "failed" {
1400-
assert.GreaterOrEqual(t, *metric.Counter.Value, float64(5))
1418+
ExtractPrometheusLabelValue("state", labels) == "failed" &&
1419+
metric.GetCounter().GetValue() >= 5 {
14011420
found = true
14021421
}
14031422
}
1404-
assert.Equal(t, true, found)
1405-
} else {
1406-
t.Errorf("metric not available")
1423+
return found
14071424
}
14081425

1409-
KubectlDeleteWithTemplate(t, data, "wrongCloudEventSourceTemplate", wrongCloudEventSourceTemplate)
1410-
KubectlApplyWithTemplate(t, data, "cloudEventSourceTemplate", cloudEventSourceTemplate)
1426+
families := WaitForPrometheusMetric(t, "keda_cloudeventsource_events_emitted_total", familyValidator)
1427+
metric := families["keda_cloudeventsource_events_emitted_total"]
1428+
1429+
assert.True(t, familyValidator(metric))
14111430
}

0 commit comments

Comments
 (0)