github.com/muhammadn/cortex@v1.9.1-0.20220510110439-46bb7000d03d/tools/querytee/response_comparator.go (about) 1 package querytee 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "math" 7 8 "github.com/go-kit/log/level" 9 "github.com/pkg/errors" 10 "github.com/prometheus/common/model" 11 12 util_log "github.com/cortexproject/cortex/pkg/util/log" 13 ) 14 15 // SamplesComparatorFunc helps with comparing different types of samples coming from /api/v1/query and /api/v1/query_range routes. 16 type SamplesComparatorFunc func(expected, actual json.RawMessage, tolerance float64) error 17 18 type SamplesResponse struct { 19 Status string 20 Data struct { 21 ResultType string 22 Result json.RawMessage 23 } 24 } 25 26 func NewSamplesComparator(tolerance float64) *SamplesComparator { 27 return &SamplesComparator{ 28 tolerance: tolerance, 29 sampleTypesComparator: map[string]SamplesComparatorFunc{ 30 "matrix": compareMatrix, 31 "vector": compareVector, 32 "scalar": compareScalar, 33 }, 34 } 35 } 36 37 type SamplesComparator struct { 38 tolerance float64 39 sampleTypesComparator map[string]SamplesComparatorFunc 40 } 41 42 // RegisterSamplesComparator helps with registering custom sample types 43 func (s *SamplesComparator) RegisterSamplesType(samplesType string, comparator SamplesComparatorFunc) { 44 s.sampleTypesComparator[samplesType] = comparator 45 } 46 47 func (s *SamplesComparator) Compare(expectedResponse, actualResponse []byte) error { 48 var expected, actual SamplesResponse 49 50 err := json.Unmarshal(expectedResponse, &expected) 51 if err != nil { 52 return errors.Wrap(err, "unable to unmarshal expected response") 53 } 54 55 err = json.Unmarshal(actualResponse, &actual) 56 if err != nil { 57 return errors.Wrap(err, "unable to unmarshal actual response") 58 } 59 60 if expected.Status != actual.Status { 61 return fmt.Errorf("expected status %s but got %s", expected.Status, actual.Status) 62 } 63 64 if expected.Data.ResultType != actual.Data.ResultType { 65 return fmt.Errorf("expected resultType %s but got %s", expected.Data.ResultType, actual.Data.ResultType) 66 } 67 68 comparator, ok := s.sampleTypesComparator[expected.Data.ResultType] 69 if !ok { 70 return fmt.Errorf("resultType %s not registered for comparison", expected.Data.ResultType) 71 } 72 73 return comparator(expected.Data.Result, actual.Data.Result, s.tolerance) 74 } 75 76 func compareMatrix(expectedRaw, actualRaw json.RawMessage, tolerance float64) error { 77 var expected, actual model.Matrix 78 79 err := json.Unmarshal(expectedRaw, &expected) 80 if err != nil { 81 return err 82 } 83 err = json.Unmarshal(actualRaw, &actual) 84 if err != nil { 85 return err 86 } 87 88 if len(expected) != len(actual) { 89 return fmt.Errorf("expected %d metrics but got %d", len(expected), 90 len(actual)) 91 } 92 93 metricFingerprintToIndexMap := make(map[model.Fingerprint]int, len(expected)) 94 for i, actualMetric := range actual { 95 metricFingerprintToIndexMap[actualMetric.Metric.Fingerprint()] = i 96 } 97 98 for _, expectedMetric := range expected { 99 actualMetricIndex, ok := metricFingerprintToIndexMap[expectedMetric.Metric.Fingerprint()] 100 if !ok { 101 return fmt.Errorf("expected metric %s missing from actual response", expectedMetric.Metric) 102 } 103 104 actualMetric := actual[actualMetricIndex] 105 expectedMetricLen := len(expectedMetric.Values) 106 actualMetricLen := len(actualMetric.Values) 107 108 if expectedMetricLen != actualMetricLen { 109 err := fmt.Errorf("expected %d samples for metric %s but got %d", expectedMetricLen, 110 expectedMetric.Metric, actualMetricLen) 111 if expectedMetricLen > 0 && actualMetricLen > 0 { 112 level.Error(util_log.Logger).Log("msg", err.Error(), "oldest-expected-ts", expectedMetric.Values[0].Timestamp, 113 "newest-expected-ts", expectedMetric.Values[expectedMetricLen-1].Timestamp, 114 "oldest-actual-ts", actualMetric.Values[0].Timestamp, "newest-actual-ts", actualMetric.Values[actualMetricLen-1].Timestamp) 115 } 116 return err 117 } 118 119 for i, expectedSamplePair := range expectedMetric.Values { 120 actualSamplePair := actualMetric.Values[i] 121 err := compareSamplePair(expectedSamplePair, actualSamplePair, tolerance) 122 if err != nil { 123 return errors.Wrapf(err, "sample pair not matching for metric %s", expectedMetric.Metric) 124 } 125 } 126 } 127 128 return nil 129 } 130 131 func compareVector(expectedRaw, actualRaw json.RawMessage, tolerance float64) error { 132 var expected, actual model.Vector 133 134 err := json.Unmarshal(expectedRaw, &expected) 135 if err != nil { 136 return err 137 } 138 139 err = json.Unmarshal(actualRaw, &actual) 140 if err != nil { 141 return err 142 } 143 144 if len(expected) != len(actual) { 145 return fmt.Errorf("expected %d metrics but got %d", len(expected), 146 len(actual)) 147 } 148 149 metricFingerprintToIndexMap := make(map[model.Fingerprint]int, len(expected)) 150 for i, actualMetric := range actual { 151 metricFingerprintToIndexMap[actualMetric.Metric.Fingerprint()] = i 152 } 153 154 for _, expectedMetric := range expected { 155 actualMetricIndex, ok := metricFingerprintToIndexMap[expectedMetric.Metric.Fingerprint()] 156 if !ok { 157 return fmt.Errorf("expected metric %s missing from actual response", expectedMetric.Metric) 158 } 159 160 actualMetric := actual[actualMetricIndex] 161 err := compareSamplePair(model.SamplePair{ 162 Timestamp: expectedMetric.Timestamp, 163 Value: expectedMetric.Value, 164 }, model.SamplePair{ 165 Timestamp: actualMetric.Timestamp, 166 Value: actualMetric.Value, 167 }, tolerance) 168 if err != nil { 169 return errors.Wrapf(err, "sample pair not matching for metric %s", expectedMetric.Metric) 170 } 171 } 172 173 return nil 174 } 175 176 func compareScalar(expectedRaw, actualRaw json.RawMessage, tolerance float64) error { 177 var expected, actual model.Scalar 178 err := json.Unmarshal(expectedRaw, &expected) 179 if err != nil { 180 return err 181 } 182 183 err = json.Unmarshal(actualRaw, &actual) 184 if err != nil { 185 return err 186 } 187 188 return compareSamplePair(model.SamplePair{ 189 Timestamp: expected.Timestamp, 190 Value: expected.Value, 191 }, model.SamplePair{ 192 Timestamp: actual.Timestamp, 193 Value: actual.Value, 194 }, tolerance) 195 } 196 197 func compareSamplePair(expected, actual model.SamplePair, tolerance float64) error { 198 if expected.Timestamp != actual.Timestamp { 199 return fmt.Errorf("expected timestamp %v but got %v", expected.Timestamp, actual.Timestamp) 200 } 201 if !compareSampleValue(expected.Value, actual.Value, tolerance) { 202 return fmt.Errorf("expected value %s for timestamp %v but got %s", expected.Value, expected.Timestamp, actual.Value) 203 } 204 205 return nil 206 } 207 208 func compareSampleValue(first, second model.SampleValue, tolerance float64) bool { 209 f := float64(first) 210 s := float64(second) 211 212 if math.IsNaN(f) && math.IsNaN(s) { 213 return true 214 } else if tolerance <= 0 { 215 return math.Float64bits(f) == math.Float64bits(s) 216 } 217 218 return math.Abs(f-s) <= tolerance 219 }