github.com/thanos-io/thanos@v0.32.5/internal/cortex/querier/queryrange/query_range_test.go (about)

     1  // Copyright (c) The Cortex Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package queryrange
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"strconv"
    12  	"testing"
    13  
    14  	"github.com/prometheus/common/model"
    15  
    16  	jsoniter "github.com/json-iterator/go"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  	"github.com/weaveworks/common/httpgrpc"
    20  	"github.com/weaveworks/common/user"
    21  
    22  	"github.com/thanos-io/thanos/internal/cortex/cortexpb"
    23  )
    24  
    25  func TestRequest(t *testing.T) {
    26  	// Create a Copy parsedRequest to assign the expected headers to the request without affecting other tests using the global.
    27  	// 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
    28  	parsedRequestWithHeaders := *parsedRequest
    29  	parsedRequestWithHeaders.Headers = reqHeaders
    30  	for _, tc := range []struct {
    31  		url         string
    32  		expected    Request
    33  		expectedErr error
    34  	}{
    35  		{
    36  			url:      query,
    37  			expected: &parsedRequestWithHeaders,
    38  		},
    39  		{
    40  			url:         "api/v1/query_range?start=foo&stats=all",
    41  			expectedErr: httpgrpc.Errorf(http.StatusBadRequest, "invalid parameter \"start\"; cannot parse \"foo\" to a valid timestamp"),
    42  		},
    43  		{
    44  			url:         "api/v1/query_range?start=123&end=bar",
    45  			expectedErr: httpgrpc.Errorf(http.StatusBadRequest, "invalid parameter \"end\"; cannot parse \"bar\" to a valid timestamp"),
    46  		},
    47  		{
    48  			url:         "api/v1/query_range?start=123&end=0",
    49  			expectedErr: errEndBeforeStart,
    50  		},
    51  		{
    52  			url:         "api/v1/query_range?start=123&end=456&step=baz",
    53  			expectedErr: httpgrpc.Errorf(http.StatusBadRequest, "invalid parameter \"step\"; cannot parse \"baz\" to a valid duration"),
    54  		},
    55  		{
    56  			url:         "api/v1/query_range?start=123&end=456&step=-1",
    57  			expectedErr: errNegativeStep,
    58  		},
    59  		{
    60  			url:         "api/v1/query_range?start=0&end=11001&step=1",
    61  			expectedErr: errStepTooSmall,
    62  		},
    63  	} {
    64  		t.Run(tc.url, func(t *testing.T) {
    65  			r, err := http.NewRequest("GET", tc.url, nil)
    66  			require.NoError(t, err)
    67  			r.Header.Add("Test-Header", "test")
    68  
    69  			ctx := user.InjectOrgID(context.Background(), "1")
    70  
    71  			// Get a deep copy of the request with Context changed to ctx
    72  			r = r.Clone(ctx)
    73  
    74  			req, err := PrometheusCodec.DecodeRequest(ctx, r, []string{"Test-Header"})
    75  			if err != nil {
    76  				require.EqualValues(t, tc.expectedErr, err)
    77  				return
    78  			}
    79  			require.EqualValues(t, tc.expected, req)
    80  
    81  			rdash, err := PrometheusCodec.EncodeRequest(context.Background(), req)
    82  			require.NoError(t, err)
    83  			require.EqualValues(t, tc.url, rdash.RequestURI)
    84  		})
    85  	}
    86  }
    87  
    88  func TestResponse(t *testing.T) {
    89  	for i, tc := range []struct {
    90  		body     string
    91  		expected *PrometheusResponse
    92  	}{
    93  		{
    94  			body:     responseBody,
    95  			expected: withHeaders(parsedResponse, respHeaders),
    96  		},
    97  		{
    98  			body:     histogramResponseBody,
    99  			expected: withHeaders(parsedHistogramResponse, respHeaders),
   100  		},
   101  	} {
   102  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   103  			response := &http.Response{
   104  				StatusCode: 200,
   105  				Header:     http.Header{"Content-Type": []string{"application/json"}},
   106  				Body:       ioutil.NopCloser(bytes.NewBuffer([]byte(tc.body))),
   107  			}
   108  			resp, err := PrometheusCodec.DecodeResponse(context.Background(), response, nil)
   109  			require.NoError(t, err)
   110  			assert.Equal(t, tc.expected, resp)
   111  
   112  			// Reset response, as the above call will have consumed the body reader.
   113  			response = &http.Response{
   114  				StatusCode:    200,
   115  				Header:        http.Header{"Content-Type": []string{"application/json"}},
   116  				Body:          ioutil.NopCloser(bytes.NewBuffer([]byte(tc.body))),
   117  				ContentLength: int64(len(tc.body)),
   118  			}
   119  			resp2, err := PrometheusCodec.EncodeResponse(context.Background(), resp)
   120  			require.NoError(t, err)
   121  			assert.Equal(t, response, resp2)
   122  		})
   123  	}
   124  }
   125  
   126  func TestResponseWithStats(t *testing.T) {
   127  	for i, tc := range []struct {
   128  		body     string
   129  		expected *PrometheusResponse
   130  	}{
   131  		{
   132  			body: `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"foo":"bar"},"values":[[1536673680,"137"],[1536673780,"137"]]}],"stats":{"samples":{"totalQueryableSamples":10,"totalQueryableSamplesPerStep":[[1536673680,5],[1536673780,5]]}},"explanation":null}}`,
   133  			expected: &PrometheusResponse{
   134  				Status: "success",
   135  				Data: PrometheusData{
   136  					ResultType: model.ValMatrix.String(),
   137  					Result: []SampleStream{
   138  						{
   139  							Labels: []cortexpb.LabelAdapter{
   140  								{Name: "foo", Value: "bar"},
   141  							},
   142  							Samples: []cortexpb.Sample{
   143  								{Value: 137, TimestampMs: 1536673680000},
   144  								{Value: 137, TimestampMs: 1536673780000},
   145  							},
   146  						},
   147  					},
   148  					Stats: &PrometheusResponseStats{
   149  						Samples: &PrometheusResponseSamplesStats{
   150  							TotalQueryableSamples: 10,
   151  							TotalQueryableSamplesPerStep: []*PrometheusResponseQueryableSamplesStatsPerStep{
   152  								{Value: 5, TimestampMs: 1536673680000},
   153  								{Value: 5, TimestampMs: 1536673780000},
   154  							},
   155  						},
   156  					},
   157  				},
   158  			},
   159  		},
   160  	} {
   161  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   162  			tc.expected.Headers = respHeaders
   163  			response := &http.Response{
   164  				StatusCode: 200,
   165  				Header:     http.Header{"Content-Type": []string{"application/json"}},
   166  				Body:       ioutil.NopCloser(bytes.NewBuffer([]byte(tc.body))),
   167  			}
   168  			resp, err := PrometheusCodec.DecodeResponse(context.Background(), response, nil)
   169  			require.NoError(t, err)
   170  			assert.Equal(t, tc.expected, resp)
   171  
   172  			// Reset response, as the above call will have consumed the body reader.
   173  			response = &http.Response{
   174  				StatusCode:    200,
   175  				Header:        http.Header{"Content-Type": []string{"application/json"}},
   176  				Body:          ioutil.NopCloser(bytes.NewBuffer([]byte(tc.body))),
   177  				ContentLength: int64(len(tc.body)),
   178  			}
   179  			resp2, err := PrometheusCodec.EncodeResponse(context.Background(), resp)
   180  			require.NoError(t, err)
   181  			assert.Equal(t, response, resp2)
   182  		})
   183  	}
   184  }
   185  
   186  func TestMergeAPIResponses(t *testing.T) {
   187  	for _, tc := range []struct {
   188  		name     string
   189  		input    []Response
   190  		expected Response
   191  	}{
   192  		{
   193  			name:  "No responses shouldn't panic and return a non-null result and result type.",
   194  			input: []Response{},
   195  			expected: &PrometheusResponse{
   196  				Status: StatusSuccess,
   197  				Data: PrometheusData{
   198  					ResultType: matrix,
   199  					Result:     []SampleStream{},
   200  				},
   201  			},
   202  		},
   203  
   204  		{
   205  			name: "A single empty response shouldn't panic.",
   206  			input: []Response{
   207  				&PrometheusResponse{
   208  					Data: PrometheusData{
   209  						ResultType: matrix,
   210  						Result:     []SampleStream{},
   211  					},
   212  				},
   213  			},
   214  			expected: &PrometheusResponse{
   215  				Status: StatusSuccess,
   216  				Data: PrometheusData{
   217  					ResultType: matrix,
   218  					Result:     []SampleStream{},
   219  				},
   220  			},
   221  		},
   222  
   223  		{
   224  			name: "Multiple empty responses shouldn't panic.",
   225  			input: []Response{
   226  				&PrometheusResponse{
   227  					Data: PrometheusData{
   228  						ResultType: matrix,
   229  						Result:     []SampleStream{},
   230  					},
   231  				},
   232  				&PrometheusResponse{
   233  					Data: PrometheusData{
   234  						ResultType: matrix,
   235  						Result:     []SampleStream{},
   236  					},
   237  				},
   238  			},
   239  			expected: &PrometheusResponse{
   240  				Status: StatusSuccess,
   241  				Data: PrometheusData{
   242  					ResultType: matrix,
   243  					Result:     []SampleStream{},
   244  				},
   245  			},
   246  		},
   247  
   248  		{
   249  			name: "Basic merging of two responses.",
   250  			input: []Response{
   251  				&PrometheusResponse{
   252  					Data: PrometheusData{
   253  						ResultType: matrix,
   254  						Result: []SampleStream{
   255  							{
   256  								Labels: []cortexpb.LabelAdapter{},
   257  								Samples: []cortexpb.Sample{
   258  									{Value: 0, TimestampMs: 0},
   259  									{Value: 1, TimestampMs: 1},
   260  								},
   261  							},
   262  						},
   263  					},
   264  				},
   265  				&PrometheusResponse{
   266  					Data: PrometheusData{
   267  						ResultType: matrix,
   268  						Result: []SampleStream{
   269  							{
   270  								Labels: []cortexpb.LabelAdapter{},
   271  								Samples: []cortexpb.Sample{
   272  									{Value: 2, TimestampMs: 2},
   273  									{Value: 3, TimestampMs: 3},
   274  								},
   275  							},
   276  						},
   277  					},
   278  				},
   279  			},
   280  			expected: &PrometheusResponse{
   281  				Status: StatusSuccess,
   282  				Data: PrometheusData{
   283  					ResultType: matrix,
   284  					Result: []SampleStream{
   285  						{
   286  							Labels: []cortexpb.LabelAdapter{},
   287  							Samples: []cortexpb.Sample{
   288  								{Value: 0, TimestampMs: 0},
   289  								{Value: 1, TimestampMs: 1},
   290  								{Value: 2, TimestampMs: 2},
   291  								{Value: 3, TimestampMs: 3},
   292  							},
   293  						},
   294  					},
   295  				},
   296  			},
   297  		},
   298  
   299  		{
   300  			name: "Merging of responses when labels are in different order.",
   301  			input: []Response{
   302  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[0,"0"],[1,"1"]]}]}}`),
   303  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"c":"d","a":"b"},"values":[[2,"2"],[3,"3"]]}]}}`),
   304  			},
   305  			expected: &PrometheusResponse{
   306  				Status: StatusSuccess,
   307  				Data: PrometheusData{
   308  					ResultType: matrix,
   309  					Result: []SampleStream{
   310  						{
   311  							Labels: []cortexpb.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}},
   312  							Samples: []cortexpb.Sample{
   313  								{Value: 0, TimestampMs: 0},
   314  								{Value: 1, TimestampMs: 1000},
   315  								{Value: 2, TimestampMs: 2000},
   316  								{Value: 3, TimestampMs: 3000},
   317  							},
   318  						},
   319  					},
   320  				},
   321  			},
   322  		},
   323  
   324  		{
   325  			name: "Merging of samples where there is single overlap.",
   326  			input: []Response{
   327  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[1,"1"],[2,"2"]]}]}}`),
   328  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"c":"d","a":"b"},"values":[[2,"2"],[3,"3"]]}]}}`),
   329  			},
   330  			expected: &PrometheusResponse{
   331  				Status: StatusSuccess,
   332  				Data: PrometheusData{
   333  					ResultType: matrix,
   334  					Result: []SampleStream{
   335  						{
   336  							Labels: []cortexpb.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}},
   337  							Samples: []cortexpb.Sample{
   338  								{Value: 1, TimestampMs: 1000},
   339  								{Value: 2, TimestampMs: 2000},
   340  								{Value: 3, TimestampMs: 3000},
   341  							},
   342  						},
   343  					},
   344  				},
   345  			},
   346  		},
   347  		{
   348  			name: "Merging of samples where there is multiple partial overlaps.",
   349  			input: []Response{
   350  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[1,"1"],[2,"2"],[3,"3"]]}]}}`),
   351  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"c":"d","a":"b"},"values":[[2,"2"],[3,"3"],[4,"4"],[5,"5"]]}]}}`),
   352  			},
   353  			expected: &PrometheusResponse{
   354  				Status: StatusSuccess,
   355  				Data: PrometheusData{
   356  					ResultType: matrix,
   357  					Result: []SampleStream{
   358  						{
   359  							Labels: []cortexpb.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}},
   360  							Samples: []cortexpb.Sample{
   361  								{Value: 1, TimestampMs: 1000},
   362  								{Value: 2, TimestampMs: 2000},
   363  								{Value: 3, TimestampMs: 3000},
   364  								{Value: 4, TimestampMs: 4000},
   365  								{Value: 5, TimestampMs: 5000},
   366  							},
   367  						},
   368  					},
   369  				},
   370  			},
   371  		},
   372  		{
   373  			name: "Merging of samples where there is complete overlap.",
   374  			input: []Response{
   375  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[2,"2"],[3,"3"]]}]}}`),
   376  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"c":"d","a":"b"},"values":[[2,"2"],[3,"3"],[4,"4"],[5,"5"]]}]}}`),
   377  			},
   378  			expected: &PrometheusResponse{
   379  				Status: StatusSuccess,
   380  				Data: PrometheusData{
   381  					ResultType: matrix,
   382  					Result: []SampleStream{
   383  						{
   384  							Labels: []cortexpb.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}},
   385  							Samples: []cortexpb.Sample{
   386  								{Value: 2, TimestampMs: 2000},
   387  								{Value: 3, TimestampMs: 3000},
   388  								{Value: 4, TimestampMs: 4000},
   389  								{Value: 5, TimestampMs: 5000},
   390  							},
   391  						},
   392  					},
   393  				},
   394  			},
   395  		},
   396  		{
   397  			name: "[stats] A single empty response shouldn't panic.",
   398  			input: []Response{
   399  				&PrometheusResponse{
   400  					Data: PrometheusData{
   401  						ResultType: matrix,
   402  						Result:     []SampleStream{},
   403  						Stats:      &PrometheusResponseStats{Samples: &PrometheusResponseSamplesStats{}},
   404  					},
   405  				},
   406  			},
   407  			expected: &PrometheusResponse{
   408  				Status: StatusSuccess,
   409  				Data: PrometheusData{
   410  					ResultType: matrix,
   411  					Result:     []SampleStream{},
   412  					Stats:      &PrometheusResponseStats{Samples: &PrometheusResponseSamplesStats{}},
   413  				},
   414  			},
   415  		},
   416  
   417  		{
   418  			name: "[stats] Multiple empty responses shouldn't panic.",
   419  			input: []Response{
   420  				&PrometheusResponse{
   421  					Data: PrometheusData{
   422  						ResultType: matrix,
   423  						Result:     []SampleStream{},
   424  						Stats:      &PrometheusResponseStats{Samples: &PrometheusResponseSamplesStats{}},
   425  					},
   426  				},
   427  				&PrometheusResponse{
   428  					Data: PrometheusData{
   429  						ResultType: matrix,
   430  						Result:     []SampleStream{},
   431  						Stats:      &PrometheusResponseStats{Samples: &PrometheusResponseSamplesStats{}},
   432  					},
   433  				},
   434  			},
   435  			expected: &PrometheusResponse{
   436  				Status: StatusSuccess,
   437  				Data: PrometheusData{
   438  					ResultType: matrix,
   439  					Result:     []SampleStream{},
   440  					Stats:      &PrometheusResponseStats{Samples: &PrometheusResponseSamplesStats{}},
   441  				},
   442  			},
   443  		},
   444  
   445  		{
   446  			name: "[stats] Basic merging of two responses.",
   447  			input: []Response{
   448  				&PrometheusResponse{
   449  					Data: PrometheusData{
   450  						ResultType: matrix,
   451  						Result: []SampleStream{
   452  							{
   453  								Labels: []cortexpb.LabelAdapter{},
   454  								Samples: []cortexpb.Sample{
   455  									{Value: 0, TimestampMs: 0},
   456  									{Value: 1, TimestampMs: 1},
   457  								},
   458  							},
   459  						},
   460  						Stats: &PrometheusResponseStats{Samples: &PrometheusResponseSamplesStats{
   461  							TotalQueryableSamples: 20,
   462  							TotalQueryableSamplesPerStep: []*PrometheusResponseQueryableSamplesStatsPerStep{
   463  								{Value: 5, TimestampMs: 0},
   464  								{Value: 15, TimestampMs: 1},
   465  							},
   466  						}},
   467  					},
   468  				},
   469  				&PrometheusResponse{
   470  					Data: PrometheusData{
   471  						ResultType: matrix,
   472  						Result: []SampleStream{
   473  							{
   474  								Labels: []cortexpb.LabelAdapter{},
   475  								Samples: []cortexpb.Sample{
   476  									{Value: 2, TimestampMs: 2},
   477  									{Value: 3, TimestampMs: 3},
   478  								},
   479  							},
   480  						},
   481  						Stats: &PrometheusResponseStats{Samples: &PrometheusResponseSamplesStats{
   482  							TotalQueryableSamples: 10,
   483  							TotalQueryableSamplesPerStep: []*PrometheusResponseQueryableSamplesStatsPerStep{
   484  								{Value: 5, TimestampMs: 2},
   485  								{Value: 5, TimestampMs: 3},
   486  							},
   487  						}},
   488  					},
   489  				},
   490  			},
   491  			expected: &PrometheusResponse{
   492  				Status: StatusSuccess,
   493  				Data: PrometheusData{
   494  					ResultType: matrix,
   495  					Result: []SampleStream{
   496  						{
   497  							Labels: []cortexpb.LabelAdapter{},
   498  							Samples: []cortexpb.Sample{
   499  								{Value: 0, TimestampMs: 0},
   500  								{Value: 1, TimestampMs: 1},
   501  								{Value: 2, TimestampMs: 2},
   502  								{Value: 3, TimestampMs: 3},
   503  							},
   504  						},
   505  					},
   506  					Stats: &PrometheusResponseStats{Samples: &PrometheusResponseSamplesStats{
   507  						TotalQueryableSamples: 30,
   508  						TotalQueryableSamplesPerStep: []*PrometheusResponseQueryableSamplesStatsPerStep{
   509  							{Value: 5, TimestampMs: 0},
   510  							{Value: 15, TimestampMs: 1},
   511  							{Value: 5, TimestampMs: 2},
   512  							{Value: 5, TimestampMs: 3},
   513  						},
   514  					}},
   515  				},
   516  			},
   517  		},
   518  		{
   519  			name: "[stats] Merging of samples where there is single overlap.",
   520  			input: []Response{
   521  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[1,"1"],[2,"2"]]}],"stats":{"samples":{"totalQueryableSamples":10,"totalQueryableSamplesPerStep":[[1,5],[2,5]]}}}}`),
   522  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[2,"2"],[3,"3"]]}],"stats":{"samples":{"totalQueryableSamples":20,"totalQueryableSamplesPerStep":[[2,5],[3,15]]}}}}`),
   523  			},
   524  			expected: &PrometheusResponse{
   525  				Status: StatusSuccess,
   526  				Data: PrometheusData{
   527  					ResultType: matrix,
   528  					Result: []SampleStream{
   529  						{
   530  							Labels: []cortexpb.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}},
   531  							Samples: []cortexpb.Sample{
   532  								{Value: 1, TimestampMs: 1000},
   533  								{Value: 2, TimestampMs: 2000},
   534  								{Value: 3, TimestampMs: 3000},
   535  							},
   536  						},
   537  					},
   538  					Stats: &PrometheusResponseStats{Samples: &PrometheusResponseSamplesStats{
   539  						TotalQueryableSamples: 25,
   540  						TotalQueryableSamplesPerStep: []*PrometheusResponseQueryableSamplesStatsPerStep{
   541  							{Value: 5, TimestampMs: 1000},
   542  							{Value: 5, TimestampMs: 2000},
   543  							{Value: 15, TimestampMs: 3000},
   544  						},
   545  					}},
   546  				},
   547  			},
   548  		},
   549  		{
   550  			name: "[stats] Merging of multiple responses with some overlap.",
   551  			input: []Response{
   552  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[3,"3"],[4,"4"],[5,"5"]]}],"stats":{"samples":{"totalQueryableSamples":12,"totalQueryableSamplesPerStep":[[3,3],[4,4],[5,5]]}}}}`),
   553  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[1,"1"],[2,"2"],[3,"3"],[4,"4"]]}],"stats":{"samples":{"totalQueryableSamples":6,"totalQueryableSamplesPerStep":[[1,1],[2,2],[3,3],[4,4]]}}}}`),
   554  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[5,"5"],[6,"6"],[7,"7"]]}],"stats":{"samples":{"totalQueryableSamples":18,"totalQueryableSamplesPerStep":[[5,5],[6,6],[7,7]]}}}}`),
   555  			},
   556  			expected: &PrometheusResponse{
   557  				Status: StatusSuccess,
   558  				Data: PrometheusData{
   559  					ResultType: matrix,
   560  					Result: []SampleStream{
   561  						{
   562  							Labels: []cortexpb.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}},
   563  							Samples: []cortexpb.Sample{
   564  								{Value: 1, TimestampMs: 1000},
   565  								{Value: 2, TimestampMs: 2000},
   566  								{Value: 3, TimestampMs: 3000},
   567  								{Value: 4, TimestampMs: 4000},
   568  								{Value: 5, TimestampMs: 5000},
   569  								{Value: 6, TimestampMs: 6000},
   570  								{Value: 7, TimestampMs: 7000},
   571  							},
   572  						},
   573  					},
   574  					Stats: &PrometheusResponseStats{Samples: &PrometheusResponseSamplesStats{
   575  						TotalQueryableSamples: 28,
   576  						TotalQueryableSamplesPerStep: []*PrometheusResponseQueryableSamplesStatsPerStep{
   577  							{Value: 1, TimestampMs: 1000},
   578  							{Value: 2, TimestampMs: 2000},
   579  							{Value: 3, TimestampMs: 3000},
   580  							{Value: 4, TimestampMs: 4000},
   581  							{Value: 5, TimestampMs: 5000},
   582  							{Value: 6, TimestampMs: 6000},
   583  							{Value: 7, TimestampMs: 7000},
   584  						},
   585  					}},
   586  				},
   587  			},
   588  		},
   589  		{
   590  			name: "[stats] Merging of samples where there is multiple partial overlaps.",
   591  			input: []Response{
   592  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[1,"1"],[2,"2"],[3,"3"]]}],"stats":{"samples":{"totalQueryableSamples":6,"totalQueryableSamplesPerStep":[[1,1],[2,2],[3,3]]}}}}`),
   593  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"c":"d","a":"b"},"values":[[2,"2"],[3,"3"],[4,"4"],[5,"5"]]}],"stats":{"samples":{"totalQueryableSamples":20,"totalQueryableSamplesPerStep":[[2,2],[3,3],[4,4],[5,5]]}}}}`),
   594  			},
   595  			expected: &PrometheusResponse{
   596  				Status: StatusSuccess,
   597  				Data: PrometheusData{
   598  					ResultType: matrix,
   599  					Result: []SampleStream{
   600  						{
   601  							Labels: []cortexpb.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}},
   602  							Samples: []cortexpb.Sample{
   603  								{Value: 1, TimestampMs: 1000},
   604  								{Value: 2, TimestampMs: 2000},
   605  								{Value: 3, TimestampMs: 3000},
   606  								{Value: 4, TimestampMs: 4000},
   607  								{Value: 5, TimestampMs: 5000},
   608  							},
   609  						},
   610  					},
   611  					Stats: &PrometheusResponseStats{Samples: &PrometheusResponseSamplesStats{
   612  						TotalQueryableSamples: 15,
   613  						TotalQueryableSamplesPerStep: []*PrometheusResponseQueryableSamplesStatsPerStep{
   614  							{Value: 1, TimestampMs: 1000},
   615  							{Value: 2, TimestampMs: 2000},
   616  							{Value: 3, TimestampMs: 3000},
   617  							{Value: 4, TimestampMs: 4000},
   618  							{Value: 5, TimestampMs: 5000},
   619  						},
   620  					}},
   621  				},
   622  			},
   623  		},
   624  		{
   625  			name: "[stats] Merging of samples where there is complete overlap.",
   626  			input: []Response{
   627  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[2,"2"],[3,"3"]]}],"stats":{"samples":{"totalQueryableSamples":20,"totalQueryableSamplesPerStep":[[2,2],[3,3]]}}}}`),
   628  				mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"c":"d","a":"b"},"values":[[2,"2"],[3,"3"],[4,"4"],[5,"5"]]}],"stats":{"samples":{"totalQueryableSamples":20,"totalQueryableSamplesPerStep":[[2,2],[3,3],[4,4],[5,5]]}}}}`),
   629  			},
   630  			expected: &PrometheusResponse{
   631  				Status: StatusSuccess,
   632  				Data: PrometheusData{
   633  					ResultType: matrix,
   634  					Result: []SampleStream{
   635  						{
   636  							Labels: []cortexpb.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}},
   637  							Samples: []cortexpb.Sample{
   638  								{Value: 2, TimestampMs: 2000},
   639  								{Value: 3, TimestampMs: 3000},
   640  								{Value: 4, TimestampMs: 4000},
   641  								{Value: 5, TimestampMs: 5000},
   642  							},
   643  						},
   644  					},
   645  					Stats: &PrometheusResponseStats{Samples: &PrometheusResponseSamplesStats{
   646  						TotalQueryableSamples: 14,
   647  						TotalQueryableSamplesPerStep: []*PrometheusResponseQueryableSamplesStatsPerStep{
   648  							{Value: 2, TimestampMs: 2000},
   649  							{Value: 3, TimestampMs: 3000},
   650  							{Value: 4, TimestampMs: 4000},
   651  							{Value: 5, TimestampMs: 5000},
   652  						},
   653  					}},
   654  				},
   655  			},
   656  		}} {
   657  		t.Run(tc.name, func(t *testing.T) {
   658  			output, err := PrometheusCodec.MergeResponse(nil, tc.input...)
   659  			require.NoError(t, err)
   660  			require.Equal(t, tc.expected, output)
   661  		})
   662  	}
   663  }
   664  
   665  func mustParse(t *testing.T, response string) Response {
   666  	var resp PrometheusResponse
   667  	// Needed as goimports automatically add a json import otherwise.
   668  	json := jsoniter.ConfigCompatibleWithStandardLibrary
   669  	require.NoError(t, json.Unmarshal([]byte(response), &resp))
   670  	return &resp
   671  }
   672  
   673  func withHeaders(response *PrometheusResponse, headers []*PrometheusResponseHeader) *PrometheusResponse {
   674  	r := *response
   675  	r.Headers = headers
   676  	return &r
   677  }