1// Copyright 2017 The Prometheus Authors 2// Licensed under the Apache License, Version 2.0 (the "License"); 3// you may not use this file except in compliance with the License. 4// You may obtain a copy of the License at 5// 6// http://www.apache.org/licenses/LICENSE-2.0 7// 8// Unless required by applicable law or agreed to in writing, software 9// distributed under the License is distributed on an "AS IS" BASIS, 10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11// See the License for the specific language governing permissions and 12// limitations under the License. 13 14// +build go1.7 15 16package v1 17 18import ( 19 "context" 20 "encoding/json" 21 "errors" 22 "fmt" 23 "net/http" 24 "net/url" 25 "reflect" 26 "strings" 27 "testing" 28 "time" 29 30 "github.com/prometheus/common/model" 31) 32 33type apiTest struct { 34 do func() (interface{}, error) 35 inErr error 36 inStatusCode int 37 inRes interface{} 38 39 reqPath string 40 reqParam url.Values 41 reqMethod string 42 res interface{} 43 err error 44} 45 46type apiTestClient struct { 47 *testing.T 48 curTest apiTest 49} 50 51func (c *apiTestClient) URL(ep string, args map[string]string) *url.URL { 52 path := ep 53 for k, v := range args { 54 path = strings.Replace(path, ":"+k, v, -1) 55 } 56 u := &url.URL{ 57 Host: "test:9090", 58 Path: path, 59 } 60 return u 61} 62 63func (c *apiTestClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { 64 65 test := c.curTest 66 67 if req.URL.Path != test.reqPath { 68 c.Errorf("unexpected request path: want %s, got %s", test.reqPath, req.URL.Path) 69 } 70 if req.Method != test.reqMethod { 71 c.Errorf("unexpected request method: want %s, got %s", test.reqMethod, req.Method) 72 } 73 74 b, err := json.Marshal(test.inRes) 75 if err != nil { 76 c.Fatal(err) 77 } 78 79 resp := &http.Response{} 80 if test.inStatusCode != 0 { 81 resp.StatusCode = test.inStatusCode 82 } else if test.inErr != nil { 83 resp.StatusCode = statusAPIError 84 } else { 85 resp.StatusCode = http.StatusOK 86 } 87 88 return resp, b, test.inErr 89} 90 91func TestAPIs(t *testing.T) { 92 93 testTime := time.Now() 94 95 client := &apiTestClient{T: t} 96 97 promAPI := &httpAPI{ 98 client: client, 99 } 100 101 doAlertManagers := func() func() (interface{}, error) { 102 return func() (interface{}, error) { 103 return promAPI.AlertManagers(context.Background()) 104 } 105 } 106 107 doCleanTombstones := func() func() (interface{}, error) { 108 return func() (interface{}, error) { 109 return nil, promAPI.CleanTombstones(context.Background()) 110 } 111 } 112 113 doConfig := func() func() (interface{}, error) { 114 return func() (interface{}, error) { 115 return promAPI.Config(context.Background()) 116 } 117 } 118 119 doDeleteSeries := func(matcher string, startTime time.Time, endTime time.Time) func() (interface{}, error) { 120 return func() (interface{}, error) { 121 return nil, promAPI.DeleteSeries(context.Background(), []string{matcher}, startTime, endTime) 122 } 123 } 124 125 doFlags := func() func() (interface{}, error) { 126 return func() (interface{}, error) { 127 return promAPI.Flags(context.Background()) 128 } 129 } 130 131 doLabelValues := func(label string) func() (interface{}, error) { 132 return func() (interface{}, error) { 133 return promAPI.LabelValues(context.Background(), label) 134 } 135 } 136 137 doQuery := func(q string, ts time.Time) func() (interface{}, error) { 138 return func() (interface{}, error) { 139 return promAPI.Query(context.Background(), q, ts) 140 } 141 } 142 143 doQueryRange := func(q string, rng Range) func() (interface{}, error) { 144 return func() (interface{}, error) { 145 return promAPI.QueryRange(context.Background(), q, rng) 146 } 147 } 148 149 doSeries := func(matcher string, startTime time.Time, endTime time.Time) func() (interface{}, error) { 150 return func() (interface{}, error) { 151 return promAPI.Series(context.Background(), []string{matcher}, startTime, endTime) 152 } 153 } 154 155 doSnapshot := func(skipHead bool) func() (interface{}, error) { 156 return func() (interface{}, error) { 157 return promAPI.Snapshot(context.Background(), skipHead) 158 } 159 } 160 161 doTargets := func() func() (interface{}, error) { 162 return func() (interface{}, error) { 163 return promAPI.Targets(context.Background()) 164 } 165 } 166 167 queryTests := []apiTest{ 168 { 169 do: doQuery("2", testTime), 170 inRes: &queryResult{ 171 Type: model.ValScalar, 172 Result: &model.Scalar{ 173 Value: 2, 174 Timestamp: model.TimeFromUnix(testTime.Unix()), 175 }, 176 }, 177 178 reqMethod: "GET", 179 reqPath: "/api/v1/query", 180 reqParam: url.Values{ 181 "query": []string{"2"}, 182 "time": []string{testTime.Format(time.RFC3339Nano)}, 183 }, 184 res: &model.Scalar{ 185 Value: 2, 186 Timestamp: model.TimeFromUnix(testTime.Unix()), 187 }, 188 }, 189 { 190 do: doQuery("2", testTime), 191 inErr: fmt.Errorf("some error"), 192 193 reqMethod: "GET", 194 reqPath: "/api/v1/query", 195 reqParam: url.Values{ 196 "query": []string{"2"}, 197 "time": []string{testTime.Format(time.RFC3339Nano)}, 198 }, 199 err: fmt.Errorf("some error"), 200 }, 201 { 202 do: doQuery("2", testTime), 203 inRes: "some body", 204 inStatusCode: 500, 205 inErr: &Error{ 206 Type: ErrServer, 207 Msg: "server error: 500", 208 Detail: "some body", 209 }, 210 211 reqMethod: "GET", 212 reqPath: "/api/v1/query", 213 reqParam: url.Values{ 214 "query": []string{"2"}, 215 "time": []string{testTime.Format(time.RFC3339Nano)}, 216 }, 217 err: errors.New("server_error: server error: 500"), 218 }, 219 { 220 do: doQuery("2", testTime), 221 inRes: "some body", 222 inStatusCode: 404, 223 inErr: &Error{ 224 Type: ErrClient, 225 Msg: "client error: 404", 226 Detail: "some body", 227 }, 228 229 reqMethod: "GET", 230 reqPath: "/api/v1/query", 231 reqParam: url.Values{ 232 "query": []string{"2"}, 233 "time": []string{testTime.Format(time.RFC3339Nano)}, 234 }, 235 err: errors.New("client_error: client error: 404"), 236 }, 237 238 { 239 do: doQueryRange("2", Range{ 240 Start: testTime.Add(-time.Minute), 241 End: testTime, 242 Step: time.Minute, 243 }), 244 inErr: fmt.Errorf("some error"), 245 246 reqMethod: "GET", 247 reqPath: "/api/v1/query_range", 248 reqParam: url.Values{ 249 "query": []string{"2"}, 250 "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, 251 "end": []string{testTime.Format(time.RFC3339Nano)}, 252 "step": []string{time.Minute.String()}, 253 }, 254 err: fmt.Errorf("some error"), 255 }, 256 257 { 258 do: doLabelValues("mylabel"), 259 inRes: []string{"val1", "val2"}, 260 reqMethod: "GET", 261 reqPath: "/api/v1/label/mylabel/values", 262 res: model.LabelValues{"val1", "val2"}, 263 }, 264 265 { 266 do: doLabelValues("mylabel"), 267 inErr: fmt.Errorf("some error"), 268 reqMethod: "GET", 269 reqPath: "/api/v1/label/mylabel/values", 270 err: fmt.Errorf("some error"), 271 }, 272 273 { 274 do: doSeries("up", testTime.Add(-time.Minute), testTime), 275 inRes: []map[string]string{ 276 { 277 "__name__": "up", 278 "job": "prometheus", 279 "instance": "localhost:9090"}, 280 }, 281 reqMethod: "GET", 282 reqPath: "/api/v1/series", 283 reqParam: url.Values{ 284 "match": []string{"up"}, 285 "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, 286 "end": []string{testTime.Format(time.RFC3339Nano)}, 287 }, 288 res: []model.LabelSet{ 289 model.LabelSet{ 290 "__name__": "up", 291 "job": "prometheus", 292 "instance": "localhost:9090", 293 }, 294 }, 295 }, 296 297 { 298 do: doSeries("up", testTime.Add(-time.Minute), testTime), 299 inErr: fmt.Errorf("some error"), 300 reqMethod: "GET", 301 reqPath: "/api/v1/series", 302 reqParam: url.Values{ 303 "match": []string{"up"}, 304 "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, 305 "end": []string{testTime.Format(time.RFC3339Nano)}, 306 }, 307 err: fmt.Errorf("some error"), 308 }, 309 310 { 311 do: doSnapshot(true), 312 inRes: map[string]string{ 313 "name": "20171210T211224Z-2be650b6d019eb54", 314 }, 315 reqMethod: "POST", 316 reqPath: "/api/v1/admin/tsdb/snapshot", 317 reqParam: url.Values{ 318 "skip_head": []string{"true"}, 319 }, 320 res: SnapshotResult{ 321 Name: "20171210T211224Z-2be650b6d019eb54", 322 }, 323 }, 324 325 { 326 do: doSnapshot(true), 327 inErr: fmt.Errorf("some error"), 328 reqMethod: "POST", 329 reqPath: "/api/v1/admin/tsdb/snapshot", 330 err: fmt.Errorf("some error"), 331 }, 332 333 { 334 do: doCleanTombstones(), 335 reqMethod: "POST", 336 reqPath: "/api/v1/admin/tsdb/clean_tombstones", 337 }, 338 339 { 340 do: doCleanTombstones(), 341 inErr: fmt.Errorf("some error"), 342 reqMethod: "POST", 343 reqPath: "/api/v1/admin/tsdb/clean_tombstones", 344 err: fmt.Errorf("some error"), 345 }, 346 347 { 348 do: doDeleteSeries("up", testTime.Add(-time.Minute), testTime), 349 inRes: []map[string]string{ 350 { 351 "__name__": "up", 352 "job": "prometheus", 353 "instance": "localhost:9090"}, 354 }, 355 reqMethod: "POST", 356 reqPath: "/api/v1/admin/tsdb/delete_series", 357 reqParam: url.Values{ 358 "match": []string{"up"}, 359 "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, 360 "end": []string{testTime.Format(time.RFC3339Nano)}, 361 }, 362 }, 363 364 { 365 do: doDeleteSeries("up", testTime.Add(-time.Minute), testTime), 366 inErr: fmt.Errorf("some error"), 367 reqMethod: "POST", 368 reqPath: "/api/v1/admin/tsdb/delete_series", 369 reqParam: url.Values{ 370 "match": []string{"up"}, 371 "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, 372 "end": []string{testTime.Format(time.RFC3339Nano)}, 373 }, 374 err: fmt.Errorf("some error"), 375 }, 376 377 { 378 do: doConfig(), 379 reqMethod: "GET", 380 reqPath: "/api/v1/status/config", 381 inRes: map[string]string{ 382 "yaml": "<content of the loaded config file in YAML>", 383 }, 384 res: ConfigResult{ 385 YAML: "<content of the loaded config file in YAML>", 386 }, 387 }, 388 389 { 390 do: doConfig(), 391 reqMethod: "GET", 392 reqPath: "/api/v1/status/config", 393 inErr: fmt.Errorf("some error"), 394 err: fmt.Errorf("some error"), 395 }, 396 397 { 398 do: doFlags(), 399 reqMethod: "GET", 400 reqPath: "/api/v1/status/flags", 401 inRes: map[string]string{ 402 "alertmanager.notification-queue-capacity": "10000", 403 "alertmanager.timeout": "10s", 404 "log.level": "info", 405 "query.lookback-delta": "5m", 406 "query.max-concurrency": "20", 407 }, 408 res: FlagsResult{ 409 "alertmanager.notification-queue-capacity": "10000", 410 "alertmanager.timeout": "10s", 411 "log.level": "info", 412 "query.lookback-delta": "5m", 413 "query.max-concurrency": "20", 414 }, 415 }, 416 417 { 418 do: doFlags(), 419 reqMethod: "GET", 420 reqPath: "/api/v1/status/flags", 421 inErr: fmt.Errorf("some error"), 422 err: fmt.Errorf("some error"), 423 }, 424 425 { 426 do: doAlertManagers(), 427 reqMethod: "GET", 428 reqPath: "/api/v1/alertmanagers", 429 inRes: map[string]interface{}{ 430 "activeAlertManagers": []map[string]string{ 431 { 432 "url": "http://127.0.0.1:9091/api/v1/alerts", 433 }, 434 }, 435 "droppedAlertManagers": []map[string]string{ 436 { 437 "url": "http://127.0.0.1:9092/api/v1/alerts", 438 }, 439 }, 440 }, 441 res: AlertManagersResult{ 442 Active: []AlertManager{ 443 { 444 URL: "http://127.0.0.1:9091/api/v1/alerts", 445 }, 446 }, 447 Dropped: []AlertManager{ 448 { 449 URL: "http://127.0.0.1:9092/api/v1/alerts", 450 }, 451 }, 452 }, 453 }, 454 455 { 456 do: doAlertManagers(), 457 reqMethod: "GET", 458 reqPath: "/api/v1/alertmanagers", 459 inErr: fmt.Errorf("some error"), 460 err: fmt.Errorf("some error"), 461 }, 462 463 { 464 do: doTargets(), 465 reqMethod: "GET", 466 reqPath: "/api/v1/targets", 467 inRes: map[string]interface{}{ 468 "activeTargets": []map[string]interface{}{ 469 { 470 "discoveredLabels": map[string]string{ 471 "__address__": "127.0.0.1:9090", 472 "__metrics_path__": "/metrics", 473 "__scheme__": "http", 474 "job": "prometheus", 475 }, 476 "labels": map[string]string{ 477 "instance": "127.0.0.1:9090", 478 "job": "prometheus", 479 }, 480 "scrapeUrl": "http://127.0.0.1:9090", 481 "lastError": "error while scraping target", 482 "lastScrape": testTime.UTC().Format(time.RFC3339Nano), 483 "health": "up", 484 }, 485 }, 486 "droppedTargets": []map[string]interface{}{ 487 { 488 "discoveredLabels": map[string]string{ 489 "__address__": "127.0.0.1:9100", 490 "__metrics_path__": "/metrics", 491 "__scheme__": "http", 492 "job": "node", 493 }, 494 }, 495 }, 496 }, 497 res: TargetsResult{ 498 Active: []ActiveTarget{ 499 { 500 DiscoveredLabels: model.LabelSet{ 501 "__address__": "127.0.0.1:9090", 502 "__metrics_path__": "/metrics", 503 "__scheme__": "http", 504 "job": "prometheus", 505 }, 506 Labels: model.LabelSet{ 507 "instance": "127.0.0.1:9090", 508 "job": "prometheus", 509 }, 510 ScrapeURL: "http://127.0.0.1:9090", 511 LastError: "error while scraping target", 512 LastScrape: testTime.UTC(), 513 Health: HealthGood, 514 }, 515 }, 516 Dropped: []DroppedTarget{ 517 { 518 DiscoveredLabels: model.LabelSet{ 519 "__address__": "127.0.0.1:9100", 520 "__metrics_path__": "/metrics", 521 "__scheme__": "http", 522 "job": "node", 523 }, 524 }, 525 }, 526 }, 527 }, 528 529 { 530 do: doTargets(), 531 reqMethod: "GET", 532 reqPath: "/api/v1/targets", 533 inErr: fmt.Errorf("some error"), 534 err: fmt.Errorf("some error"), 535 }, 536 } 537 538 var tests []apiTest 539 tests = append(tests, queryTests...) 540 541 for i, test := range tests { 542 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 543 client.curTest = test 544 545 res, err := test.do() 546 547 if test.err != nil { 548 if err == nil { 549 t.Fatalf("expected error %q but got none", test.err) 550 } 551 if err.Error() != test.err.Error() { 552 t.Errorf("unexpected error: want %s, got %s", test.err, err) 553 } 554 if apiErr, ok := err.(*Error); ok { 555 if apiErr.Detail != test.inRes { 556 t.Errorf("%q should be %q", apiErr.Detail, test.inRes) 557 } 558 } 559 return 560 } 561 if err != nil { 562 t.Fatalf("unexpected error: %s", err) 563 } 564 565 if !reflect.DeepEqual(res, test.res) { 566 t.Errorf("unexpected result: want %v, got %v", test.res, res) 567 } 568 }) 569 } 570} 571 572type testClient struct { 573 *testing.T 574 575 ch chan apiClientTest 576 req *http.Request 577} 578 579type apiClientTest struct { 580 code int 581 response interface{} 582 expectedBody string 583 expectedErr *Error 584} 585 586func (c *testClient) URL(ep string, args map[string]string) *url.URL { 587 return nil 588} 589 590func (c *testClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { 591 if ctx == nil { 592 c.Fatalf("context was not passed down") 593 } 594 if req != c.req { 595 c.Fatalf("request was not passed down") 596 } 597 598 test := <-c.ch 599 600 var b []byte 601 var err error 602 603 switch v := test.response.(type) { 604 case string: 605 b = []byte(v) 606 default: 607 b, err = json.Marshal(v) 608 if err != nil { 609 c.Fatal(err) 610 } 611 } 612 613 resp := &http.Response{ 614 StatusCode: test.code, 615 } 616 617 return resp, b, nil 618} 619 620func TestAPIClientDo(t *testing.T) { 621 tests := []apiClientTest{ 622 { 623 code: statusAPIError, 624 response: &apiResponse{ 625 Status: "error", 626 Data: json.RawMessage(`null`), 627 ErrorType: ErrBadData, 628 Error: "failed", 629 }, 630 expectedErr: &Error{ 631 Type: ErrBadData, 632 Msg: "failed", 633 }, 634 expectedBody: `null`, 635 }, 636 { 637 code: statusAPIError, 638 response: &apiResponse{ 639 Status: "error", 640 Data: json.RawMessage(`"test"`), 641 ErrorType: ErrTimeout, 642 Error: "timed out", 643 }, 644 expectedErr: &Error{ 645 Type: ErrTimeout, 646 Msg: "timed out", 647 }, 648 expectedBody: `test`, 649 }, 650 { 651 code: http.StatusInternalServerError, 652 response: "500 error details", 653 expectedErr: &Error{ 654 Type: ErrServer, 655 Msg: "server error: 500", 656 Detail: "500 error details", 657 }, 658 }, 659 { 660 code: http.StatusNotFound, 661 response: "404 error details", 662 expectedErr: &Error{ 663 Type: ErrClient, 664 Msg: "client error: 404", 665 Detail: "404 error details", 666 }, 667 }, 668 { 669 code: http.StatusBadRequest, 670 response: &apiResponse{ 671 Status: "error", 672 Data: json.RawMessage(`null`), 673 ErrorType: ErrBadData, 674 Error: "end timestamp must not be before start time", 675 }, 676 expectedErr: &Error{ 677 Type: ErrBadData, 678 Msg: "end timestamp must not be before start time", 679 }, 680 }, 681 { 682 code: statusAPIError, 683 response: "bad json", 684 expectedErr: &Error{ 685 Type: ErrBadResponse, 686 Msg: "invalid character 'b' looking for beginning of value", 687 }, 688 }, 689 { 690 code: statusAPIError, 691 response: &apiResponse{ 692 Status: "success", 693 Data: json.RawMessage(`"test"`), 694 }, 695 expectedErr: &Error{ 696 Type: ErrBadResponse, 697 Msg: "inconsistent body for response code", 698 }, 699 }, 700 { 701 code: statusAPIError, 702 response: &apiResponse{ 703 Status: "success", 704 Data: json.RawMessage(`"test"`), 705 ErrorType: ErrTimeout, 706 Error: "timed out", 707 }, 708 expectedErr: &Error{ 709 Type: ErrBadResponse, 710 Msg: "inconsistent body for response code", 711 }, 712 }, 713 { 714 code: http.StatusOK, 715 response: &apiResponse{ 716 Status: "error", 717 Data: json.RawMessage(`"test"`), 718 ErrorType: ErrTimeout, 719 Error: "timed out", 720 }, 721 expectedErr: &Error{ 722 Type: ErrBadResponse, 723 Msg: "inconsistent body for response code", 724 }, 725 }, 726 } 727 728 tc := &testClient{ 729 T: t, 730 ch: make(chan apiClientTest, 1), 731 req: &http.Request{}, 732 } 733 client := &apiClient{tc} 734 735 for i, test := range tests { 736 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 737 738 tc.ch <- test 739 740 _, body, err := client.Do(context.Background(), tc.req) 741 742 if test.expectedErr != nil { 743 if err == nil { 744 t.Fatalf("expected error %q but got none", test.expectedErr) 745 } 746 if test.expectedErr.Error() != err.Error() { 747 t.Errorf("unexpected error: want %q, got %q", test.expectedErr, err) 748 } 749 if test.expectedErr.Detail != "" { 750 apiErr := err.(*Error) 751 if apiErr.Detail != test.expectedErr.Detail { 752 t.Errorf("unexpected error details: want %q, got %q", test.expectedErr.Detail, apiErr.Detail) 753 } 754 } 755 return 756 } 757 if err != nil { 758 t.Fatalf("unexpeceted error %s", err) 759 } 760 761 want, got := test.expectedBody, string(body) 762 if want != got { 763 t.Errorf("unexpected body: want %q, got %q", want, got) 764 } 765 }) 766 767 } 768} 769