github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/prom/read_test.go (about) 1 // Copyright (c) 2020 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package prom 22 23 import ( 24 "context" 25 "encoding/json" 26 "fmt" 27 "net/http" 28 "net/http/httptest" 29 "net/url" 30 "testing" 31 "time" 32 33 "github.com/prometheus/prometheus/pkg/labels" 34 "github.com/prometheus/prometheus/promql" 35 promstorage "github.com/prometheus/prometheus/storage" 36 "github.com/stretchr/testify/require" 37 "go.uber.org/zap" 38 39 "github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions" 40 "github.com/m3db/m3/src/query/api/v1/handler/prometheus/native" 41 "github.com/m3db/m3/src/query/api/v1/options" 42 "github.com/m3db/m3/src/query/executor" 43 "github.com/m3db/m3/src/query/storage" 44 "github.com/m3db/m3/src/query/storage/prometheus" 45 xerrors "github.com/m3db/m3/src/x/errors" 46 "github.com/m3db/m3/src/x/instrument" 47 ) 48 49 const promQuery = `http_requests_total{job="prometheus",group="canary"}` 50 51 const ( 52 queryParam = "query" 53 startParam = "start" 54 endParam = "end" 55 ) 56 57 var testPromQLEngineFn = func(_ time.Duration) (*promql.Engine, error) { 58 return newMockPromQLEngine(), nil 59 } 60 61 type testHandlers struct { 62 queryable *mockQueryable 63 readHandler http.Handler 64 readInstantHandler http.Handler 65 } 66 67 func setupTest(t *testing.T) testHandlers { 68 fetchOptsBuilderCfg := handleroptions.FetchOptionsBuilderOptions{ 69 Timeout: 15 * time.Second, 70 } 71 fetchOptsBuilder, err := handleroptions.NewFetchOptionsBuilder(fetchOptsBuilderCfg) 72 require.NoError(t, err) 73 instrumentOpts := instrument.NewOptions() 74 engineOpts := executor.NewEngineOptions(). 75 SetLookbackDuration(time.Minute). 76 SetInstrumentOptions(instrumentOpts) 77 engine := executor.NewEngine(engineOpts) 78 hOpts := options.EmptyHandlerOptions(). 79 SetFetchOptionsBuilder(fetchOptsBuilder). 80 SetEngine(engine) 81 82 queryable := &mockQueryable{} 83 readHandler, err := newReadHandler(hOpts, opts{ 84 queryable: queryable, 85 instant: false, 86 newQueryFn: newRangeQueryFn(testPromQLEngineFn, queryable), 87 }) 88 require.NoError(t, err) 89 readInstantHandler, err := newReadHandler(hOpts, opts{ 90 queryable: queryable, 91 instant: true, 92 newQueryFn: newInstantQueryFn(testPromQLEngineFn, queryable), 93 }) 94 require.NoError(t, err) 95 return testHandlers{ 96 queryable: queryable, 97 readHandler: readHandler, 98 readInstantHandler: readInstantHandler, 99 } 100 } 101 102 func defaultParams() url.Values { 103 vals := url.Values{} 104 now := time.Now() 105 vals.Add(queryParam, promQuery) 106 vals.Add(startParam, now.Format(time.RFC3339)) 107 vals.Add(endParam, now.Add(time.Hour).Format(time.RFC3339)) 108 vals.Add(handleroptions.StepParam, (time.Duration(10) * time.Second).String()) 109 return vals 110 } 111 112 func defaultParamsWithoutQuery() url.Values { 113 vals := url.Values{} 114 now := time.Now() 115 vals.Add(startParam, now.Format(time.RFC3339)) 116 vals.Add(endParam, now.Add(time.Hour).Format(time.RFC3339)) 117 vals.Add(handleroptions.StepParam, (time.Duration(10) * time.Second).String()) 118 return vals 119 } 120 121 func TestPromReadHandler(t *testing.T) { 122 setup := setupTest(t) 123 124 req, _ := http.NewRequest("GET", native.PromReadURL, nil) 125 req.URL.RawQuery = defaultParams().Encode() 126 127 recorder := httptest.NewRecorder() 128 setup.readHandler.ServeHTTP(recorder, req) 129 130 var resp response 131 require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp)) 132 require.Equal(t, statusSuccess, resp.Status) 133 } 134 135 func TestPromReadHandlerInvalidQuery(t *testing.T) { 136 setup := setupTest(t) 137 138 req, _ := http.NewRequest("GET", native.PromReadURL, nil) 139 req.URL.RawQuery = defaultParamsWithoutQuery().Encode() 140 141 recorder := httptest.NewRecorder() 142 setup.readHandler.ServeHTTP(recorder, req) 143 144 var resp response 145 require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp)) 146 require.Equal(t, statusError, resp.Status) 147 require.Equal(t, http.StatusBadRequest, recorder.Code) 148 } 149 150 func TestPromReadHandlerErrors(t *testing.T) { 151 testCases := []struct { 152 name string 153 err error 154 httpCode int 155 }{ 156 { 157 name: "prom error", 158 err: fmt.Errorf("prom error"), 159 httpCode: http.StatusBadRequest, 160 }, 161 { 162 name: "prom timeout", 163 err: promql.ErrQueryTimeout("timeout"), 164 httpCode: http.StatusGatewayTimeout, 165 }, 166 { 167 name: "prom cancel", 168 err: promql.ErrQueryCanceled("cancel"), 169 httpCode: 499, 170 }, 171 { 172 name: "storage 500", 173 err: prometheus.NewStorageErr(fmt.Errorf("500 storage error")), 174 httpCode: http.StatusInternalServerError, 175 }, 176 { 177 name: "storage 400", 178 err: prometheus.NewStorageErr(xerrors.NewInvalidParamsError(fmt.Errorf("400 storage error"))), 179 httpCode: http.StatusBadRequest, 180 }, 181 } 182 183 for _, tc := range testCases { 184 tc := tc 185 t.Run(tc.name, func(t *testing.T) { 186 setup := setupTest(t) 187 setup.queryable.selectFn = func( 188 sortSeries bool, 189 hints *promstorage.SelectHints, 190 labelMatchers ...*labels.Matcher, 191 ) promstorage.SeriesSet { 192 return promstorage.ErrSeriesSet(tc.err) 193 } 194 195 req, _ := http.NewRequestWithContext(context.Background(), "GET", native.PromReadURL, nil) 196 req.URL.RawQuery = defaultParams().Encode() 197 198 recorder := httptest.NewRecorder() 199 setup.readHandler.ServeHTTP(recorder, req) 200 201 var resp response 202 require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp)) 203 require.Equal(t, statusError, resp.Status) 204 require.Equal(t, tc.httpCode, recorder.Code) 205 }) 206 } 207 } 208 209 func TestPromReadInstantHandler(t *testing.T) { 210 setup := setupTest(t) 211 212 req, _ := http.NewRequest("GET", native.PromReadInstantURL, nil) 213 req.URL.RawQuery = defaultParams().Encode() 214 215 recorder := httptest.NewRecorder() 216 setup.readInstantHandler.ServeHTTP(recorder, req) 217 218 var resp response 219 require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp)) 220 require.Equal(t, statusSuccess, resp.Status) 221 } 222 223 func TestPromReadInstantHandlerInvalidQuery(t *testing.T) { 224 setup := setupTest(t) 225 226 req, _ := http.NewRequest("GET", native.PromReadInstantURL, nil) 227 req.URL.RawQuery = defaultParamsWithoutQuery().Encode() 228 229 recorder := httptest.NewRecorder() 230 setup.readInstantHandler.ServeHTTP(recorder, req) 231 232 var resp response 233 require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp)) 234 require.Equal(t, statusError, resp.Status) 235 } 236 237 func TestPromReadInstantHandlerParseMinTime(t *testing.T) { 238 setup := setupTest(t) 239 240 var ( 241 query *promstorage.SelectHints 242 selects int 243 ) 244 setup.queryable.selectFn = func( 245 sortSeries bool, 246 hints *promstorage.SelectHints, 247 labelMatchers ...*labels.Matcher, 248 ) promstorage.SeriesSet { 249 selects++ 250 query = hints 251 return &mockSeriesSet{} 252 } 253 254 req, _ := http.NewRequest("GET", native.PromReadInstantURL, nil) 255 params := defaultParams() 256 params.Set("time", minTimeFormatted) 257 req.URL.RawQuery = params.Encode() 258 259 var resp response 260 recorder := httptest.NewRecorder() 261 262 setup.readInstantHandler.ServeHTTP(recorder, req) 263 264 require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp)) 265 require.Equal(t, statusSuccess, resp.Status) 266 267 require.Equal(t, 1, selects) 268 269 fudge := 5 * time.Minute // Need to account for lookback 270 expected := time.Unix(0, 0) 271 actual := millisTime(query.Start) 272 require.True(t, abs(expected.Sub(actual)) <= fudge, 273 fmt.Sprintf("expected=%v, actual=%v, fudge=%v, delta=%v", 274 expected, actual, fudge, expected.Sub(actual))) 275 276 fudge = 5 * time.Minute // Need to account for lookback 277 expected = time.Unix(0, 0) 278 actual = millisTime(query.Start) 279 require.True(t, abs(expected.Sub(actual)) <= fudge, 280 fmt.Sprintf("expected=%v, actual=%v, fudge=%v, delta=%v", 281 expected, actual, fudge, expected.Sub(actual))) 282 } 283 284 func TestPromReadInstantHandlerParseMaxTime(t *testing.T) { 285 setup := setupTest(t) 286 287 var ( 288 query *promstorage.SelectHints 289 selects int 290 ) 291 setup.queryable.selectFn = func( 292 sortSeries bool, 293 hints *promstorage.SelectHints, 294 labelMatchers ...*labels.Matcher, 295 ) promstorage.SeriesSet { 296 selects++ 297 query = hints 298 return &mockSeriesSet{} 299 } 300 301 req, _ := http.NewRequest("GET", native.PromReadInstantURL, nil) 302 params := defaultParams() 303 params.Set("time", maxTimeFormatted) 304 req.URL.RawQuery = params.Encode() 305 306 var resp response 307 recorder := httptest.NewRecorder() 308 309 setup.readInstantHandler.ServeHTTP(recorder, req) 310 311 require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp)) 312 require.Equal(t, statusSuccess, resp.Status) 313 314 require.Equal(t, 1, selects) 315 316 fudge := 6 * time.Minute // Need to account for lookback + time.Now() skew 317 expected := time.Now() 318 actual := millisTime(query.Start) 319 require.True(t, abs(expected.Sub(actual)) <= fudge, 320 fmt.Sprintf("expected=%v, actual=%v, fudge=%v, delta=%v", 321 expected, actual, fudge, expected.Sub(actual))) 322 323 fudge = 6 * time.Minute // Need to account for lookback + time.Now() skew 324 expected = time.Now() 325 actual = millisTime(query.Start) 326 require.True(t, abs(expected.Sub(actual)) <= fudge, 327 fmt.Sprintf("expected=%v, actual=%v, fudge=%v, delta=%v", 328 expected, actual, fudge, expected.Sub(actual))) 329 } 330 331 func TestLimitedReturnedDataVector(t *testing.T) { 332 handler := &readHandler{ 333 logger: zap.NewNop(), 334 } 335 336 r := &promql.Result{ 337 Value: promql.Vector{ 338 {Point: promql.Point{T: 1, V: 1.0}}, 339 {Point: promql.Point{T: 2, V: 2.0}}, 340 {Point: promql.Point{T: 3, V: 3.0}}, 341 }, 342 } 343 344 tests := []struct { 345 name string 346 maxSeries int 347 maxDatapoints int 348 expectedSeries int 349 expectedTotalSeries int 350 expectedDatapoints int 351 expectedLimited bool 352 }{ 353 { 354 name: "Omit limits", 355 expectedLimited: false, 356 }, 357 { 358 name: "Series below max", 359 maxSeries: 4, 360 expectedLimited: false, 361 }, 362 { 363 name: "Series at max", 364 maxSeries: 3, 365 expectedLimited: false, 366 }, 367 { 368 name: "Series above max", 369 maxSeries: 2, 370 expectedLimited: true, 371 expectedSeries: 2, 372 expectedTotalSeries: 3, 373 expectedDatapoints: 2, 374 }, 375 { 376 name: "Datapoints below max", 377 maxDatapoints: 4, 378 expectedLimited: false, 379 }, 380 { 381 name: "Datapoints at max", 382 maxDatapoints: 3, 383 expectedLimited: false, 384 }, 385 { 386 name: "Datapoints above max", 387 maxDatapoints: 2, 388 expectedLimited: true, 389 expectedSeries: 2, 390 expectedTotalSeries: 3, 391 expectedDatapoints: 2, 392 }, 393 { 394 name: "Series and datapoints limit (former lower)", 395 maxSeries: 1, 396 maxDatapoints: 2, 397 expectedLimited: true, 398 expectedSeries: 1, 399 expectedTotalSeries: 3, 400 expectedDatapoints: 1, 401 }, 402 { 403 name: "Series and datapoints limit (former higher)", 404 maxSeries: 2, 405 maxDatapoints: 1, 406 expectedLimited: true, 407 expectedSeries: 1, 408 expectedTotalSeries: 3, 409 expectedDatapoints: 1, 410 }, 411 { 412 name: "Series and datapoints limit (only one under limit)", 413 maxSeries: 1, 414 maxDatapoints: 10, 415 expectedLimited: true, 416 expectedSeries: 1, 417 expectedTotalSeries: 3, 418 expectedDatapoints: 1, 419 }, 420 } 421 422 for _, test := range tests { 423 t.Run(test.name, func(t *testing.T) { 424 result := *r 425 limited := handler.limitReturnedData("", &result, &storage.FetchOptions{ 426 ReturnedSeriesLimit: test.maxSeries, 427 ReturnedDatapointsLimit: test.maxDatapoints, 428 }) 429 require.Equal(t, test.expectedLimited, limited.Limited) 430 431 seriesCount := len(result.Value.(promql.Vector)) 432 433 if limited.Limited { 434 require.Equal(t, test.expectedSeries, seriesCount) 435 require.Equal(t, test.expectedSeries, limited.Series) 436 require.Equal(t, test.expectedTotalSeries, limited.TotalSeries) 437 require.Equal(t, test.expectedDatapoints, limited.Datapoints) 438 } else { 439 // Full results 440 require.Equal(t, 3, seriesCount) 441 } 442 }) 443 } 444 } 445 446 func TestLimitedReturnedDataMatrix(t *testing.T) { 447 handler := &readHandler{ 448 logger: zap.NewNop(), 449 } 450 451 r := &promql.Result{ 452 Value: promql.Matrix{ 453 {Points: []promql.Point{ 454 {T: 1, V: 1.0}, 455 }}, 456 {Points: []promql.Point{ 457 {T: 1, V: 1.0}, 458 {T: 2, V: 2.0}, 459 }}, 460 {Points: []promql.Point{ 461 {T: 1, V: 1.0}, 462 {T: 2, V: 2.0}, 463 {T: 3, V: 3.0}, 464 }}, 465 }, 466 } 467 468 tests := []struct { 469 name string 470 maxSeries int 471 maxDatapoints int 472 expectedSeries int 473 expectedTotalSeries int 474 expectedDatapoints int 475 expectedLimited bool 476 }{ 477 { 478 name: "Omit limits", 479 expectedLimited: false, 480 }, 481 { 482 name: "Series below max", 483 maxSeries: 4, 484 expectedLimited: false, 485 }, 486 { 487 name: "Series at max", 488 maxSeries: 3, 489 expectedLimited: false, 490 }, 491 { 492 name: "Series above max", 493 maxSeries: 2, 494 expectedLimited: true, 495 expectedSeries: 2, 496 expectedTotalSeries: 3, 497 expectedDatapoints: 3, 498 }, 499 { 500 name: "Datapoints below max", 501 maxDatapoints: 7, 502 expectedLimited: false, 503 }, 504 { 505 name: "Datapoints at max", 506 maxDatapoints: 6, 507 expectedLimited: false, 508 }, 509 { 510 name: "Datapoints above max - 2 series left - A", 511 maxDatapoints: 5, 512 expectedLimited: true, 513 expectedSeries: 2, 514 expectedTotalSeries: 3, 515 expectedDatapoints: 3, 516 }, 517 { 518 name: "Datapoints above max - 2 series left - B", 519 maxDatapoints: 3, 520 expectedLimited: true, 521 expectedSeries: 2, 522 expectedTotalSeries: 3, 523 expectedDatapoints: 3, 524 }, 525 { 526 name: "Datapoints above max - 1 series left - A", 527 maxDatapoints: 2, 528 expectedLimited: true, 529 expectedSeries: 1, 530 expectedTotalSeries: 3, 531 expectedDatapoints: 1, 532 }, 533 { 534 name: "Datapoints above max - 1 series left - B", 535 maxDatapoints: 1, 536 expectedLimited: true, 537 expectedSeries: 1, 538 expectedTotalSeries: 3, 539 expectedDatapoints: 1, 540 }, 541 { 542 name: "Series and datapoints limit (former lower)", 543 maxSeries: 1, 544 maxDatapoints: 6, 545 expectedLimited: true, 546 expectedSeries: 1, 547 expectedTotalSeries: 3, 548 expectedDatapoints: 1, 549 }, 550 { 551 name: "Series and datapoints limit (former higher)", 552 maxSeries: 6, 553 maxDatapoints: 1, 554 expectedLimited: true, 555 expectedSeries: 1, 556 expectedTotalSeries: 3, 557 expectedDatapoints: 1, 558 }, 559 { 560 name: "Series and datapoints limit (only one under limit)", 561 maxSeries: 1, 562 maxDatapoints: 10, 563 expectedLimited: true, 564 expectedSeries: 1, 565 expectedTotalSeries: 3, 566 expectedDatapoints: 1, 567 }, 568 } 569 570 for _, test := range tests { 571 t.Run(test.name, func(t *testing.T) { 572 result := *r 573 limited := handler.limitReturnedData("", &result, &storage.FetchOptions{ 574 ReturnedSeriesLimit: test.maxSeries, 575 ReturnedDatapointsLimit: test.maxDatapoints, 576 }) 577 require.Equal(t, test.expectedLimited, limited.Limited) 578 579 m := result.Value.(promql.Matrix) 580 seriesCount := len(m) 581 datapointCount := 0 582 for _, d := range m { 583 datapointCount += len(d.Points) 584 } 585 586 if limited.Limited { 587 require.Equal(t, test.expectedSeries, seriesCount, "series count") 588 require.Equal(t, test.expectedDatapoints, datapointCount, "datapoint count") 589 require.Equal(t, test.expectedSeries, limited.Series, "series") 590 require.Equal(t, test.expectedTotalSeries, limited.TotalSeries, "total series") 591 require.Equal(t, test.expectedDatapoints, limited.Datapoints, "datapoints") 592 } else { 593 // Full results 594 require.Equal(t, 3, seriesCount, "expect full series") 595 require.Equal(t, 6, datapointCount, "expect full datapoints") 596 } 597 }) 598 } 599 } 600 601 func abs(v time.Duration) time.Duration { 602 if v < 0 { 603 return v * -1 604 } 605 return v 606 } 607 608 func millisTime(timestampMilliseconds int64) time.Time { 609 return time.Unix(0, timestampMilliseconds*int64(time.Millisecond)) 610 }