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