1// Copyright 2018, OpenCensus Authors 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 15package stackdriver 16 17import ( 18 "context" 19 "fmt" 20 "strings" 21 "testing" 22 23 resourcepb "github.com/census-instrumentation/opencensus-proto/gen-go/resource/v1" 24 "github.com/golang/protobuf/ptypes/timestamp" 25 "google.golang.org/api/option" 26 distributionpb "google.golang.org/genproto/googleapis/api/distribution" 27 labelpb "google.golang.org/genproto/googleapis/api/label" 28 googlemetricpb "google.golang.org/genproto/googleapis/api/metric" 29 monitoredrespb "google.golang.org/genproto/googleapis/api/monitoredres" 30 monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3" 31 "google.golang.org/grpc" 32 "google.golang.org/protobuf/testing/protocmp" 33 34 metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1" 35 "github.com/golang/protobuf/ptypes/wrappers" 36 "github.com/google/go-cmp/cmp" 37 "go.opencensus.io/resource/resourcekeys" 38) 39 40func TestExportTimeSeriesWithDifferentLabels(t *testing.T) { 41 server, addr, doneFn := createFakeServer(t) 42 defer doneFn() 43 44 // Now create a gRPC connection to the agent. 45 conn, err := grpc.Dial(addr, grpc.WithInsecure()) 46 if err != nil { 47 t.Fatalf("Failed to make a gRPC connection to the agent: %v", err) 48 } 49 defer conn.Close() 50 51 // Finally create the OpenCensus stats exporter 52 exporterOptions := Options{ 53 ProjectID: "equivalence", 54 MonitoringClientOptions: []option.ClientOption{option.WithGRPCConn(conn)}, 55 56 // Set empty labels to avoid the opencensus-task 57 DefaultMonitoringLabels: &Labels{}, 58 MapResource: DefaultMapResource, 59 } 60 se, err := newStatsExporter(exporterOptions) 61 if err != nil { 62 t.Fatalf("Failed to create the statsExporter: %v", err) 63 } 64 65 startTimestamp := ×tamp.Timestamp{ 66 Seconds: 1543160298, 67 Nanos: 100000090, 68 } 69 endTimestamp := ×tamp.Timestamp{ 70 Seconds: 1543160298, 71 Nanos: 101000090, 72 } 73 74 // Generate the proto Metrics. 75 var metricPbs []*metricspb.Metric 76 metricPbs = append(metricPbs, 77 &metricspb.Metric{ 78 MetricDescriptor: &metricspb.MetricDescriptor{ 79 Name: "ocagent.io/calls", 80 Description: "The number of the various calls", 81 LabelKeys: []*metricspb.LabelKey{ 82 { 83 Key: "empty_key", 84 }, 85 { 86 Key: "operation_type", 87 }, 88 }, 89 Unit: "1", 90 Type: metricspb.MetricDescriptor_CUMULATIVE_INT64, 91 }, 92 Timeseries: []*metricspb.TimeSeries{ 93 { 94 StartTimestamp: startTimestamp, 95 LabelValues: []*metricspb.LabelValue{ 96 { 97 Value: "", 98 HasValue: true, 99 }, 100 { 101 Value: "test_1", 102 HasValue: true, 103 }, 104 }, 105 Points: []*metricspb.Point{ 106 { 107 Timestamp: endTimestamp, 108 Value: &metricspb.Point_Int64Value{Int64Value: int64(1)}, 109 }, 110 }, 111 }, 112 { 113 StartTimestamp: startTimestamp, 114 LabelValues: []*metricspb.LabelValue{ 115 { 116 Value: "", 117 HasValue: true, 118 }, 119 { 120 Value: "test_2", 121 HasValue: true, 122 }, 123 }, 124 Points: []*metricspb.Point{ 125 { 126 Timestamp: endTimestamp, 127 Value: &metricspb.Point_Int64Value{Int64Value: int64(1)}, 128 }, 129 }, 130 }, 131 }, 132 }) 133 134 var wantTimeSeries []*monitoringpb.CreateTimeSeriesRequest 135 wantTimeSeries = append(wantTimeSeries, &monitoringpb.CreateTimeSeriesRequest{ 136 Name: "projects/equivalence", 137 TimeSeries: []*monitoringpb.TimeSeries{ 138 { 139 Metric: &googlemetricpb.Metric{ 140 Type: "custom.googleapis.com/opencensus/ocagent.io/calls", 141 Labels: map[string]string{ 142 "empty_key": "", 143 "operation_type": "test_1", 144 }, 145 }, 146 Resource: &monitoredrespb.MonitoredResource{ 147 Type: "global", 148 }, 149 MetricKind: googlemetricpb.MetricDescriptor_CUMULATIVE, 150 ValueType: googlemetricpb.MetricDescriptor_INT64, 151 Points: []*monitoringpb.Point{ 152 { 153 Interval: &monitoringpb.TimeInterval{ 154 StartTime: startTimestamp, 155 EndTime: endTimestamp, 156 }, 157 Value: &monitoringpb.TypedValue{ 158 Value: &monitoringpb.TypedValue_Int64Value{ 159 Int64Value: 1, 160 }, 161 }, 162 }, 163 }, 164 }, 165 { 166 Metric: &googlemetricpb.Metric{ 167 Type: "custom.googleapis.com/opencensus/ocagent.io/calls", 168 Labels: map[string]string{ 169 "empty_key": "", 170 "operation_type": "test_2", 171 }, 172 }, 173 Resource: &monitoredrespb.MonitoredResource{ 174 Type: "global", 175 }, 176 MetricKind: googlemetricpb.MetricDescriptor_CUMULATIVE, 177 ValueType: googlemetricpb.MetricDescriptor_INT64, 178 Points: []*monitoringpb.Point{ 179 { 180 Interval: &monitoringpb.TimeInterval{ 181 StartTime: startTimestamp, 182 EndTime: endTimestamp, 183 }, 184 Value: &monitoringpb.TypedValue{ 185 Value: &monitoringpb.TypedValue_Int64Value{ 186 Int64Value: 1, 187 }, 188 }, 189 }, 190 }, 191 }, 192 }, 193 }) 194 195 // Export the proto Metrics to the Stackdriver backend. 196 dropped, err := se.PushMetricsProto(context.Background(), nil, nil, metricPbs) 197 if dropped != 0 || err != nil { 198 t.Fatalf("Error pushing metrics, dropped:%d err:%v", dropped, err) 199 } 200 201 var gotTimeSeries []*monitoringpb.CreateTimeSeriesRequest 202 server.forEachStackdriverTimeSeries(func(sdt *monitoringpb.CreateTimeSeriesRequest) { 203 gotTimeSeries = append(gotTimeSeries, sdt) 204 }) 205 206 requireTimeSeriesRequestEqual(t, gotTimeSeries, wantTimeSeries) 207} 208 209func TestProtoMetricToCreateTimeSeriesRequest(t *testing.T) { 210 startTimestamp := ×tamp.Timestamp{ 211 Seconds: 1543160298, 212 Nanos: 100000090, 213 } 214 endTimestamp := ×tamp.Timestamp{ 215 Seconds: 1543160298, 216 Nanos: 101000090, 217 } 218 219 tests := []struct { 220 name string 221 in *metricspb.Metric 222 want []*monitoringpb.CreateTimeSeriesRequest 223 wantErr string 224 statsExporter *statsExporter 225 }{ 226 { 227 name: "Test converting Distribution", 228 in: &metricspb.Metric{ 229 MetricDescriptor: &metricspb.MetricDescriptor{ 230 Name: "with_metric_descriptor", 231 Description: "This is a test", 232 Unit: "By", 233 }, 234 Timeseries: []*metricspb.TimeSeries{ 235 { 236 StartTimestamp: startTimestamp, 237 Points: []*metricspb.Point{ 238 { 239 Timestamp: endTimestamp, 240 Value: &metricspb.Point_DistributionValue{ 241 DistributionValue: &metricspb.DistributionValue{ 242 Count: 1, 243 Sum: 11.9, 244 SumOfSquaredDeviation: 0, 245 Buckets: []*metricspb.DistributionValue_Bucket{ 246 {Count: 1}, {}, {}, {}, 247 }, 248 BucketOptions: &metricspb.DistributionValue_BucketOptions{ 249 Type: &metricspb.DistributionValue_BucketOptions_Explicit_{ 250 Explicit: &metricspb.DistributionValue_BucketOptions_Explicit{ 251 // Without zero bucket in 252 Bounds: []float64{10, 20, 30, 40}, 253 }, 254 }, 255 }, 256 }, 257 }, 258 }, 259 }, 260 }, 261 }, 262 }, 263 statsExporter: &statsExporter{ 264 o: Options{ProjectID: "foo", MapResource: DefaultMapResource}, 265 }, 266 want: []*monitoringpb.CreateTimeSeriesRequest{ 267 { 268 Name: "projects/foo", 269 TimeSeries: []*monitoringpb.TimeSeries{ 270 { 271 Metric: &googlemetricpb.Metric{ 272 Type: "custom.googleapis.com/opencensus/with_metric_descriptor", 273 Labels: nil, 274 }, 275 Resource: &monitoredrespb.MonitoredResource{ 276 Type: "global", 277 }, 278 MetricKind: googlemetricpb.MetricDescriptor_CUMULATIVE, 279 ValueType: googlemetricpb.MetricDescriptor_DISTRIBUTION, 280 Points: []*monitoringpb.Point{ 281 { 282 Interval: &monitoringpb.TimeInterval{ 283 StartTime: startTimestamp, 284 EndTime: endTimestamp, 285 }, 286 Value: &monitoringpb.TypedValue{ 287 Value: &monitoringpb.TypedValue_DistributionValue{ 288 DistributionValue: &distributionpb.Distribution{ 289 Count: 1, 290 Mean: 11.9, 291 SumOfSquaredDeviation: 0, 292 BucketCounts: []int64{0, 1, 0, 0, 0}, 293 BucketOptions: &distributionpb.Distribution_BucketOptions{ 294 Options: &distributionpb.Distribution_BucketOptions_ExplicitBuckets{ 295 ExplicitBuckets: &distributionpb.Distribution_BucketOptions_Explicit{ 296 Bounds: []float64{0, 10, 20, 30, 40}, 297 }, 298 }, 299 }, 300 }, 301 }, 302 }, 303 }, 304 }, 305 }, 306 }, 307 }, 308 }, 309 }, 310 { 311 name: "Test some label keys don't have values", 312 in: &metricspb.Metric{ 313 MetricDescriptor: &metricspb.MetricDescriptor{ 314 Name: "with_metric_descriptor_2", 315 Description: "This is a test", 316 Unit: "By", 317 LabelKeys: []*metricspb.LabelKey{{Key: "key1"}, {Key: "key2"}, {Key: "key3"}}, 318 }, 319 Timeseries: []*metricspb.TimeSeries{ 320 { 321 StartTimestamp: startTimestamp, 322 LabelValues: []*metricspb.LabelValue{{}, {}, {HasValue: true, Value: "val3"}}, 323 Points: []*metricspb.Point{ 324 { 325 Timestamp: endTimestamp, 326 Value: &metricspb.Point_DoubleValue{ 327 DoubleValue: 25.0, 328 }, 329 }, 330 }, 331 }, 332 }, 333 }, 334 statsExporter: &statsExporter{ 335 o: Options{ProjectID: "foo", MapResource: DefaultMapResource}, 336 }, 337 want: []*monitoringpb.CreateTimeSeriesRequest{ 338 { 339 Name: "projects/foo", 340 TimeSeries: []*monitoringpb.TimeSeries{ 341 { 342 Metric: &googlemetricpb.Metric{ 343 Type: "custom.googleapis.com/opencensus/with_metric_descriptor_2", 344 Labels: map[string]string{"key3": "val3"}, 345 }, 346 Resource: &monitoredrespb.MonitoredResource{ 347 Type: "global", 348 }, 349 MetricKind: googlemetricpb.MetricDescriptor_CUMULATIVE, 350 ValueType: googlemetricpb.MetricDescriptor_DISTRIBUTION, 351 Points: []*monitoringpb.Point{ 352 { 353 Interval: &monitoringpb.TimeInterval{ 354 StartTime: startTimestamp, 355 EndTime: endTimestamp, 356 }, 357 Value: &monitoringpb.TypedValue{ 358 Value: &monitoringpb.TypedValue_DoubleValue{ 359 DoubleValue: 25.0, 360 }, 361 }, 362 }, 363 }, 364 }, 365 }, 366 }, 367 }, 368 }, 369 } 370 371 seenResources := make(map[*resourcepb.Resource]*monitoredrespb.MonitoredResource) 372 373 for i, tt := range tests { 374 se := tt.statsExporter 375 if se == nil { 376 se = new(statsExporter) 377 } 378 allTss, err := protoMetricToTimeSeries(context.Background(), se, se.getResource(nil, tt.in, seenResources), tt.in) 379 if tt.wantErr != "" { 380 if err == nil || !strings.Contains(err.Error(), tt.wantErr) { 381 t.Errorf("#%v: unmatched error. Got\n\t%v\nWant\n\t%v", tt.name, err, tt.wantErr) 382 } 383 continue 384 } 385 if err != nil { 386 t.Errorf("#%v: unexpected error: %v", tt.name, err) 387 continue 388 } 389 390 got := se.combineTimeSeriesToCreateTimeSeriesRequest(allTss) 391 // Our saving grace is serialization equality since some 392 // unexported fields could be present in the various values. 393 if diff := cmpTSReqs(got, tt.want); diff != "" { 394 t.Fatalf("Test %d failed. Unexpected CreateTimeSeriesRequests -got +want: %s", i, diff) 395 } 396 } 397} 398 399func TestProtoMetricWithDifferentResource(t *testing.T) { 400 startTimestamp := ×tamp.Timestamp{ 401 Seconds: 1543160298, 402 Nanos: 100000090, 403 } 404 endTimestamp := ×tamp.Timestamp{ 405 Seconds: 1543160298, 406 Nanos: 101000090, 407 } 408 409 seenResources := make(map[*resourcepb.Resource]*monitoredrespb.MonitoredResource) 410 411 tests := []struct { 412 in *metricspb.Metric 413 want []*monitoringpb.CreateTimeSeriesRequest 414 wantErr string 415 statsExporter *statsExporter 416 }{ 417 { 418 in: &metricspb.Metric{ 419 MetricDescriptor: &metricspb.MetricDescriptor{ 420 Name: "with_container_resource", 421 Description: "This is a test", 422 Unit: "By", 423 Type: metricspb.MetricDescriptor_CUMULATIVE_INT64, 424 }, 425 Resource: &resourcepb.Resource{ 426 Type: resourcekeys.ContainerType, 427 Labels: map[string]string{ 428 resourcekeys.K8SKeyClusterName: "cluster1", 429 resourcekeys.K8SKeyPodName: "pod1", 430 resourcekeys.K8SKeyNamespaceName: "namespace1", 431 resourcekeys.ContainerKeyName: "container-name1", 432 resourcekeys.CloudKeyZone: "zone1", 433 }, 434 }, 435 Timeseries: []*metricspb.TimeSeries{ 436 { 437 StartTimestamp: startTimestamp, 438 Points: []*metricspb.Point{ 439 { 440 Timestamp: endTimestamp, 441 Value: &metricspb.Point_Int64Value{ 442 Int64Value: 1, 443 }, 444 }, 445 }, 446 }, 447 }, 448 }, 449 statsExporter: &statsExporter{ 450 o: Options{ProjectID: "foo", MapResource: DefaultMapResource}, 451 }, 452 want: []*monitoringpb.CreateTimeSeriesRequest{ 453 { 454 Name: "projects/foo", 455 TimeSeries: []*monitoringpb.TimeSeries{ 456 { 457 Metric: &googlemetricpb.Metric{ 458 Type: "custom.googleapis.com/opencensus/with_container_resource", 459 Labels: nil, 460 }, 461 Resource: &monitoredrespb.MonitoredResource{ 462 Type: "k8s_container", 463 Labels: map[string]string{ 464 "location": "zone1", 465 "cluster_name": "cluster1", 466 "namespace_name": "namespace1", 467 "pod_name": "pod1", 468 "container_name": "container-name1", 469 }, 470 }, 471 MetricKind: googlemetricpb.MetricDescriptor_CUMULATIVE, 472 ValueType: googlemetricpb.MetricDescriptor_INT64, 473 Points: []*monitoringpb.Point{ 474 { 475 Interval: &monitoringpb.TimeInterval{ 476 StartTime: startTimestamp, 477 EndTime: endTimestamp, 478 }, 479 Value: &monitoringpb.TypedValue{ 480 Value: &monitoringpb.TypedValue_Int64Value{ 481 Int64Value: 1, 482 }, 483 }, 484 }, 485 }, 486 }, 487 }, 488 }, 489 }, 490 }, 491 { 492 in: &metricspb.Metric{ 493 MetricDescriptor: &metricspb.MetricDescriptor{ 494 Name: "with_gce_resource", 495 Description: "This is a test", 496 Unit: "By", 497 Type: metricspb.MetricDescriptor_CUMULATIVE_INT64, 498 }, 499 Resource: &resourcepb.Resource{ 500 Type: resourcekeys.CloudType, 501 Labels: map[string]string{ 502 resourcekeys.CloudKeyProvider: resourcekeys.CloudProviderGCP, 503 resourcekeys.HostKeyID: "inst1", 504 resourcekeys.CloudKeyZone: "zone1", 505 }, 506 }, 507 Timeseries: []*metricspb.TimeSeries{ 508 { 509 StartTimestamp: startTimestamp, 510 Points: []*metricspb.Point{ 511 { 512 Timestamp: endTimestamp, 513 Value: &metricspb.Point_Int64Value{ 514 Int64Value: 1, 515 }, 516 }, 517 }, 518 }, 519 }, 520 }, 521 statsExporter: &statsExporter{ 522 o: Options{ProjectID: "foo", MapResource: DefaultMapResource}, 523 }, 524 want: []*monitoringpb.CreateTimeSeriesRequest{ 525 { 526 Name: "projects/foo", 527 TimeSeries: []*monitoringpb.TimeSeries{ 528 { 529 Metric: &googlemetricpb.Metric{ 530 Type: "custom.googleapis.com/opencensus/with_gce_resource", 531 Labels: nil, 532 }, 533 Resource: &monitoredrespb.MonitoredResource{ 534 Type: "gce_instance", 535 Labels: map[string]string{ 536 "instance_id": "inst1", 537 "zone": "zone1", 538 }, 539 }, 540 MetricKind: googlemetricpb.MetricDescriptor_CUMULATIVE, 541 ValueType: googlemetricpb.MetricDescriptor_INT64, 542 Points: []*monitoringpb.Point{ 543 { 544 Interval: &monitoringpb.TimeInterval{ 545 StartTime: startTimestamp, 546 EndTime: endTimestamp, 547 }, 548 Value: &monitoringpb.TypedValue{ 549 Value: &monitoringpb.TypedValue_Int64Value{ 550 Int64Value: 1, 551 }, 552 }, 553 }, 554 }, 555 }, 556 }, 557 }, 558 }, 559 }, 560 } 561 562 for i, tt := range tests { 563 se := tt.statsExporter 564 if se == nil { 565 se = new(statsExporter) 566 } 567 allTss, err := protoMetricToTimeSeries(context.Background(), se, se.getResource(nil, tt.in, seenResources), tt.in) 568 if tt.wantErr != "" { 569 if err == nil || !strings.Contains(err.Error(), tt.wantErr) { 570 t.Errorf("#%d: unmatched error. Got\n\t%v\nWant\n\t%v", i, err, tt.wantErr) 571 } 572 continue 573 } 574 if err != nil { 575 t.Errorf("#%d: unexpected error: %v", i, err) 576 continue 577 } 578 579 got := se.combineTimeSeriesToCreateTimeSeriesRequest(allTss) 580 // Our saving grace is serialization equality since some 581 // unexported fields could be present in the various values. 582 if diff := cmpTSReqs(got, tt.want); diff != "" { 583 t.Fatalf("Test %d failed. Unexpected CreateTimeSeriesRequests -got +want: %s", i, diff) 584 } 585 } 586 587 if len(seenResources) != 2 { 588 t.Errorf("Should cache 2 resources, got %d", len(seenResources)) 589 } 590} 591 592func TestProtoToMonitoringMetricDescriptor(t *testing.T) { 593 tests := []struct { 594 in *metricspb.Metric 595 want *googlemetricpb.MetricDescriptor 596 wantErr string 597 598 statsExporter *statsExporter 599 }{ 600 {in: nil, wantErr: "non-nil metric or metric descriptor"}, 601 { 602 in: &metricspb.Metric{}, 603 wantErr: "non-nil metric or metric descriptor", 604 }, 605 { 606 in: &metricspb.Metric{ 607 MetricDescriptor: &metricspb.MetricDescriptor{ 608 Name: "with_metric_descriptor", 609 Description: "This is with metric descriptor", 610 Unit: "By", 611 }, 612 }, 613 statsExporter: &statsExporter{ 614 o: Options{ProjectID: "test"}, 615 }, 616 want: &googlemetricpb.MetricDescriptor{ 617 Name: "projects/test/metricDescriptors/custom.googleapis.com/opencensus/with_metric_descriptor", 618 Type: "custom.googleapis.com/opencensus/with_metric_descriptor", 619 Labels: []*labelpb.LabelDescriptor{}, 620 DisplayName: "OpenCensus/with_metric_descriptor", 621 Description: "This is with metric descriptor", 622 Unit: "By", 623 }, 624 }, 625 { 626 in: &metricspb.Metric{ 627 MetricDescriptor: &metricspb.MetricDescriptor{ 628 Name: "external.googleapis.com/user/with_domain", 629 Description: "With metric descriptor and domain prefix", 630 Unit: "By", 631 }, 632 }, 633 statsExporter: &statsExporter{ 634 o: Options{ProjectID: "test"}, 635 }, 636 want: &googlemetricpb.MetricDescriptor{ 637 Name: "projects/test/metricDescriptors/external.googleapis.com/user/with_domain", 638 Type: "external.googleapis.com/user/with_domain", 639 Labels: []*labelpb.LabelDescriptor{}, 640 DisplayName: "external.googleapis.com/user/with_domain", 641 Description: "With metric descriptor and domain prefix", 642 Unit: "By", 643 }, 644 }, 645 } 646 647 for i, tt := range tests { 648 se := tt.statsExporter 649 if se == nil { 650 se = new(statsExporter) 651 } 652 got, err := se.protoToMonitoringMetricDescriptor(tt.in, nil) 653 if tt.wantErr != "" { 654 if err == nil || !strings.Contains(err.Error(), tt.wantErr) { 655 t.Errorf("#%d: \nGot %v\nWanted error substring %q", i, err, tt.wantErr) 656 } 657 continue 658 } 659 660 if err != nil { 661 t.Errorf("#%d: Unexpected error: %v", i, err) 662 continue 663 } 664 665 // Our saving grace is serialization equality since some 666 // unexported fields could be present in the various values. 667 if diff := cmpMD(got, tt.want); diff != "" { 668 t.Fatalf("Test %d failed. Unexpected MetricDescriptor -got +want: %s", i, diff) 669 } 670 } 671} 672 673func TestProtoMetricsToMonitoringMetrics_fromProtoPoint(t *testing.T) { 674 startTimestamp := ×tamp.Timestamp{ 675 Seconds: 1543160298, 676 Nanos: 100000090, 677 } 678 endTimestamp := ×tamp.Timestamp{ 679 Seconds: 1543160298, 680 Nanos: 101000090, 681 } 682 683 tests := []struct { 684 in *metricspb.Point 685 want *monitoringpb.Point 686 wantErr string 687 }{ 688 { 689 in: &metricspb.Point{ 690 Timestamp: endTimestamp, 691 Value: &metricspb.Point_DistributionValue{ 692 DistributionValue: &metricspb.DistributionValue{ 693 Count: 1, 694 Sum: 11.9, 695 SumOfSquaredDeviation: 0, 696 Buckets: []*metricspb.DistributionValue_Bucket{ 697 {}, {Count: 1}, {}, {}, {}, 698 }, 699 BucketOptions: &metricspb.DistributionValue_BucketOptions{ 700 Type: &metricspb.DistributionValue_BucketOptions_Explicit_{ 701 Explicit: &metricspb.DistributionValue_BucketOptions_Explicit{ 702 // With zero bucket in 703 Bounds: []float64{0, 10, 20, 30, 40}, 704 }, 705 }, 706 }, 707 }, 708 }, 709 }, 710 want: &monitoringpb.Point{ 711 Interval: &monitoringpb.TimeInterval{ 712 StartTime: startTimestamp, 713 EndTime: endTimestamp, 714 }, 715 Value: &monitoringpb.TypedValue{ 716 Value: &monitoringpb.TypedValue_DistributionValue{ 717 DistributionValue: &distributionpb.Distribution{ 718 Count: 1, 719 Mean: 11.9, 720 SumOfSquaredDeviation: 0, 721 BucketCounts: []int64{0, 1, 0, 0, 0}, 722 BucketOptions: &distributionpb.Distribution_BucketOptions{ 723 Options: &distributionpb.Distribution_BucketOptions_ExplicitBuckets{ 724 ExplicitBuckets: &distributionpb.Distribution_BucketOptions_Explicit{ 725 Bounds: []float64{0, 10, 20, 30, 40}, 726 }, 727 }, 728 }, 729 }, 730 }, 731 }, 732 }, 733 }, 734 { 735 in: &metricspb.Point{ 736 Timestamp: endTimestamp, 737 Value: &metricspb.Point_DoubleValue{DoubleValue: 50}, 738 }, 739 want: &monitoringpb.Point{ 740 Interval: &monitoringpb.TimeInterval{ 741 StartTime: startTimestamp, 742 EndTime: endTimestamp, 743 }, 744 Value: &monitoringpb.TypedValue{ 745 Value: &monitoringpb.TypedValue_DoubleValue{DoubleValue: 50}, 746 }, 747 }, 748 }, 749 { 750 in: &metricspb.Point{ 751 Timestamp: endTimestamp, 752 Value: &metricspb.Point_Int64Value{Int64Value: 17}, 753 }, 754 want: &monitoringpb.Point{ 755 Interval: &monitoringpb.TimeInterval{ 756 StartTime: startTimestamp, 757 EndTime: endTimestamp, 758 }, 759 Value: &monitoringpb.TypedValue{ 760 Value: &monitoringpb.TypedValue_Int64Value{Int64Value: 17}, 761 }, 762 }, 763 }, 764 } 765 766 for i, tt := range tests { 767 mpt, err := fromProtoPoint(startTimestamp, tt.in) 768 if tt.wantErr != "" { 769 continue 770 } 771 772 if err != nil { 773 t.Errorf("#%d: unexpected error: %v", i, err) 774 continue 775 } 776 777 // Our saving grace is serialization equality since some 778 // unexported fields could be present in the various values. 779 if diff := cmpPoint(mpt, tt.want); diff != "" { 780 t.Fatalf("Test %d failed. Unexpected Point -got +want: %s", i, diff) 781 } 782 } 783} 784 785func TestCombineTimeSeriesAndDeduplication(t *testing.T) { 786 se := new(statsExporter) 787 788 tests := []struct { 789 in []*monitoringpb.TimeSeries 790 want []*monitoringpb.CreateTimeSeriesRequest 791 }{ 792 { 793 in: []*monitoringpb.TimeSeries{ 794 { 795 Metric: &googlemetricpb.Metric{ 796 Type: "a/b/c", 797 Labels: map[string]string{ 798 "k1": "v1", 799 }, 800 }, 801 }, 802 { 803 Metric: &googlemetricpb.Metric{ 804 Type: "a/b/c", 805 Labels: map[string]string{ 806 "k1": "v2", 807 }, 808 }, 809 }, 810 { 811 Metric: &googlemetricpb.Metric{ 812 Type: "A/b/c", 813 }, 814 }, 815 { 816 Metric: &googlemetricpb.Metric{ 817 Type: "a/b/c", 818 Labels: map[string]string{ 819 "k1": "v1", 820 }, 821 }, 822 }, 823 { 824 Metric: &googlemetricpb.Metric{ 825 Type: "X/Y/Z", 826 }, 827 }, 828 }, 829 want: []*monitoringpb.CreateTimeSeriesRequest{ 830 { 831 Name: fmt.Sprintf("projects/%s", se.o.ProjectID), 832 TimeSeries: []*monitoringpb.TimeSeries{ 833 { 834 Metric: &googlemetricpb.Metric{ 835 Type: "a/b/c", 836 Labels: map[string]string{ 837 "k1": "v1", 838 }, 839 }, 840 }, 841 { 842 Metric: &googlemetricpb.Metric{ 843 Type: "a/b/c", 844 Labels: map[string]string{ 845 "k1": "v2", 846 }, 847 }, 848 }, 849 { 850 Metric: &googlemetricpb.Metric{ 851 Type: "A/b/c", 852 }, 853 }, 854 { 855 Metric: &googlemetricpb.Metric{ 856 Type: "X/Y/Z", 857 }, 858 }, 859 }, 860 }, 861 { 862 Name: fmt.Sprintf("projects/%s", se.o.ProjectID), 863 TimeSeries: []*monitoringpb.TimeSeries{ 864 { 865 Metric: &googlemetricpb.Metric{ 866 Type: "a/b/c", 867 Labels: map[string]string{ 868 "k1": "v1", 869 }, 870 }, 871 }, 872 }, 873 }, 874 }, 875 }, 876 } 877 878 for i, tt := range tests { 879 got := se.combineTimeSeriesToCreateTimeSeriesRequest(tt.in) 880 if diff := cmpTSReqs(got, tt.want); diff != "" { 881 t.Fatalf("Test %d failed. Unexpected CreateTimeSeriesRequests -got +want: %s", i, diff) 882 } 883 } 884} 885 886func TestConvertSummaryMetrics(t *testing.T) { 887 startTimestamp := ×tamp.Timestamp{ 888 Seconds: 1543160298, 889 Nanos: 100000090, 890 } 891 endTimestamp := ×tamp.Timestamp{ 892 Seconds: 1543160298, 893 Nanos: 101000090, 894 } 895 896 res := &resourcepb.Resource{ 897 Type: resourcekeys.ContainerType, 898 Labels: map[string]string{ 899 resourcekeys.ContainerKeyName: "container1", 900 resourcekeys.K8SKeyClusterName: "cluster1", 901 }, 902 } 903 904 tests := []struct { 905 in *metricspb.Metric 906 want []*metricspb.Metric 907 statsExporter *statsExporter 908 }{ 909 { 910 in: &metricspb.Metric{ 911 MetricDescriptor: &metricspb.MetricDescriptor{ 912 Name: "summary_metric_descriptor", 913 Description: "This is a test", 914 Unit: "ms", 915 Type: metricspb.MetricDescriptor_SUMMARY, 916 }, 917 Timeseries: []*metricspb.TimeSeries{ 918 { 919 StartTimestamp: startTimestamp, 920 Points: []*metricspb.Point{ 921 { 922 Timestamp: endTimestamp, 923 Value: &metricspb.Point_SummaryValue{ 924 SummaryValue: &metricspb.SummaryValue{ 925 Count: &wrappers.Int64Value{Value: 10}, 926 Sum: &wrappers.DoubleValue{Value: 119.0}, 927 Snapshot: &metricspb.SummaryValue_Snapshot{ 928 PercentileValues: []*metricspb.SummaryValue_Snapshot_ValueAtPercentile{ 929 makePercentileValue(5.6, 10.0), 930 makePercentileValue(9.6, 50.0), 931 makePercentileValue(12.6, 90.0), 932 makePercentileValue(19.6, 99.0), 933 }, 934 }, 935 }, 936 }, 937 }, 938 }, 939 }, 940 }, 941 Resource: res, 942 }, 943 statsExporter: &statsExporter{ 944 o: Options{ProjectID: "foo"}, 945 }, 946 want: []*metricspb.Metric{ 947 { 948 MetricDescriptor: &metricspb.MetricDescriptor{ 949 Name: "summary_metric_descriptor_summary_sum", 950 Description: "This is a test", 951 Unit: "ms", 952 Type: metricspb.MetricDescriptor_CUMULATIVE_DOUBLE, 953 }, 954 Timeseries: []*metricspb.TimeSeries{ 955 makeDoubleTs(119.0, "", startTimestamp, endTimestamp), 956 }, 957 Resource: res, 958 }, 959 { 960 MetricDescriptor: &metricspb.MetricDescriptor{ 961 Name: "summary_metric_descriptor_summary_count", 962 Description: "This is a test", 963 Unit: "1", 964 Type: metricspb.MetricDescriptor_CUMULATIVE_INT64, 965 }, 966 Timeseries: []*metricspb.TimeSeries{ 967 makeInt64Ts(10, "", startTimestamp, endTimestamp), 968 }, 969 Resource: res, 970 }, 971 { 972 MetricDescriptor: &metricspb.MetricDescriptor{ 973 Name: "summary_metric_descriptor_summary_percentile", 974 Description: "This is a test", 975 Unit: "ms", 976 Type: metricspb.MetricDescriptor_GAUGE_DOUBLE, 977 LabelKeys: []*metricspb.LabelKey{ 978 percentileLabelKey, 979 }, 980 }, 981 Timeseries: []*metricspb.TimeSeries{ 982 makeDoubleTs(5.6, "10.000000", nil, endTimestamp), 983 makeDoubleTs(9.6, "50.000000", nil, endTimestamp), 984 makeDoubleTs(12.6, "90.000000", nil, endTimestamp), 985 makeDoubleTs(19.6, "99.000000", nil, endTimestamp), 986 }, 987 Resource: res, 988 }, 989 }, 990 }, 991 } 992 993 for _, tt := range tests { 994 se := tt.statsExporter 995 if se == nil { 996 se = new(statsExporter) 997 } 998 got := se.convertSummaryMetrics(tt.in) 999 if !cmp.Equal(got, tt.want, protocmp.Transform()) { 1000 t.Fatalf("conversion failed:\n got=%v\n want=%v\n", got, tt.want) 1001 } 1002 } 1003} 1004 1005func TestMetricPrefix(t *testing.T) { 1006 tests := []struct { 1007 name string 1008 in string 1009 want string 1010 statsExporter *statsExporter 1011 }{ 1012 { 1013 name: "No prefix and metric name has a kubernetes domain", 1014 in: "kubernetes.io/container/memory/limit_bytes", 1015 statsExporter: &statsExporter{ 1016 o: Options{ProjectID: "foo"}, 1017 }, 1018 want: "kubernetes.io/container/memory/limit_bytes", 1019 }, 1020 { 1021 name: "Has a prefix but prefix doesn't have a domain", 1022 in: "my_metric", 1023 statsExporter: &statsExporter{ 1024 o: Options{ProjectID: "foo", MetricPrefix: "prefix/"}, 1025 }, 1026 want: "custom.googleapis.com/opencensus/prefix/my_metric", 1027 }, 1028 { 1029 name: "Has a prefix without `/` ending but prefix doesn't have a domain", 1030 in: "my_metric", 1031 statsExporter: &statsExporter{ 1032 o: Options{ProjectID: "foo", MetricPrefix: "prefix"}, 1033 }, 1034 want: "custom.googleapis.com/opencensus/prefix/my_metric", 1035 }, 1036 { 1037 name: "Has a prefix and prefix has a domain", 1038 in: "my_metric", 1039 statsExporter: &statsExporter{ 1040 o: Options{ProjectID: "foo", MetricPrefix: "appengine.googleapis.com/"}, 1041 }, 1042 want: "appengine.googleapis.com/my_metric", 1043 }, 1044 { 1045 name: "Has a GetMetricPrefix func but result doesn't have a domain", 1046 in: "my_metric", 1047 statsExporter: &statsExporter{ 1048 o: Options{ 1049 ProjectID: "foo", 1050 GetMetricPrefix: func(name string) string { 1051 return "prefix" 1052 }}, 1053 }, 1054 want: "custom.googleapis.com/opencensus/prefix/my_metric", 1055 }, 1056 { 1057 name: "Has a GetMetricPrefix func and result has a domain", 1058 in: "my_metric", 1059 statsExporter: &statsExporter{ 1060 o: Options{ 1061 ProjectID: "foo", 1062 GetMetricPrefix: func(name string) string { 1063 return "knative.dev/serving" 1064 }}, 1065 }, 1066 want: "knative.dev/serving/my_metric", 1067 }, 1068 { 1069 name: "Has both a prefix and GetMetricPrefix func", 1070 in: "my_metric", 1071 statsExporter: &statsExporter{ 1072 o: Options{ 1073 ProjectID: "foo", 1074 MetricPrefix: "appengine.googleapis.com/", 1075 GetMetricPrefix: func(name string) string { 1076 return "knative.dev/serving" 1077 }}, 1078 }, 1079 want: "knative.dev/serving/my_metric", 1080 }, 1081 } 1082 1083 for _, tt := range tests { 1084 got := tt.statsExporter.metricTypeFromProto(tt.in) 1085 if !cmp.Equal(got, tt.want) { 1086 t.Fatalf("mismatch metric names for test %v:\n got=%v\n want=%v\n", tt.name, got, tt.want) 1087 } 1088 } 1089} 1090 1091func makeInt64Ts(val int64, label string, start, end *timestamp.Timestamp) *metricspb.TimeSeries { 1092 ts := &metricspb.TimeSeries{ 1093 StartTimestamp: start, 1094 Points: makeInt64Point(val, end), 1095 } 1096 if label != "" { 1097 ts.LabelValues = makeLabelValue(label) 1098 } 1099 return ts 1100} 1101 1102func makeInt64Point(val int64, end *timestamp.Timestamp) []*metricspb.Point { 1103 return []*metricspb.Point{ 1104 { 1105 Timestamp: end, 1106 Value: &metricspb.Point_Int64Value{ 1107 Int64Value: val, 1108 }, 1109 }, 1110 } 1111} 1112 1113func makeDoubleTs(val float64, label string, start, end *timestamp.Timestamp) *metricspb.TimeSeries { 1114 ts := &metricspb.TimeSeries{ 1115 StartTimestamp: start, 1116 Points: makeDoublePoint(val, end), 1117 } 1118 if label != "" { 1119 ts.LabelValues = makeLabelValue(label) 1120 } 1121 return ts 1122} 1123 1124func makeDoublePoint(val float64, end *timestamp.Timestamp) []*metricspb.Point { 1125 return []*metricspb.Point{ 1126 { 1127 Timestamp: end, 1128 Value: &metricspb.Point_DoubleValue{ 1129 DoubleValue: val, 1130 }, 1131 }, 1132 } 1133} 1134 1135func makeLabelValue(value string) []*metricspb.LabelValue { 1136 return []*metricspb.LabelValue{ 1137 { 1138 HasValue: true, 1139 Value: value, 1140 }, 1141 } 1142} 1143 1144func makePercentileValue(val, percentile float64) *metricspb.SummaryValue_Snapshot_ValueAtPercentile { 1145 return &metricspb.SummaryValue_Snapshot_ValueAtPercentile{ 1146 Value: val, 1147 Percentile: percentile, 1148 } 1149} 1150 1151func protoMetricToTimeSeries(ctx context.Context, se *statsExporter, mappedRsc *monitoredrespb.MonitoredResource, metric *metricspb.Metric) ([]*monitoringpb.TimeSeries, error) { 1152 mb := newMetricsBatcher(ctx, se.o.ProjectID, se.o.NumberOfWorkers, se.c, defaultTimeout) 1153 se.protoMetricToTimeSeries(ctx, mappedRsc, metric, mb) 1154 return mb.allTss, mb.close(ctx) 1155} 1156