mirror of https://github.com/grafana/loki
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.
343 lines
9.6 KiB
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
|
|
}
|
|
|