Like Prometheus, but for logs.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
loki/tools/querytee/response_comparator.go

233 lines
6.8 KiB

package querytee
import (
"encoding/json"
"fmt"
"math"
"time"
"github.com/go-kit/log/level"
"github.com/pkg/errors"
"github.com/prometheus/common/model"
util_log "github.com/grafana/loki/pkg/util/log"
)
// SamplesComparatorFunc helps with comparing different types of samples coming from /api/v1/query and /api/v1/query_range routes.
type SamplesComparatorFunc func(expected, actual json.RawMessage, opts SampleComparisonOptions) error
type SamplesResponse struct {
Status string
Data struct {
ResultType string
Result json.RawMessage
}
}
type SampleComparisonOptions struct {
Tolerance float64
UseRelativeError bool
SkipRecentSamples time.Duration
}
func NewSamplesComparator(opts SampleComparisonOptions) *SamplesComparator {
return &SamplesComparator{
opts: opts,
sampleTypesComparator: map[string]SamplesComparatorFunc{
"matrix": compareMatrix,
"vector": compareVector,
"scalar": compareScalar,
},
}
}
type SamplesComparator struct {
opts SampleComparisonOptions
sampleTypesComparator map[string]SamplesComparatorFunc
}
// RegisterSamplesComparator helps with registering custom sample types
func (s *SamplesComparator) RegisterSamplesType(samplesType string, comparator SamplesComparatorFunc) {
s.sampleTypesComparator[samplesType] = comparator
}
func (s *SamplesComparator) Compare(expectedResponse, actualResponse []byte) error {
var expected, actual SamplesResponse
err := json.Unmarshal(expectedResponse, &expected)
if err != nil {
return errors.Wrap(err, "unable to unmarshal expected response")
}
err = json.Unmarshal(actualResponse, &actual)
if err != nil {
return errors.Wrap(err, "unable to unmarshal actual response")
}
if expected.Status != actual.Status {
return fmt.Errorf("expected status %s but got %s", expected.Status, actual.Status)
}
if expected.Data.ResultType != actual.Data.ResultType {
return fmt.Errorf("expected resultType %s but got %s", expected.Data.ResultType, actual.Data.ResultType)
}
comparator, ok := s.sampleTypesComparator[expected.Data.ResultType]
if !ok {
return fmt.Errorf("resultType %s not registered for comparison", expected.Data.ResultType)
}
return comparator(expected.Data.Result, actual.Data.Result, s.opts)
}
func compareMatrix(expectedRaw, actualRaw json.RawMessage, opts SampleComparisonOptions) error {
var expected, actual model.Matrix
err := json.Unmarshal(expectedRaw, &expected)
if err != nil {
return err
}
err = json.Unmarshal(actualRaw, &actual)
if err != nil {
return err
}
if len(expected) != len(actual) {
return fmt.Errorf("expected %d metrics but got %d", len(expected),
len(actual))
}
metricFingerprintToIndexMap := make(map[model.Fingerprint]int, len(expected))
for i, actualMetric := range actual {
metricFingerprintToIndexMap[actualMetric.Metric.Fingerprint()] = i
}
for _, expectedMetric := range expected {
actualMetricIndex, ok := metricFingerprintToIndexMap[expectedMetric.Metric.Fingerprint()]
if !ok {
return fmt.Errorf("expected metric %s missing from actual response", expectedMetric.Metric)
}
actualMetric := actual[actualMetricIndex]
expectedMetricLen := len(expectedMetric.Values)
actualMetricLen := len(actualMetric.Values)
if expectedMetricLen != actualMetricLen {
err := fmt.Errorf("expected %d samples for metric %s but got %d", expectedMetricLen,
expectedMetric.Metric, actualMetricLen)
if expectedMetricLen > 0 && actualMetricLen > 0 {
level.Error(util_log.Logger).Log("msg", err.Error(), "oldest-expected-ts", expectedMetric.Values[0].Timestamp,
"newest-expected-ts", expectedMetric.Values[expectedMetricLen-1].Timestamp,
"oldest-actual-ts", actualMetric.Values[0].Timestamp, "newest-actual-ts", actualMetric.Values[actualMetricLen-1].Timestamp)
}
return err
}
for i, expectedSamplePair := range expectedMetric.Values {
actualSamplePair := actualMetric.Values[i]
err := compareSamplePair(expectedSamplePair, actualSamplePair, opts)
if err != nil {
return errors.Wrapf(err, "sample pair not matching for metric %s", expectedMetric.Metric)
}
}
}
return nil
}
func compareVector(expectedRaw, actualRaw json.RawMessage, opts SampleComparisonOptions) error {
var expected, actual model.Vector
err := json.Unmarshal(expectedRaw, &expected)
if err != nil {
return err
}
err = json.Unmarshal(actualRaw, &actual)
if err != nil {
return err
}
if len(expected) != len(actual) {
return fmt.Errorf("expected %d metrics but got %d", len(expected),
len(actual))
}
metricFingerprintToIndexMap := make(map[model.Fingerprint]int, len(expected))
for i, actualMetric := range actual {
metricFingerprintToIndexMap[actualMetric.Metric.Fingerprint()] = i
}
for _, expectedMetric := range expected {
actualMetricIndex, ok := metricFingerprintToIndexMap[expectedMetric.Metric.Fingerprint()]
if !ok {
return fmt.Errorf("expected metric %s missing from actual response", expectedMetric.Metric)
}
actualMetric := actual[actualMetricIndex]
err := compareSamplePair(model.SamplePair{
Timestamp: expectedMetric.Timestamp,
Value: expectedMetric.Value,
}, model.SamplePair{
Timestamp: actualMetric.Timestamp,
Value: actualMetric.Value,
}, opts)
if err != nil {
return errors.Wrapf(err, "sample pair not matching for metric %s", expectedMetric.Metric)
}
}
return nil
}
func compareScalar(expectedRaw, actualRaw json.RawMessage, opts SampleComparisonOptions) error {
var expected, actual model.Scalar
err := json.Unmarshal(expectedRaw, &expected)
if err != nil {
return err
}
err = json.Unmarshal(actualRaw, &actual)
if err != nil {
return err
}
return compareSamplePair(model.SamplePair{
Timestamp: expected.Timestamp,
Value: expected.Value,
}, model.SamplePair{
Timestamp: actual.Timestamp,
Value: actual.Value,
}, opts)
}
func compareSamplePair(expected, actual model.SamplePair, opts SampleComparisonOptions) error {
if expected.Timestamp != actual.Timestamp {
return fmt.Errorf("expected timestamp %v but got %v", expected.Timestamp, actual.Timestamp)
}
if opts.SkipRecentSamples > 0 && time.Since(expected.Timestamp.Time()) < opts.SkipRecentSamples {
return nil
}
if !compareSampleValue(expected.Value, actual.Value, opts) {
return fmt.Errorf("expected value %s for timestamp %v but got %s", expected.Value, expected.Timestamp, actual.Value)
}
return nil
}
func compareSampleValue(first, second model.SampleValue, opts SampleComparisonOptions) bool {
f := float64(first)
s := float64(second)
if (math.IsNaN(f) && math.IsNaN(s)) ||
(math.IsInf(f, 1) && math.IsInf(s, 1)) ||
(math.IsInf(f, -1) && math.IsInf(s, -1)) {
return true
} else if opts.Tolerance <= 0 {
return math.Float64bits(f) == math.Float64bits(s)
}
if opts.UseRelativeError && s != 0 {
return math.Abs(f-s)/math.Abs(s) <= opts.Tolerance
}
return math.Abs(f-s) <= opts.Tolerance
}