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  }