github.com/galamsiva2020/kubernetes-heapster-monitoring@v0.0.0-20210823134957-3c1baa7c1e70/metrics/api/v1/historical_handlers_test.go (about) 1 // Copyright 2016 Google Inc. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package v1 16 17 import ( 18 "bytes" 19 "encoding/json" 20 "fmt" 21 "net/http" 22 "net/url" 23 "testing" 24 "time" 25 26 "github.com/stretchr/testify/assert" 27 "github.com/stretchr/testify/require" 28 29 restful "github.com/emicklei/go-restful" 30 "k8s.io/heapster/metrics/api/v1/types" 31 "k8s.io/heapster/metrics/core" 32 ) 33 34 type metricReq struct { 35 name string 36 keys []core.HistoricalKey 37 start time.Time 38 end time.Time 39 labels map[string]string 40 41 aggregations []core.AggregationType 42 bucketSize time.Duration 43 } 44 45 type fakeHistoricalSource struct { 46 metricNames map[core.HistoricalKey][]string 47 48 nodes []string 49 namespaces []string 50 podsForNamespace map[string][]string 51 containersForNode map[string][]string 52 53 metricRequests []metricReq 54 aggregationRequests []metricReq 55 56 nowTime time.Time 57 } 58 59 func (src *fakeHistoricalSource) GetMetric(metricName string, metricKeys []core.HistoricalKey, start, end time.Time) (map[core.HistoricalKey][]core.TimestampedMetricValue, error) { 60 return src.GetLabeledMetric(metricName, nil, metricKeys, start, end) 61 } 62 63 func (src *fakeHistoricalSource) GetLabeledMetric(metricName string, labels map[string]string, metricKeys []core.HistoricalKey, start, end time.Time) (map[core.HistoricalKey][]core.TimestampedMetricValue, error) { 64 if metricName == "invalid" { 65 return nil, fmt.Errorf("fake error fetching metrics") 66 } 67 68 src.metricRequests = append(src.metricRequests, metricReq{ 69 name: metricName, 70 keys: metricKeys, 71 labels: labels, 72 start: start, 73 end: end, 74 }) 75 76 res := make(map[core.HistoricalKey][]core.TimestampedMetricValue, len(metricKeys)) 77 78 for _, key := range metricKeys { 79 res[key] = []core.TimestampedMetricValue{ 80 { 81 Timestamp: src.nowTime.Add(-10 * time.Second), 82 MetricValue: core.MetricValue{ 83 ValueType: core.ValueFloat, 84 FloatValue: 33, 85 }, 86 }, 87 } 88 } 89 90 return res, nil 91 } 92 93 func (src *fakeHistoricalSource) GetAggregation(metricName string, aggregations []core.AggregationType, metricKeys []core.HistoricalKey, start, end time.Time, bucketSize time.Duration) (map[core.HistoricalKey][]core.TimestampedAggregationValue, error) { 94 return src.GetLabeledAggregation(metricName, nil, aggregations, metricKeys, start, end, bucketSize) 95 } 96 97 func (src *fakeHistoricalSource) GetLabeledAggregation(metricName string, labels map[string]string, aggregations []core.AggregationType, metricKeys []core.HistoricalKey, start, end time.Time, bucketSize time.Duration) (map[core.HistoricalKey][]core.TimestampedAggregationValue, error) { 98 if metricName == "invalid" { 99 return nil, fmt.Errorf("fake error fetching metrics") 100 } 101 102 src.metricRequests = append(src.aggregationRequests, metricReq{ 103 name: metricName, 104 keys: metricKeys, 105 start: start, 106 end: end, 107 labels: labels, 108 109 aggregations: aggregations, 110 bucketSize: bucketSize, 111 }) 112 113 res := make(map[core.HistoricalKey][]core.TimestampedAggregationValue, len(metricKeys)) 114 115 for _, key := range metricKeys { 116 countVal := uint64(10) 117 res[key] = []core.TimestampedAggregationValue{ 118 { 119 Timestamp: src.nowTime.Add(-10 * time.Second), 120 BucketSize: 10 * time.Second, 121 AggregationValue: core.AggregationValue{ 122 Count: &countVal, 123 }, 124 }, 125 } 126 } 127 128 return res, nil 129 } 130 131 func (src *fakeHistoricalSource) GetMetricNames(metricKey core.HistoricalKey) ([]string, error) { 132 if names, ok := src.metricNames[metricKey]; ok { 133 return names, nil 134 } 135 136 return nil, fmt.Errorf("no such object %q", metricKey.String()) 137 } 138 139 func (src *fakeHistoricalSource) GetNodes() ([]string, error) { 140 return src.nodes, nil 141 } 142 143 func (src *fakeHistoricalSource) GetNamespaces() ([]string, error) { 144 return src.namespaces, nil 145 } 146 147 func (src *fakeHistoricalSource) GetPodsFromNamespace(namespace string) ([]string, error) { 148 if pods, ok := src.podsForNamespace[namespace]; ok { 149 return pods, nil 150 } 151 152 return nil, fmt.Errorf("no such namespace %q", namespace) 153 } 154 155 func (src *fakeHistoricalSource) GetSystemContainersFromNode(node string) ([]string, error) { 156 if conts, ok := src.containersForNode[node]; ok { 157 return conts, nil 158 } 159 160 return nil, fmt.Errorf("no such node %q", node) 161 } 162 163 func prepApi() (*HistoricalApi, *fakeHistoricalSource) { 164 histSrc := &fakeHistoricalSource{ 165 metricNames: make(map[core.HistoricalKey][]string), 166 } 167 168 api := &HistoricalApi{ 169 Api: &Api{ 170 historicalSource: histSrc, 171 }, 172 } 173 174 return api, histSrc 175 } 176 177 type fakeRespRecorder struct { 178 headers http.Header 179 status int 180 data *bytes.Buffer 181 } 182 183 func (r *fakeRespRecorder) Header() http.Header { 184 return r.headers 185 } 186 187 func (r *fakeRespRecorder) WriteHeader(status int) { 188 r.status = status 189 } 190 191 func (r *fakeRespRecorder) Write(content []byte) (int, error) { 192 return r.data.Write(content) 193 } 194 195 func TestAvailableMetrics(t *testing.T) { 196 api, src := prepApi() 197 198 src.metricNames = map[core.HistoricalKey][]string{ 199 { 200 ObjectType: core.MetricSetTypeCluster, 201 }: {"cm1", "cm2"}, 202 203 { 204 ObjectType: core.MetricSetTypeNode, 205 NodeName: "somenode1", 206 }: {"nm1", "nm2"}, 207 208 { 209 ObjectType: core.MetricSetTypeNamespace, 210 NamespaceName: "somens1", 211 }: {"nsm1", "nsm2"}, 212 213 { 214 ObjectType: core.MetricSetTypePod, 215 NamespaceName: "somens1", 216 PodName: "somepod1", 217 }: {"pm1", "pm2"}, 218 219 { 220 ObjectType: core.MetricSetTypePodContainer, 221 NamespaceName: "somens1", 222 PodName: "somepod1", 223 ContainerName: "somecont1", 224 }: {"pcm1", "pcm2"}, 225 226 { 227 ObjectType: core.MetricSetTypeSystemContainer, 228 NodeName: "somenode1", 229 ContainerName: "somecont1", 230 }: {"ncm1", "ncm2"}, 231 } 232 233 tests := []struct { 234 name string 235 fun func(request *restful.Request, response *restful.Response) 236 pathParams map[string]string 237 expectedNames []string 238 }{ 239 { 240 name: "cluster metrics", 241 fun: api.availableClusterMetrics, 242 pathParams: map[string]string{}, 243 expectedNames: []string{"cm1", "cm2"}, 244 }, 245 { 246 name: "node metrics", 247 fun: api.availableNodeMetrics, 248 pathParams: map[string]string{"node-name": "somenode1"}, 249 expectedNames: []string{"nm1", "nm2"}, 250 }, 251 { 252 name: "namespace metrics", 253 fun: api.availableNamespaceMetrics, 254 pathParams: map[string]string{"namespace-name": "somens1"}, 255 expectedNames: []string{"nsm1", "nsm2"}, 256 }, 257 { 258 name: "pod metrics", 259 fun: api.availablePodMetrics, 260 pathParams: map[string]string{ 261 "namespace-name": "somens1", 262 "pod-name": "somepod1", 263 }, 264 expectedNames: []string{"pm1", "pm2"}, 265 }, 266 { 267 name: "pod container metrics", 268 fun: api.availablePodContainerMetrics, 269 pathParams: map[string]string{ 270 "namespace-name": "somens1", 271 "pod-name": "somepod1", 272 "container-name": "somecont1", 273 }, 274 expectedNames: []string{"pcm1", "pcm2"}, 275 }, 276 { 277 name: "free container metrics", 278 fun: api.availableFreeContainerMetrics, 279 pathParams: map[string]string{ 280 "node-name": "somenode1", 281 "container-name": "somecont1", 282 }, 283 expectedNames: []string{"ncm1", "ncm2"}, 284 }, 285 } 286 287 assert := assert.New(t) 288 restful.DefaultResponseMimeType = restful.MIME_JSON 289 290 for _, test := range tests { 291 req := restful.NewRequest(&http.Request{}) 292 pathParams := req.PathParameters() 293 for k, v := range test.pathParams { 294 pathParams[k] = v 295 } 296 recorder := &fakeRespRecorder{ 297 data: new(bytes.Buffer), 298 headers: make(http.Header), 299 } 300 resp := restful.NewResponse(recorder) 301 302 test.fun(req, resp) 303 304 actualNames := []string{} 305 if err := json.Unmarshal(recorder.data.Bytes(), &actualNames); err != nil { 306 t.Fatalf("Unexpected error: %v", err) 307 } 308 309 assert.Equal(http.StatusOK, recorder.status, "status should have been OK (200)") 310 assert.Equal(test.expectedNames, actualNames, "should have gotten expected JSON") 311 } 312 313 for _, test := range tests { 314 if len(test.pathParams) == 0 { 315 // don't test tests with no parameters for invalid parameters 316 continue 317 } 318 req := restful.NewRequest(&http.Request{}) 319 pathParams := req.PathParameters() 320 for k := range test.pathParams { 321 pathParams[k] = "some-other-value" 322 } 323 recorder := &fakeRespRecorder{ 324 data: new(bytes.Buffer), 325 headers: make(http.Header), 326 } 327 resp := restful.NewResponse(recorder) 328 329 test.fun(req, resp) 330 331 assert.Equal(http.StatusInternalServerError, recorder.status, "status should have been InternalServerError (500)") 332 } 333 } 334 335 func TestListObjects(t *testing.T) { 336 api, src := prepApi() 337 338 src.nodes = []string{"node1", "node2"} 339 src.namespaces = []string{"ns1", "ns2"} 340 src.podsForNamespace = map[string][]string{ 341 "ns1": {"pod1", "pod2"}, 342 } 343 src.containersForNode = map[string][]string{ 344 "node1": {"x/y/z", "a/b/c"}, 345 } 346 347 tests := []struct { 348 name string 349 fun func(request *restful.Request, response *restful.Response) 350 pathParams map[string]string 351 expectedNames []string 352 }{ 353 { 354 name: "nodes", 355 fun: api.nodeList, 356 expectedNames: []string{"node1", "node2"}, 357 }, 358 { 359 name: "namespaces", 360 fun: api.namespaceList, 361 expectedNames: []string{"ns1", "ns2"}, 362 }, 363 { 364 name: "pods in namespace", 365 fun: api.namespacePodList, 366 pathParams: map[string]string{"namespace-name": "ns1"}, 367 expectedNames: []string{"pod1", "pod2"}, 368 }, 369 { 370 name: "free containers on node", 371 fun: api.nodeSystemContainerList, 372 pathParams: map[string]string{ 373 "node-name": "node1", 374 }, 375 expectedNames: []string{"x/y/z", "a/b/c"}, 376 }, 377 } 378 379 assert := assert.New(t) 380 restful.DefaultResponseMimeType = restful.MIME_JSON 381 382 for _, test := range tests { 383 req := restful.NewRequest(&http.Request{}) 384 pathParams := req.PathParameters() 385 for k, v := range test.pathParams { 386 pathParams[k] = v 387 } 388 recorder := &fakeRespRecorder{ 389 data: new(bytes.Buffer), 390 headers: make(http.Header), 391 } 392 resp := restful.NewResponse(recorder) 393 394 test.fun(req, resp) 395 396 actualNames := []string{} 397 if err := json.Unmarshal(recorder.data.Bytes(), &actualNames); err != nil { 398 t.Fatalf("Unexpected error: %v", err) 399 } 400 401 assert.Equal(http.StatusOK, recorder.status, "status should have been OK (200)") 402 assert.Equal(test.expectedNames, actualNames, "should have gotten expected JSON") 403 } 404 405 for _, test := range tests { 406 if len(test.pathParams) == 0 { 407 // don't test tests with no parameters for invalid parameters 408 continue 409 } 410 req := restful.NewRequest(&http.Request{}) 411 pathParams := req.PathParameters() 412 for k := range test.pathParams { 413 pathParams[k] = "some-other-value" 414 } 415 recorder := &fakeRespRecorder{ 416 data: new(bytes.Buffer), 417 headers: make(http.Header), 418 } 419 resp := restful.NewResponse(recorder) 420 421 test.fun(req, resp) 422 423 assert.Equal(http.StatusInternalServerError, recorder.status, "status should have been InternalServerError (500)") 424 } 425 } 426 427 func TestFetchMetrics(t *testing.T) { 428 api, src := prepApi() 429 nowTime := time.Now().UTC().Truncate(time.Second) 430 src.nowTime = nowTime 431 nowFunc = func() time.Time { return nowTime } 432 433 tests := []struct { 434 test string 435 start string 436 end string 437 labels string 438 fun func(*restful.Request, *restful.Response) 439 pathParams map[string]string 440 expectedMetricReq metricReq 441 expectedStatus int 442 }{ 443 { 444 test: "cluster metrics", 445 fun: api.clusterMetrics, 446 pathParams: map[string]string{ 447 "metric-name": "some-metric", 448 }, 449 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 450 expectedMetricReq: metricReq{ 451 name: "some-metric", 452 start: nowTime.Add(-10 * time.Second), 453 keys: []core.HistoricalKey{ 454 {ObjectType: core.MetricSetTypeCluster}, 455 }, 456 }, 457 }, 458 { 459 test: "node metrics", 460 fun: api.nodeMetrics, 461 pathParams: map[string]string{ 462 "metric-name": "some-metric", 463 "node-name": "node1", 464 }, 465 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 466 expectedMetricReq: metricReq{ 467 name: "some-metric", 468 start: nowTime.Add(-10 * time.Second), 469 keys: []core.HistoricalKey{ 470 {ObjectType: core.MetricSetTypeNode, NodeName: "node1"}, 471 }, 472 }, 473 }, 474 { 475 test: "namespace metrics", 476 fun: api.namespaceMetrics, 477 pathParams: map[string]string{ 478 "metric-name": "some-metric", 479 "namespace-name": "ns1", 480 }, 481 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 482 expectedMetricReq: metricReq{ 483 name: "some-metric", 484 start: nowTime.Add(-10 * time.Second), 485 keys: []core.HistoricalKey{ 486 {ObjectType: core.MetricSetTypeNamespace, NamespaceName: "ns1"}, 487 }, 488 }, 489 }, 490 { 491 test: "pod name metrics", 492 fun: api.podMetrics, 493 pathParams: map[string]string{ 494 "metric-name": "some-metric", 495 "namespace-name": "ns1", 496 "pod-name": "pod1", 497 }, 498 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 499 expectedMetricReq: metricReq{ 500 name: "some-metric", 501 start: nowTime.Add(-10 * time.Second), 502 keys: []core.HistoricalKey{ 503 {ObjectType: core.MetricSetTypePod, NamespaceName: "ns1", PodName: "pod1"}, 504 }, 505 }, 506 }, 507 { 508 test: "pod id metrics", 509 fun: api.podMetrics, 510 pathParams: map[string]string{ 511 "metric-name": "some-metric", 512 "pod-id": "pod-1-id", 513 }, 514 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 515 expectedMetricReq: metricReq{ 516 name: "some-metric", 517 start: nowTime.Add(-10 * time.Second), 518 keys: []core.HistoricalKey{ 519 {ObjectType: core.MetricSetTypePod, PodId: "pod-1-id"}, 520 }, 521 }, 522 }, 523 { 524 test: "pod name container metrics", 525 fun: api.podContainerMetrics, 526 pathParams: map[string]string{ 527 "metric-name": "some-metric", 528 "namespace-name": "ns1", 529 "pod-name": "pod1", 530 "container-name": "cont1", 531 }, 532 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 533 expectedMetricReq: metricReq{ 534 name: "some-metric", 535 start: nowTime.Add(-10 * time.Second), 536 keys: []core.HistoricalKey{ 537 {ObjectType: core.MetricSetTypePodContainer, NamespaceName: "ns1", PodName: "pod1", ContainerName: "cont1"}, 538 }, 539 }, 540 }, 541 { 542 test: "pod id container metrics", 543 fun: api.podContainerMetrics, 544 pathParams: map[string]string{ 545 "metric-name": "some-metric", 546 "pod-id": "pod-1-id", 547 "container-name": "cont1", 548 }, 549 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 550 expectedMetricReq: metricReq{ 551 name: "some-metric", 552 start: nowTime.Add(-10 * time.Second), 553 keys: []core.HistoricalKey{ 554 {ObjectType: core.MetricSetTypePodContainer, PodId: "pod-1-id", ContainerName: "cont1"}, 555 }, 556 }, 557 }, 558 { 559 test: "system container metrics", 560 fun: api.freeContainerMetrics, 561 pathParams: map[string]string{ 562 "metric-name": "some-metric", 563 "node-name": "node1", 564 "container-name": "cont1", 565 }, 566 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 567 expectedMetricReq: metricReq{ 568 name: "some-metric", 569 start: nowTime.Add(-10 * time.Second), 570 keys: []core.HistoricalKey{ 571 {ObjectType: core.MetricSetTypeSystemContainer, NodeName: "node1", ContainerName: "cont1"}, 572 }, 573 }, 574 }, 575 { 576 test: "query with end", 577 fun: api.clusterMetrics, 578 pathParams: map[string]string{ 579 "metric-name": "some-metric", 580 }, 581 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 582 end: nowTime.Format(time.RFC3339), 583 expectedMetricReq: metricReq{ 584 name: "some-metric", 585 keys: []core.HistoricalKey{ 586 {ObjectType: core.MetricSetTypeCluster}, 587 }, 588 start: nowTime.Add(-10 * time.Second), 589 end: nowTime, 590 }, 591 }, 592 { 593 test: "query with labels", 594 fun: api.clusterMetrics, 595 pathParams: map[string]string{ 596 "metric-name": "some-metric", 597 }, 598 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 599 labels: "somelbl:v1,otherlbl:v2.3:4", 600 expectedMetricReq: metricReq{ 601 name: "some-metric", 602 keys: []core.HistoricalKey{ 603 {ObjectType: core.MetricSetTypeCluster}, 604 }, 605 labels: map[string]string{"somelbl": "v1", "otherlbl": "v2.3:4"}, 606 start: nowTime.Add(-10 * time.Second), 607 }, 608 }, 609 { 610 test: "query with bad start", 611 fun: api.clusterMetrics, 612 start: "afdsfd", 613 expectedStatus: http.StatusBadRequest, 614 }, 615 { 616 test: "query with no start", 617 fun: api.clusterMetrics, 618 expectedStatus: http.StatusBadRequest, 619 }, 620 { 621 test: "query with error while fetching metrics", 622 fun: api.clusterMetrics, 623 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 624 pathParams: map[string]string{ 625 "metric-name": "invalid", 626 }, 627 expectedStatus: http.StatusInternalServerError, 628 }, 629 { 630 test: "query with bad labels", 631 fun: api.clusterMetrics, 632 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 633 labels: "abc:", 634 expectedStatus: http.StatusBadRequest, 635 }, 636 } 637 638 assert := assert.New(t) 639 restful.DefaultResponseMimeType = restful.MIME_JSON 640 641 // doesn't particularly correspond to the query -- we're just using it to 642 // test conversion between internal types 643 expectedNormalVals := types.MetricResult{ 644 LatestTimestamp: nowTime.Add(-10 * time.Second), 645 Metrics: []types.MetricPoint{ 646 { 647 Timestamp: nowTime.Add(-10 * time.Second), 648 Value: 33, 649 }, 650 }, 651 } 652 653 for _, test := range tests { 654 queryParams := make(url.Values) 655 queryParams.Add("start", test.start) 656 queryParams.Add("end", test.end) 657 queryParams.Add("labels", test.labels) 658 u := &url.URL{RawQuery: queryParams.Encode()} 659 req := restful.NewRequest(&http.Request{URL: u}) 660 pathParams := req.PathParameters() 661 for k, v := range test.pathParams { 662 pathParams[k] = v 663 } 664 recorder := &fakeRespRecorder{ 665 data: new(bytes.Buffer), 666 headers: make(http.Header), 667 } 668 resp := restful.NewResponse(recorder) 669 670 test.fun(req, resp) 671 672 if test.expectedStatus != 0 { 673 assert.Equal(test.expectedStatus, recorder.status, "for test %q: status should have been an error status", test.test) 674 } else { 675 if !assert.Equal(http.StatusOK, recorder.status, "for test %q: status should have been OK (200)", test.test) { 676 continue 677 } 678 679 actualReq := src.metricRequests[len(src.metricRequests)-1] 680 if test.expectedMetricReq.end.IsZero() { 681 test.expectedMetricReq.end = nowTime 682 } 683 assert.Equal(test.expectedMetricReq, actualReq, "for test %q: expected a different metric request to have been placed", test.test) 684 685 actualVals := types.MetricResult{} 686 if err := json.Unmarshal(recorder.data.Bytes(), &actualVals); err != nil { 687 t.Fatalf("Unexpected error: %v", err) 688 } 689 690 assert.Equal(expectedNormalVals, actualVals, "for test %q: should have gotten expected JSON", test.test) 691 } 692 } 693 694 listTests := []struct { 695 test string 696 start string 697 end string 698 labels string 699 fun func(*restful.Request, *restful.Response) 700 pathParams map[string]string 701 expectedMetricReq metricReq 702 expectedStatus int 703 }{ 704 { 705 test: "pod list by ids", 706 fun: api.podListMetrics, 707 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 708 pathParams: map[string]string{ 709 "metric-name": "some-metric", 710 "pod-id-list": "pod-id-1,pod-id-2,pod-id-3", 711 }, 712 expectedMetricReq: metricReq{ 713 name: "some-metric", 714 keys: []core.HistoricalKey{ 715 {ObjectType: core.MetricSetTypePod, PodId: "pod-id-1"}, 716 {ObjectType: core.MetricSetTypePod, PodId: "pod-id-2"}, 717 {ObjectType: core.MetricSetTypePod, PodId: "pod-id-3"}, 718 }, 719 start: nowTime.Add(-10 * time.Second), 720 }, 721 }, 722 { 723 test: "pod list by names", 724 fun: api.podListMetrics, 725 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 726 pathParams: map[string]string{ 727 "metric-name": "some-metric", 728 "namespace-name": "ns1", 729 "pod-list": "pod1,pod2,pod3", 730 }, 731 expectedMetricReq: metricReq{ 732 name: "some-metric", 733 keys: []core.HistoricalKey{ 734 {ObjectType: core.MetricSetTypePod, NamespaceName: "ns1", PodName: "pod1"}, 735 {ObjectType: core.MetricSetTypePod, NamespaceName: "ns1", PodName: "pod2"}, 736 {ObjectType: core.MetricSetTypePod, NamespaceName: "ns1", PodName: "pod3"}, 737 }, 738 start: nowTime.Add(-10 * time.Second), 739 }, 740 }, 741 { 742 test: "pod list with labels", 743 fun: api.podListMetrics, 744 pathParams: map[string]string{ 745 "metric-name": "some-metric", 746 "pod-id-list": "pod-id-1,pod-id-2,pod-id-3", 747 }, 748 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 749 labels: "somelbl:v1,otherlbl:v2.3:4", 750 expectedMetricReq: metricReq{ 751 name: "some-metric", 752 keys: []core.HistoricalKey{ 753 {ObjectType: core.MetricSetTypePod, PodId: "pod-id-1"}, 754 {ObjectType: core.MetricSetTypePod, PodId: "pod-id-2"}, 755 {ObjectType: core.MetricSetTypePod, PodId: "pod-id-3"}, 756 }, 757 labels: map[string]string{"somelbl": "v1", "otherlbl": "v2.3:4"}, 758 start: nowTime.Add(-10 * time.Second), 759 }, 760 }, 761 { 762 test: "pod list with bad start time", 763 fun: api.podListMetrics, 764 start: "afdsfd", 765 expectedStatus: http.StatusBadRequest, 766 }, 767 { 768 test: "pod list with error fetching metrics", 769 fun: api.podListMetrics, 770 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 771 pathParams: map[string]string{ 772 "metric-name": "invalid", 773 }, 774 expectedStatus: http.StatusInternalServerError, 775 }, 776 { 777 test: "pod list with no start", 778 fun: api.podListMetrics, 779 expectedStatus: http.StatusBadRequest, 780 }, 781 { 782 test: "query with bad labels", 783 fun: api.clusterMetrics, 784 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 785 labels: "abc:", 786 expectedStatus: http.StatusBadRequest, 787 }, 788 } 789 790 expectedListVals := types.MetricResultList{ 791 Items: []types.MetricResult{ 792 expectedNormalVals, 793 expectedNormalVals, 794 expectedNormalVals, 795 }, 796 } 797 798 for _, test := range listTests { 799 queryParams := make(url.Values) 800 queryParams.Add("start", test.start) 801 queryParams.Add("end", test.end) 802 queryParams.Add("labels", test.labels) 803 u := &url.URL{RawQuery: queryParams.Encode()} 804 req := restful.NewRequest(&http.Request{URL: u}) 805 pathParams := req.PathParameters() 806 for k, v := range test.pathParams { 807 pathParams[k] = v 808 } 809 recorder := &fakeRespRecorder{ 810 data: new(bytes.Buffer), 811 headers: make(http.Header), 812 } 813 resp := restful.NewResponse(recorder) 814 815 test.fun(req, resp) 816 817 if test.expectedStatus != 0 { 818 assert.Equal(test.expectedStatus, recorder.status, "for test %q: status should have been an error status", test.test) 819 } else { 820 if !assert.Equal(http.StatusOK, recorder.status, "for test %q: status should have been OK (200)", test.test) { 821 continue 822 } 823 824 actualReq := src.metricRequests[len(src.metricRequests)-1] 825 if test.expectedMetricReq.end.IsZero() { 826 test.expectedMetricReq.end = nowTime 827 } 828 829 assert.Equal(test.expectedMetricReq, actualReq, "for test %q: expected a different metric request to have been placed", test.test) 830 831 actualVals := types.MetricResultList{} 832 if err := json.Unmarshal(recorder.data.Bytes(), &actualVals); err != nil { 833 t.Fatalf("Unexpected error: %v", err) 834 } 835 836 assert.Equal(expectedListVals, actualVals, "for test %q: should have gotten expected JSON", test.test) 837 } 838 } 839 } 840 841 func TestFetchAggregations(t *testing.T) { 842 api, src := prepApi() 843 nowTime := time.Now().UTC().Truncate(time.Second) 844 src.nowTime = nowTime 845 nowFunc = func() time.Time { return nowTime } 846 847 tests := []struct { 848 test string 849 bucketSize string 850 start string 851 end string 852 labels string 853 fun func(*restful.Request, *restful.Response) 854 pathParams map[string]string 855 expectedMetricReq metricReq 856 expectedStatus int 857 }{ 858 { 859 test: "cluster aggregations", 860 fun: api.clusterAggregations, 861 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 862 pathParams: map[string]string{ 863 "metric-name": "some-metric", 864 "aggregations": "count,average", 865 }, 866 expectedMetricReq: metricReq{ 867 name: "some-metric", 868 start: nowTime.Add(-10 * time.Second), 869 keys: []core.HistoricalKey{ 870 {ObjectType: core.MetricSetTypeCluster}, 871 }, 872 }, 873 }, 874 { 875 test: "node aggregations", 876 fun: api.nodeAggregations, 877 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 878 pathParams: map[string]string{ 879 "metric-name": "some-metric", 880 "node-name": "node1", 881 "aggregations": "count,average", 882 }, 883 expectedMetricReq: metricReq{ 884 name: "some-metric", 885 start: nowTime.Add(-10 * time.Second), 886 keys: []core.HistoricalKey{ 887 {ObjectType: core.MetricSetTypeNode, NodeName: "node1"}, 888 }, 889 }, 890 }, 891 { 892 test: "namespace aggregations", 893 fun: api.namespaceAggregations, 894 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 895 pathParams: map[string]string{ 896 "metric-name": "some-metric", 897 "namespace-name": "ns1", 898 "aggregations": "count,average", 899 }, 900 expectedMetricReq: metricReq{ 901 name: "some-metric", 902 start: nowTime.Add(-10 * time.Second), 903 keys: []core.HistoricalKey{ 904 {ObjectType: core.MetricSetTypeNamespace, NamespaceName: "ns1"}, 905 }, 906 }, 907 }, 908 { 909 test: "pod name aggregations", 910 fun: api.podAggregations, 911 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 912 pathParams: map[string]string{ 913 "metric-name": "some-metric", 914 "namespace-name": "ns1", 915 "pod-name": "pod1", 916 "aggregations": "count,average", 917 }, 918 expectedMetricReq: metricReq{ 919 name: "some-metric", 920 start: nowTime.Add(-10 * time.Second), 921 keys: []core.HistoricalKey{ 922 {ObjectType: core.MetricSetTypePod, NamespaceName: "ns1", PodName: "pod1"}, 923 }, 924 }, 925 }, 926 { 927 test: "pod id aggregations", 928 fun: api.podAggregations, 929 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 930 pathParams: map[string]string{ 931 "metric-name": "some-metric", 932 "pod-id": "pod-1-id", 933 "aggregations": "count,average", 934 }, 935 expectedMetricReq: metricReq{ 936 name: "some-metric", 937 start: nowTime.Add(-10 * time.Second), 938 keys: []core.HistoricalKey{ 939 {ObjectType: core.MetricSetTypePod, PodId: "pod-1-id"}, 940 }, 941 }, 942 }, 943 { 944 test: "pod name container aggregations", 945 fun: api.podContainerAggregations, 946 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 947 pathParams: map[string]string{ 948 "metric-name": "some-metric", 949 "namespace-name": "ns1", 950 "pod-name": "pod1", 951 "container-name": "cont1", 952 "aggregations": "count,average", 953 }, 954 expectedMetricReq: metricReq{ 955 name: "some-metric", 956 start: nowTime.Add(-10 * time.Second), 957 keys: []core.HistoricalKey{ 958 {ObjectType: core.MetricSetTypePodContainer, NamespaceName: "ns1", PodName: "pod1", ContainerName: "cont1"}, 959 }, 960 }, 961 }, 962 { 963 test: "pod id container aggregations", 964 fun: api.podContainerAggregations, 965 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 966 pathParams: map[string]string{ 967 "metric-name": "some-metric", 968 "pod-id": "pod-1-id", 969 "container-name": "cont1", 970 "aggregations": "count,average", 971 }, 972 expectedMetricReq: metricReq{ 973 name: "some-metric", 974 start: nowTime.Add(-10 * time.Second), 975 keys: []core.HistoricalKey{ 976 {ObjectType: core.MetricSetTypePodContainer, PodId: "pod-1-id", ContainerName: "cont1"}, 977 }, 978 }, 979 }, 980 { 981 test: "system container aggregations", 982 fun: api.freeContainerAggregations, 983 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 984 pathParams: map[string]string{ 985 "metric-name": "some-metric", 986 "node-name": "node1", 987 "container-name": "cont1", 988 "aggregations": "count,average", 989 }, 990 expectedMetricReq: metricReq{ 991 name: "some-metric", 992 start: nowTime.Add(-10 * time.Second), 993 keys: []core.HistoricalKey{ 994 {ObjectType: core.MetricSetTypeSystemContainer, NodeName: "node1", ContainerName: "cont1"}, 995 }, 996 }, 997 }, 998 { 999 test: "aggregations with end and bucket", 1000 fun: api.clusterAggregations, 1001 pathParams: map[string]string{ 1002 "metric-name": "some-metric", 1003 "aggregations": "count,average", 1004 }, 1005 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 1006 end: nowTime.Format(time.RFC3339), 1007 bucketSize: "20s", 1008 expectedMetricReq: metricReq{ 1009 name: "some-metric", 1010 keys: []core.HistoricalKey{ 1011 {ObjectType: core.MetricSetTypeCluster}, 1012 }, 1013 start: nowTime.Add(-10 * time.Second), 1014 end: nowTime, 1015 bucketSize: 20 * time.Second, 1016 }, 1017 }, 1018 { 1019 test: "aggregations with labels", 1020 fun: api.clusterAggregations, 1021 pathParams: map[string]string{ 1022 "metric-name": "some-metric", 1023 "aggregations": "count,average", 1024 }, 1025 labels: "somelbl:v1,otherlbl:v2.3:4", 1026 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 1027 expectedMetricReq: metricReq{ 1028 name: "some-metric", 1029 keys: []core.HistoricalKey{ 1030 {ObjectType: core.MetricSetTypeCluster}, 1031 }, 1032 start: nowTime.Add(-10 * time.Second), 1033 labels: map[string]string{"somelbl": "v1", "otherlbl": "v2.3:4"}, 1034 }, 1035 }, 1036 { 1037 test: "aggregations with bad start time", 1038 fun: api.clusterAggregations, 1039 start: "afdsfd", 1040 expectedStatus: http.StatusBadRequest, 1041 }, 1042 { 1043 test: "aggregations with fetch error", 1044 fun: api.clusterAggregations, 1045 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 1046 pathParams: map[string]string{ 1047 "metric-name": "invalid", 1048 "aggregations": "count,average", 1049 }, 1050 expectedStatus: http.StatusInternalServerError, 1051 }, 1052 { 1053 test: "aggregations with no start time", 1054 fun: api.clusterAggregations, 1055 expectedStatus: http.StatusBadRequest, 1056 }, 1057 } 1058 1059 assert := assert.New(t) 1060 restful.DefaultResponseMimeType = restful.MIME_JSON 1061 1062 // doesn't particularly correspond to the query -- we're just using it to 1063 // test conversion between internal types 1064 countVal := uint64(10) 1065 expectedNormalVals := types.MetricAggregationResult{ 1066 BucketSize: 10 * time.Second, 1067 Buckets: []types.MetricAggregationBucket{ 1068 { 1069 Timestamp: src.nowTime.Add(-10 * time.Second), 1070 Count: &countVal, 1071 }, 1072 }, 1073 } 1074 1075 aggList := []core.AggregationType{ 1076 core.AggregationTypeCount, 1077 core.AggregationTypeAverage, 1078 } 1079 1080 for _, test := range tests { 1081 queryParams := make(url.Values) 1082 queryParams.Add("start", test.start) 1083 queryParams.Add("end", test.end) 1084 queryParams.Add("bucket", test.bucketSize) 1085 queryParams.Add("labels", test.labels) 1086 u := &url.URL{RawQuery: queryParams.Encode()} 1087 req := restful.NewRequest(&http.Request{URL: u}) 1088 pathParams := req.PathParameters() 1089 for k, v := range test.pathParams { 1090 pathParams[k] = v 1091 } 1092 recorder := &fakeRespRecorder{ 1093 data: new(bytes.Buffer), 1094 headers: make(http.Header), 1095 } 1096 resp := restful.NewResponse(recorder) 1097 1098 test.fun(req, resp) 1099 1100 if test.expectedStatus != 0 { 1101 assert.Equal(test.expectedStatus, recorder.status, "for test %q: status should have been an error status", test.test) 1102 } else { 1103 if !assert.Equal(http.StatusOK, recorder.status, "for test %q: status should have been OK (200)", test.test) { 1104 continue 1105 } 1106 1107 actualReq := src.metricRequests[len(src.metricRequests)-1] 1108 if test.expectedMetricReq.end.IsZero() { 1109 test.expectedMetricReq.end = nowTime 1110 } 1111 1112 test.expectedMetricReq.aggregations = aggList 1113 assert.Equal(test.expectedMetricReq, actualReq, "for test %q: expected a different metric request to have been placed", test.test) 1114 1115 actualVals := types.MetricAggregationResult{} 1116 if err := json.Unmarshal(recorder.data.Bytes(), &actualVals); err != nil { 1117 t.Fatalf("Unexpected error: %v", err) 1118 } 1119 1120 assert.Equal(expectedNormalVals, actualVals, "for test %q: should have gotten expected JSON", test.test) 1121 } 1122 } 1123 1124 listTests := []struct { 1125 test string 1126 start string 1127 labels string 1128 end string 1129 fun func(*restful.Request, *restful.Response) 1130 pathParams map[string]string 1131 expectedMetricReq metricReq 1132 expectedStatus int 1133 }{ 1134 { 1135 test: "pod id list aggregations", 1136 fun: api.podListAggregations, 1137 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 1138 pathParams: map[string]string{ 1139 "metric-name": "some-metric", 1140 "pod-id-list": "pod-id-1,pod-id-2,pod-id-3", 1141 "aggregations": "count,average", 1142 }, 1143 expectedMetricReq: metricReq{ 1144 name: "some-metric", 1145 keys: []core.HistoricalKey{ 1146 {ObjectType: core.MetricSetTypePod, PodId: "pod-id-1"}, 1147 {ObjectType: core.MetricSetTypePod, PodId: "pod-id-2"}, 1148 {ObjectType: core.MetricSetTypePod, PodId: "pod-id-3"}, 1149 }, 1150 start: nowTime.Add(-10 * time.Second), 1151 }, 1152 }, 1153 { 1154 test: "pod name list aggregations", 1155 fun: api.podListAggregations, 1156 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 1157 pathParams: map[string]string{ 1158 "metric-name": "some-metric", 1159 "namespace-name": "ns1", 1160 "pod-list": "pod1,pod2,pod3", 1161 "aggregations": "count,average", 1162 }, 1163 expectedMetricReq: metricReq{ 1164 name: "some-metric", 1165 keys: []core.HistoricalKey{ 1166 {ObjectType: core.MetricSetTypePod, NamespaceName: "ns1", PodName: "pod1"}, 1167 {ObjectType: core.MetricSetTypePod, NamespaceName: "ns1", PodName: "pod2"}, 1168 {ObjectType: core.MetricSetTypePod, NamespaceName: "ns1", PodName: "pod3"}, 1169 }, 1170 start: nowTime.Add(-10 * time.Second), 1171 }, 1172 }, 1173 { 1174 test: "pod list aggregations with labels", 1175 fun: api.podListAggregations, 1176 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 1177 labels: "somelbl:v1,otherlbl:v2.3:4", 1178 pathParams: map[string]string{ 1179 "metric-name": "some-metric", 1180 "namespace-name": "ns1", 1181 "pod-list": "pod1,pod2,pod3", 1182 "aggregations": "count,average", 1183 }, 1184 expectedMetricReq: metricReq{ 1185 name: "some-metric", 1186 keys: []core.HistoricalKey{ 1187 {ObjectType: core.MetricSetTypePod, NamespaceName: "ns1", PodName: "pod1"}, 1188 {ObjectType: core.MetricSetTypePod, NamespaceName: "ns1", PodName: "pod2"}, 1189 {ObjectType: core.MetricSetTypePod, NamespaceName: "ns1", PodName: "pod3"}, 1190 }, 1191 start: nowTime.Add(-10 * time.Second), 1192 labels: map[string]string{"somelbl": "v1", "otherlbl": "v2.3:4"}, 1193 }, 1194 }, 1195 { 1196 test: "pod list aggregations with bad start time", 1197 fun: api.podListAggregations, 1198 start: "afdsfd", 1199 expectedStatus: http.StatusBadRequest, 1200 }, 1201 { 1202 test: "pod list aggregations with fetch error", 1203 fun: api.podListAggregations, 1204 start: nowTime.Add(-10 * time.Second).Format(time.RFC3339), 1205 pathParams: map[string]string{ 1206 "metric-name": "invalid", 1207 "aggregations": "count,average", 1208 }, 1209 expectedStatus: http.StatusInternalServerError, 1210 }, 1211 { 1212 test: "pod list aggregations with no start time", 1213 fun: api.podListAggregations, 1214 expectedStatus: http.StatusBadRequest, 1215 }, 1216 } 1217 1218 expectedListVals := types.MetricAggregationResultList{ 1219 Items: []types.MetricAggregationResult{ 1220 expectedNormalVals, 1221 expectedNormalVals, 1222 expectedNormalVals, 1223 }, 1224 } 1225 1226 for _, test := range listTests { 1227 queryParams := make(url.Values) 1228 queryParams.Add("start", test.start) 1229 queryParams.Add("end", test.end) 1230 queryParams.Add("labels", test.labels) 1231 u := &url.URL{RawQuery: queryParams.Encode()} 1232 req := restful.NewRequest(&http.Request{URL: u}) 1233 pathParams := req.PathParameters() 1234 for k, v := range test.pathParams { 1235 pathParams[k] = v 1236 } 1237 recorder := &fakeRespRecorder{ 1238 data: new(bytes.Buffer), 1239 headers: make(http.Header), 1240 } 1241 resp := restful.NewResponse(recorder) 1242 1243 test.fun(req, resp) 1244 1245 if test.expectedStatus != 0 { 1246 assert.Equal(test.expectedStatus, recorder.status, "for test %q: status should have been an error status", test.test) 1247 } else { 1248 if !assert.Equal(http.StatusOK, recorder.status, "for test %q: status should have been OK (200)", test.test) { 1249 continue 1250 } 1251 1252 actualReq := src.metricRequests[len(src.metricRequests)-1] 1253 if test.expectedMetricReq.end.IsZero() { 1254 test.expectedMetricReq.end = nowTime 1255 } 1256 test.expectedMetricReq.aggregations = aggList 1257 assert.Equal(test.expectedMetricReq, actualReq, "for test %q: expected a different metric request to have been placed", test.test) 1258 1259 actualVals := types.MetricAggregationResultList{} 1260 if err := json.Unmarshal(recorder.data.Bytes(), &actualVals); err != nil { 1261 t.Fatalf("Unexpected error: %v", err) 1262 } 1263 1264 assert.Equal(expectedListVals, actualVals, "for test %q: should have gotten expected JSON", test.test) 1265 } 1266 } 1267 } 1268 1269 func TestGetBucketSize(t *testing.T) { 1270 tests := []struct { 1271 sizeParam string 1272 expectedDuration time.Duration 1273 expectedError bool 1274 }{ 1275 { 1276 // empty duration 1277 sizeParam: "", 1278 expectedDuration: 0, 1279 }, 1280 { 1281 // no units 1282 sizeParam: "1", 1283 expectedError: true, 1284 }, 1285 { 1286 // unknown unit 1287 sizeParam: "1g", 1288 expectedError: true, 1289 }, 1290 { 1291 // invalid unit (looks like ms) 1292 sizeParam: "10gs", 1293 expectedError: true, 1294 }, 1295 { 1296 // NaN 1297 sizeParam: "abch", 1298 expectedError: true, 1299 }, 1300 { 1301 sizeParam: "5ms", 1302 expectedDuration: 5 * time.Millisecond, 1303 }, 1304 { 1305 sizeParam: "10s", 1306 expectedDuration: 10 * time.Second, 1307 }, 1308 { 1309 sizeParam: "15m", 1310 expectedDuration: 15 * time.Minute, 1311 }, 1312 { 1313 sizeParam: "20h", 1314 expectedDuration: 20 * time.Hour, 1315 }, 1316 { 1317 sizeParam: "25d", 1318 expectedDuration: 600 * time.Hour, 1319 }, 1320 } 1321 1322 assert := assert.New(t) 1323 for _, test := range tests { 1324 u, err := url.Parse(fmt.Sprintf("/foo?bucket=%s", test.sizeParam)) 1325 if err != nil { 1326 t.Fatalf("Unexpected error: %v", err) 1327 } 1328 req := restful.NewRequest(&http.Request{URL: u}) 1329 res, err := getBucketSize(req) 1330 if test.expectedError { 1331 assert.Error(err, fmt.Sprintf("%q should have been an invalid value", test.sizeParam)) 1332 } else if assert.NoError(err, fmt.Sprintf("%q should have been a valid value", test.sizeParam)) { 1333 assert.Equal(test.expectedDuration, res, fmt.Sprintf("%q should have given the correct duration", test.sizeParam)) 1334 } 1335 } 1336 } 1337 1338 func TestGetAggregations(t *testing.T) { 1339 assert := assert.New(t) 1340 1341 validAggregations := "average,max" 1342 invalidAggregations := "max,non-existant" 1343 1344 req := restful.NewRequest(&http.Request{}) 1345 pathParams := req.PathParameters() 1346 pathParams["aggregations"] = validAggregations 1347 1348 validRes, validErr := getAggregations(req) 1349 if assert.NoError(validErr, "expected valid list to not produce an error") { 1350 assert.Equal([]core.AggregationType{core.AggregationTypeAverage, core.AggregationTypeMaximum}, validRes, "expected valid list to be properly split and converted to AggregationType values") 1351 } 1352 1353 pathParams["aggregations"] = invalidAggregations 1354 _, invalidErr := getAggregations(req) 1355 assert.Error(invalidErr, "expected list with unknown aggregations to produce an error") 1356 } 1357 1358 func TestExportMetricValue(t *testing.T) { 1359 assert := assert.New(t) 1360 1361 assert.Nil(exportMetricValue(nil), "a nil input value should yield a nil output value") 1362 1363 intVal := &core.MetricValue{ 1364 IntValue: 33, 1365 ValueType: core.ValueInt64, 1366 } 1367 outputIntVal := exportMetricValue(intVal) 1368 if assert.NotNil(outputIntVal, "a non-nil input should yield a non-nil output") && assert.NotNil(outputIntVal.IntValue, "an int-valued input should yield an output with the IntValue set") { 1369 assert.Equal(intVal.IntValue, *outputIntVal.IntValue, "the input int value should be the same as the output int value") 1370 } 1371 1372 floatVal := &core.MetricValue{ 1373 FloatValue: 66.0, 1374 ValueType: core.ValueFloat, 1375 } 1376 outputFloatVal := exportMetricValue(floatVal) 1377 if assert.NotNil(outputFloatVal, "a non-nil input should yield a non-nil output") && assert.NotNil(outputFloatVal.FloatValue, "an float-valued input should yield an output with the FloatValue set") { 1378 assert.Equal(float64(floatVal.FloatValue), *outputFloatVal.FloatValue, "the input float value should be the same as the output float value (as a float64)") 1379 } 1380 } 1381 1382 func TestExtractMetricValue(t *testing.T) { 1383 assert := assert.New(t) 1384 1385 aggregationVal := &core.AggregationValue{ 1386 Aggregations: map[core.AggregationType]core.MetricValue{ 1387 core.AggregationTypeAverage: { 1388 FloatValue: 66.0, 1389 ValueType: core.ValueFloat, 1390 }, 1391 }, 1392 } 1393 1394 avgRes := extractMetricValue(aggregationVal, core.AggregationTypeAverage) 1395 if assert.NotNil(avgRes, "a present aggregation should yield a non-nil output") && assert.NotNil(avgRes.FloatValue, "the output float value should be set when the input aggregation value is of the float type") { 1396 assert.Equal(66.0, *avgRes.FloatValue, "the output float value should be the same as the aggregation's float value") 1397 } 1398 1399 maxRes := extractMetricValue(aggregationVal, core.AggregationTypeMaximum) 1400 assert.Nil(maxRes, "a non-present aggregation should yield a nil output") 1401 } 1402 1403 func TestExportTimestampedAggregationValue(t *testing.T) { 1404 assert := assert.New(t) 1405 require := require.New(t) 1406 nowTime := time.Now().UTC() 1407 1408 count10 := uint64(10) 1409 count11 := uint64(11) 1410 values := []core.TimestampedAggregationValue{ 1411 { 1412 Timestamp: nowTime.Add(-20 * time.Second), 1413 BucketSize: 10 * time.Second, 1414 AggregationValue: core.AggregationValue{ 1415 Count: &count10, 1416 Aggregations: map[core.AggregationType]core.MetricValue{ 1417 core.AggregationTypeAverage: { 1418 FloatValue: 66.0, 1419 ValueType: core.ValueFloat, 1420 }, 1421 core.AggregationTypePercentile50: { 1422 IntValue: 44, 1423 ValueType: core.ValueInt64, 1424 }, 1425 }, 1426 }, 1427 }, 1428 { 1429 Timestamp: nowTime.Add(-10 * time.Second), 1430 BucketSize: 10 * time.Second, 1431 AggregationValue: core.AggregationValue{ 1432 Count: &count11, 1433 Aggregations: map[core.AggregationType]core.MetricValue{ 1434 core.AggregationTypeAverage: { 1435 FloatValue: 88.0, 1436 ValueType: core.ValueFloat, 1437 }, 1438 core.AggregationTypePercentile50: { 1439 IntValue: 55, 1440 ValueType: core.ValueInt64, 1441 }, 1442 }, 1443 }, 1444 }, 1445 } 1446 1447 res := exportTimestampedAggregationValue(values) 1448 1449 assert.Equal(10*time.Second, res.BucketSize, "the output bucket size should be 10s") 1450 require.Equal(len(values), len(res.Buckets), "there should be an output bucket for every input value") 1451 1452 for i, bucket := range res.Buckets { 1453 inputVal := values[i] 1454 1455 assert.Equal(inputVal.Count, bucket.Count, "the output bucket should have the same sample count as the input value") 1456 1457 if assert.NotNil(bucket.Average, "the output bucket should have the average set") { 1458 metricVal := inputVal.Aggregations[core.AggregationTypeAverage] 1459 assert.Equal(exportMetricValue(&metricVal), bucket.Average, "the output bucket should have an average value equal to that of the input value") 1460 } 1461 1462 percVal, ok := bucket.Percentiles["50"] 1463 if assert.True(ok, "the output bucket should have the 50th percentile set") { 1464 metricVal := inputVal.Aggregations[core.AggregationTypePercentile50] 1465 assert.Equal(exportMetricValue(&metricVal), &percVal, "the output bucket should have a 50th-percentile value equal to that of the input value") 1466 } 1467 } 1468 } 1469 1470 func TestGetLabels(t *testing.T) { 1471 assert := assert.New(t) 1472 1473 tests := []struct { 1474 test string 1475 input string 1476 outputVal map[string]string 1477 outputError bool 1478 }{ 1479 { 1480 test: "working labels", 1481 input: "k1:v1,k2:v2.3:4+5", 1482 outputVal: map[string]string{"k1": "v1", "k2": "v2.3:4+5"}, 1483 }, 1484 { 1485 test: "bad label (no separator)", 1486 input: "k1,k2:v2", 1487 outputError: true, 1488 }, 1489 { 1490 test: "bad label (no key)", 1491 input: "k1:v1,:v2", 1492 outputError: true, 1493 }, 1494 { 1495 test: "bad label (no value)", 1496 input: "k1:v1,k1:", 1497 outputError: true, 1498 }, 1499 { 1500 test: "empty", 1501 input: "", 1502 outputVal: nil, 1503 }, 1504 } 1505 1506 for _, test := range tests { 1507 queryParams := make(url.Values) 1508 queryParams.Add("labels", test.input) 1509 u := &url.URL{RawQuery: queryParams.Encode()} 1510 req := restful.NewRequest(&http.Request{URL: u}) 1511 res, err := getLabels(req) 1512 if test.outputError && !assert.Error(err, "test %q should have yielded an error", test.test) { 1513 continue 1514 } else if !test.outputError && !assert.NoError(err, "test %q should not have yielded an error", test.test) { 1515 continue 1516 } 1517 1518 assert.Equal(test.outputVal, res, "test %q should have output the correct label map", test.test) 1519 } 1520 }