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 := &timestamp.Timestamp{
66		Seconds: 1543160298,
67		Nanos:   100000090,
68	}
69	endTimestamp := &timestamp.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 := &timestamp.Timestamp{
211		Seconds: 1543160298,
212		Nanos:   100000090,
213	}
214	endTimestamp := &timestamp.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 := &timestamp.Timestamp{
401		Seconds: 1543160298,
402		Nanos:   100000090,
403	}
404	endTimestamp := &timestamp.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 := &timestamp.Timestamp{
675		Seconds: 1543160298,
676		Nanos:   100000090,
677	}
678	endTimestamp := &timestamp.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 := &timestamp.Timestamp{
888		Seconds: 1543160298,
889		Nanos:   100000090,
890	}
891	endTimestamp := &timestamp.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