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  }