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 }