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/pkg/querier/queryrange/queryrangebase/query_range_test.go

343 lines
9.6 KiB

package queryrangebase
import (
"bytes"
"context"
"io/ioutil"
"net/http"
"strconv"
"testing"
jsoniter "github.com/json-iterator/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/weaveworks/common/httpgrpc"
"github.com/weaveworks/common/user"
"github.com/grafana/loki/pkg/logproto"
)
func TestRequest(t *testing.T) {
// Create a Copy parsedRequest to assign the expected headers to the request without affecting other tests using the global.
// The test below adds a Test-Header header to the request and expects it back once the encode/decode of request is done via PrometheusCodec
parsedRequestWithHeaders := *parsedRequest
parsedRequestWithHeaders.Headers = reqHeaders
for i, tc := range []struct {
url string
expected Request
expectedErr error
}{
{
url: query,
expected: &parsedRequestWithHeaders,
},
{
url: "api/v1/query_range?start=foo",
expectedErr: httpgrpc.Errorf(http.StatusBadRequest, "invalid parameter \"start\"; cannot parse \"foo\" to a valid timestamp"),
},
{
url: "api/v1/query_range?start=123&end=bar",
expectedErr: httpgrpc.Errorf(http.StatusBadRequest, "invalid parameter \"end\"; cannot parse \"bar\" to a valid timestamp"),
},
{
url: "api/v1/query_range?start=123&end=0",
expectedErr: errEndBeforeStart,
},
{
url: "api/v1/query_range?start=123&end=456&step=baz",
expectedErr: httpgrpc.Errorf(http.StatusBadRequest, "invalid parameter \"step\"; cannot parse \"baz\" to a valid duration"),
},
{
url: "api/v1/query_range?start=123&end=456&step=-1",
expectedErr: errNegativeStep,
},
{
url: "api/v1/query_range?start=0&end=11001&step=1",
expectedErr: errStepTooSmall,
},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
r, err := http.NewRequest("GET", tc.url, nil)
require.NoError(t, err)
r.Header.Add("Test-Header", "test")
ctx := user.InjectOrgID(context.Background(), "1")
// Get a deep copy of the request with Context changed to ctx
r = r.Clone(ctx)
req, err := PrometheusCodec.DecodeRequest(ctx, r, []string{"Test-Header"})
if err != nil {
require.EqualValues(t, tc.expectedErr, err)
return
}
require.EqualValues(t, tc.expected, req)
rdash, err := PrometheusCodec.EncodeRequest(context.Background(), req)
require.NoError(t, err)
require.EqualValues(t, tc.url, rdash.RequestURI)
})
}
}
func TestResponse(t *testing.T) {
r := *parsedResponse
r.Headers = respHeaders
for i, tc := range []struct {
body string
expected *PrometheusResponse
}{
{
body: responseBody,
expected: &r,
},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
response := &http.Response{
StatusCode: 200,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: ioutil.NopCloser(bytes.NewBuffer([]byte(tc.body))),
}
resp, err := PrometheusCodec.DecodeResponse(context.Background(), response, nil)
require.NoError(t, err)
assert.Equal(t, tc.expected, resp)
// Reset response, as the above call will have consumed the body reader.
response = &http.Response{
StatusCode: 200,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: ioutil.NopCloser(bytes.NewBuffer([]byte(tc.body))),
ContentLength: int64(len(tc.body)),
}
resp2, err := PrometheusCodec.EncodeResponse(context.Background(), resp)
require.NoError(t, err)
assert.Equal(t, response, resp2)
})
}
}
func TestMergeAPIResponses(t *testing.T) {
for _, tc := range []struct {
name string
input []Response
expected Response
}{
{
name: "No responses shouldn't panic and return a non-null result and result type.",
input: []Response{},
expected: &PrometheusResponse{
Status: StatusSuccess,
Data: PrometheusData{
ResultType: matrix,
Result: []SampleStream{},
},
},
},
{
name: "A single empty response shouldn't panic.",
input: []Response{
&PrometheusResponse{
Data: PrometheusData{
ResultType: matrix,
Result: []SampleStream{},
},
},
},
expected: &PrometheusResponse{
Status: StatusSuccess,
Data: PrometheusData{
ResultType: matrix,
Result: []SampleStream{},
},
},
},
{
name: "Multiple empty responses shouldn't panic.",
input: []Response{
&PrometheusResponse{
Data: PrometheusData{
ResultType: matrix,
Result: []SampleStream{},
},
},
&PrometheusResponse{
Data: PrometheusData{
ResultType: matrix,
Result: []SampleStream{},
},
},
},
expected: &PrometheusResponse{
Status: StatusSuccess,
Data: PrometheusData{
ResultType: matrix,
Result: []SampleStream{},
},
},
},
{
name: "Basic merging of two responses.",
input: []Response{
&PrometheusResponse{
Data: PrometheusData{
ResultType: matrix,
Result: []SampleStream{
{
Labels: []logproto.LabelAdapter{},
Samples: []logproto.Sample{
{Value: 0, Timestamp: 0},
{Value: 1, Timestamp: 1},
},
},
},
},
},
&PrometheusResponse{
Data: PrometheusData{
ResultType: matrix,
Result: []SampleStream{
{
Labels: []logproto.LabelAdapter{},
Samples: []logproto.Sample{
{Value: 2, Timestamp: 2},
{Value: 3, Timestamp: 3},
},
},
},
},
},
},
expected: &PrometheusResponse{
Status: StatusSuccess,
Data: PrometheusData{
ResultType: matrix,
Result: []SampleStream{
{
Labels: []logproto.LabelAdapter{},
Samples: []logproto.Sample{
{Value: 0, Timestamp: 0},
{Value: 1, Timestamp: 1},
{Value: 2, Timestamp: 2},
{Value: 3, Timestamp: 3},
},
},
},
},
},
},
{
name: "Merging of responses when labels are in different order.",
input: []Response{
mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[0,"0"],[1,"1"]]}]}}`),
mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"c":"d","a":"b"},"values":[[2,"2"],[3,"3"]]}]}}`),
},
expected: &PrometheusResponse{
Status: StatusSuccess,
Data: PrometheusData{
ResultType: matrix,
Result: []SampleStream{
{
Labels: []logproto.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}},
Samples: []logproto.Sample{
{Value: 0, Timestamp: 0},
{Value: 1, Timestamp: 1000},
{Value: 2, Timestamp: 2000},
{Value: 3, Timestamp: 3000},
},
},
},
},
},
},
{
name: "Merging of samples where there is single overlap.",
input: []Response{
mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[1,"1"],[2,"2"]]}]}}`),
mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"c":"d","a":"b"},"values":[[2,"2"],[3,"3"]]}]}}`),
},
expected: &PrometheusResponse{
Status: StatusSuccess,
Data: PrometheusData{
ResultType: matrix,
Result: []SampleStream{
{
Labels: []logproto.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}},
Samples: []logproto.Sample{
{Value: 1, Timestamp: 1000},
{Value: 2, Timestamp: 2000},
{Value: 3, Timestamp: 3000},
},
},
},
},
},
},
{
name: "Merging of samples where there is multiple partial overlaps.",
input: []Response{
mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[1,"1"],[2,"2"],[3,"3"]]}]}}`),
mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"c":"d","a":"b"},"values":[[2,"2"],[3,"3"],[4,"4"],[5,"5"]]}]}}`),
},
expected: &PrometheusResponse{
Status: StatusSuccess,
Data: PrometheusData{
ResultType: matrix,
Result: []SampleStream{
{
Labels: []logproto.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}},
Samples: []logproto.Sample{
{Value: 1, Timestamp: 1000},
{Value: 2, Timestamp: 2000},
{Value: 3, Timestamp: 3000},
{Value: 4, Timestamp: 4000},
{Value: 5, Timestamp: 5000},
},
},
},
},
},
},
{
name: "Merging of samples where there is complete overlap.",
input: []Response{
mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[2,"2"],[3,"3"]]}]}}`),
mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"c":"d","a":"b"},"values":[[2,"2"],[3,"3"],[4,"4"],[5,"5"]]}]}}`),
},
expected: &PrometheusResponse{
Status: StatusSuccess,
Data: PrometheusData{
ResultType: matrix,
Result: []SampleStream{
{
Labels: []logproto.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}},
Samples: []logproto.Sample{
{Value: 2, Timestamp: 2000},
{Value: 3, Timestamp: 3000},
{Value: 4, Timestamp: 4000},
{Value: 5, Timestamp: 5000},
},
},
},
},
},
}} {
t.Run(tc.name, func(t *testing.T) {
output, err := PrometheusCodec.MergeResponse(tc.input...)
require.NoError(t, err)
require.Equal(t, tc.expected, output)
})
}
}
func mustParse(t *testing.T, response string) Response {
var resp PrometheusResponse
// Needed as goimports automatically add a json import otherwise.
json := jsoniter.ConfigCompatibleWithStandardLibrary
require.NoError(t, json.Unmarshal([]byte(response), &resp))
return &resp
}