package queryrangebase import ( "bytes" "context" "io" "net/http" "strconv" "testing" "time" jsoniter "github.com/json-iterator/go" "github.com/opentracing/opentracing-go/mocktracer" "github.com/prometheus/prometheus/model/timestamp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/loki/v3/pkg/logproto" ) 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: io.NopCloser(bytes.NewBuffer([]byte(tc.body))), } resp, err := PrometheusCodecForRangeQueries.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: io.NopCloser(bytes.NewBuffer([]byte(tc.body))), ContentLength: int64(len(tc.body)), } resp2, err := PrometheusCodecForRangeQueries.EncodeResponse(context.Background(), nil, 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{ Warnings: []string{"warning1", "warning2"}, Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{ { Labels: []logproto.LabelAdapter{}, Samples: []logproto.LegacySample{ {Value: 0, TimestampMs: 0}, {Value: 1, TimestampMs: 1}, }, }, }, }, }, &PrometheusResponse{ Warnings: []string{"warning2", "warning3"}, Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{ { Labels: []logproto.LabelAdapter{}, Samples: []logproto.LegacySample{ {Value: 2, TimestampMs: 2}, {Value: 3, TimestampMs: 3}, }, }, }, }, }, }, expected: &PrometheusResponse{ Status: StatusSuccess, Warnings: []string{"warning1", "warning2", "warning3"}, Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{ { Labels: []logproto.LabelAdapter{}, Samples: []logproto.LegacySample{ {Value: 0, TimestampMs: 0}, {Value: 1, TimestampMs: 1}, {Value: 2, TimestampMs: 2}, {Value: 3, TimestampMs: 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.LegacySample{ {Value: 0, TimestampMs: 0}, {Value: 1, TimestampMs: 1000}, {Value: 2, TimestampMs: 2000}, {Value: 3, TimestampMs: 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.LegacySample{ {Value: 1, TimestampMs: 1000}, {Value: 2, TimestampMs: 2000}, {Value: 3, TimestampMs: 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.LegacySample{ {Value: 1, TimestampMs: 1000}, {Value: 2, TimestampMs: 2000}, {Value: 3, TimestampMs: 3000}, {Value: 4, TimestampMs: 4000}, {Value: 5, TimestampMs: 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.LegacySample{ {Value: 2, TimestampMs: 2000}, {Value: 3, TimestampMs: 3000}, {Value: 4, TimestampMs: 4000}, {Value: 5, TimestampMs: 5000}, }, }, }, }, }, }} { t.Run(tc.name, func(t *testing.T) { output, err := PrometheusCodecForRangeQueries.MergeResponse(tc.input...) require.NoError(t, err) require.Equal(t, tc.expected, output) }) } } func TestPrometheusRequestSpanLogging(t *testing.T) { now := time.Now() end := now.Add(1000 * time.Second) req := PrometheusRequest{ Start: now, End: end, } span := mocktracer.MockSpan{} req.LogToSpan(&span) for _, l := range span.Logs() { for _, field := range l.Fields { if field.Key == "start" { require.Equal(t, timestamp.Time(now.UnixMilli()).String(), field.ValueString) } if field.Key == "end" { require.Equal(t, timestamp.Time(end.UnixMilli()).String(), field.ValueString) } } } } 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 }