1// Copyright 2017, 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	"testing"
21	"time"
22
23	monitoring "cloud.google.com/go/monitoring/apiv3/v2"
24	"contrib.go.opencensus.io/exporter/stackdriver/monitoredresource"
25
26	"github.com/golang/protobuf/ptypes"
27	"github.com/golang/protobuf/ptypes/timestamp"
28	"github.com/google/go-cmp/cmp"
29	"go.opencensus.io/stats"
30	"go.opencensus.io/stats/view"
31	"go.opencensus.io/tag"
32	"google.golang.org/api/option"
33	"google.golang.org/genproto/googleapis/api/distribution"
34	metricpb "google.golang.org/genproto/googleapis/api/metric"
35	monitoredrespb "google.golang.org/genproto/googleapis/api/monitoredres"
36	monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3"
37	"google.golang.org/grpc"
38	"google.golang.org/protobuf/testing/protocmp"
39)
40
41var authOptions = []option.ClientOption{option.WithGRPCConn(&grpc.ClientConn{})}
42
43var testOptions = Options{ProjectID: "opencensus-test", MonitoringClientOptions: authOptions}
44
45func TestRejectBlankProjectID(t *testing.T) {
46	ids := []string{"", "     ", " "}
47	for _, projectID := range ids {
48		opts := Options{ProjectID: projectID, MonitoringClientOptions: authOptions}
49		exp, err := newStatsExporter(opts)
50		if err == nil || exp != nil {
51			t.Errorf("%q ProjectID must be rejected: NewExporter() = %v err = %q", projectID, exp, err)
52		}
53	}
54}
55
56func TestExporter_makeReq(t *testing.T) {
57	m := stats.Float64("test-measure", "measure desc", "unit")
58
59	key, err := tag.NewKey("test_key")
60	if err != nil {
61		t.Fatal(err)
62	}
63
64	v := &view.View{
65		Name:        "example.com/views/testview",
66		Description: "desc",
67		TagKeys:     []tag.Key{key},
68		Measure:     m,
69		Aggregation: view.Count(),
70	}
71
72	lastValueView := &view.View{
73		Name:        "lasttestview",
74		Description: "desc",
75		TagKeys:     []tag.Key{key},
76		Measure:     m,
77		Aggregation: view.LastValue(),
78	}
79
80	distView := &view.View{
81		Name:        "distview",
82		Description: "desc",
83		Measure:     m,
84		Aggregation: view.Distribution(2, 4, 7),
85	}
86
87	start := time.Now()
88	end := start.Add(time.Minute)
89	count1 := &view.CountData{Value: 10}
90	count2 := &view.CountData{Value: 16}
91	sum1 := &view.SumData{Value: 5.5}
92	sum2 := &view.SumData{Value: -11.1}
93	last1 := view.LastValueData{Value: 100}
94	last2 := view.LastValueData{Value: 200}
95	taskValue := getTaskValue()
96
97	tests := []struct {
98		name   string
99		projID string
100		vd     *view.Data
101		want   []*monitoringpb.CreateTimeSeriesRequest
102		opts   Options
103	}{
104		{
105			name:   "count agg + timeline",
106			projID: "proj-id",
107			vd:     newTestViewData(v, start, end, count1, count2),
108			want: []*monitoringpb.CreateTimeSeriesRequest{
109				{
110					Name: fmt.Sprintf("projects/%s", "proj-id"),
111					TimeSeries: []*monitoringpb.TimeSeries{
112						{
113							Metric: &metricpb.Metric{
114								Type: "custom.googleapis.com/opencensus/example.com/views/testview",
115								Labels: map[string]string{
116									"test_key":        "test-value-1",
117									opencensusTaskKey: taskValue,
118								},
119							},
120							Resource: &monitoredrespb.MonitoredResource{
121								Type: "global",
122							},
123							Points: []*monitoringpb.Point{
124								{
125									Interval: &monitoringpb.TimeInterval{
126										StartTime: &timestamp.Timestamp{
127											Seconds: start.Unix(),
128											Nanos:   int32(start.Nanosecond()),
129										},
130										EndTime: &timestamp.Timestamp{
131											Seconds: end.Unix(),
132											Nanos:   int32(end.Nanosecond()),
133										},
134									},
135									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_Int64Value{
136										Int64Value: 10,
137									}},
138								},
139							},
140						},
141						{
142							Metric: &metricpb.Metric{
143								Type: "custom.googleapis.com/opencensus/example.com/views/testview",
144								Labels: map[string]string{
145									"test_key":        "test-value-2",
146									opencensusTaskKey: taskValue,
147								},
148							},
149							Resource: &monitoredrespb.MonitoredResource{
150								Type: "global",
151							},
152							Points: []*monitoringpb.Point{
153								{
154									Interval: &monitoringpb.TimeInterval{
155										StartTime: &timestamp.Timestamp{
156											Seconds: start.Unix(),
157											Nanos:   int32(start.Nanosecond()),
158										},
159										EndTime: &timestamp.Timestamp{
160											Seconds: end.Unix(),
161											Nanos:   int32(end.Nanosecond()),
162										},
163									},
164									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_Int64Value{
165										Int64Value: 16,
166									}},
167								},
168							},
169						},
170					},
171				},
172			},
173		},
174		{
175			name:   "metric type formatter",
176			projID: "proj-id",
177			vd:     newTestViewData(v, start, end, sum1, sum2),
178			opts: Options{
179				GetMetricType: func(v *view.View) string {
180					return fmt.Sprintf("external.googleapis.com/%s", v.Name)
181				},
182			},
183			want: []*monitoringpb.CreateTimeSeriesRequest{
184				{
185					Name: fmt.Sprintf("projects/%s", "proj-id"),
186					TimeSeries: []*monitoringpb.TimeSeries{
187						{
188							Metric: &metricpb.Metric{
189								Type: "external.googleapis.com/example.com/views/testview",
190								Labels: map[string]string{
191									"test_key":        "test-value-1",
192									opencensusTaskKey: taskValue,
193								},
194							},
195							Resource: &monitoredrespb.MonitoredResource{
196								Type: "global",
197							},
198							Points: []*monitoringpb.Point{
199								{
200									Interval: &monitoringpb.TimeInterval{
201										StartTime: &timestamp.Timestamp{
202											Seconds: start.Unix(),
203											Nanos:   int32(start.Nanosecond()),
204										},
205										EndTime: &timestamp.Timestamp{
206											Seconds: end.Unix(),
207											Nanos:   int32(end.Nanosecond()),
208										},
209									},
210									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_DoubleValue{
211										DoubleValue: 5.5,
212									}},
213								},
214							},
215						},
216						{
217							Metric: &metricpb.Metric{
218								Type: "external.googleapis.com/example.com/views/testview",
219								Labels: map[string]string{
220									"test_key":        "test-value-2",
221									opencensusTaskKey: taskValue,
222								},
223							},
224							Resource: &monitoredrespb.MonitoredResource{
225								Type: "global",
226							},
227							Points: []*monitoringpb.Point{
228								{
229									Interval: &monitoringpb.TimeInterval{
230										StartTime: &timestamp.Timestamp{
231											Seconds: start.Unix(),
232											Nanos:   int32(start.Nanosecond()),
233										},
234										EndTime: &timestamp.Timestamp{
235											Seconds: end.Unix(),
236											Nanos:   int32(end.Nanosecond()),
237										},
238									},
239									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_DoubleValue{
240										DoubleValue: -11.1,
241									}},
242								},
243							},
244						},
245					},
246				},
247			},
248		},
249		{
250			name:   "sum agg + timeline",
251			projID: "proj-id",
252			vd:     newTestViewData(v, start, end, sum1, sum2),
253			want: []*monitoringpb.CreateTimeSeriesRequest{
254				{
255					Name: fmt.Sprintf("projects/%s", "proj-id"),
256					TimeSeries: []*monitoringpb.TimeSeries{
257						{
258							Metric: &metricpb.Metric{
259								Type: "custom.googleapis.com/opencensus/example.com/views/testview",
260								Labels: map[string]string{
261									"test_key":        "test-value-1",
262									opencensusTaskKey: taskValue,
263								},
264							},
265							Resource: &monitoredrespb.MonitoredResource{
266								Type: "global",
267							},
268							Points: []*monitoringpb.Point{
269								{
270									Interval: &monitoringpb.TimeInterval{
271										StartTime: &timestamp.Timestamp{
272											Seconds: start.Unix(),
273											Nanos:   int32(start.Nanosecond()),
274										},
275										EndTime: &timestamp.Timestamp{
276											Seconds: end.Unix(),
277											Nanos:   int32(end.Nanosecond()),
278										},
279									},
280									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_DoubleValue{
281										DoubleValue: 5.5,
282									}},
283								},
284							},
285						},
286						{
287							Metric: &metricpb.Metric{
288								Type: "custom.googleapis.com/opencensus/example.com/views/testview",
289								Labels: map[string]string{
290									"test_key":        "test-value-2",
291									opencensusTaskKey: taskValue,
292								},
293							},
294							Resource: &monitoredrespb.MonitoredResource{
295								Type: "global",
296							},
297							Points: []*monitoringpb.Point{
298								{
299									Interval: &monitoringpb.TimeInterval{
300										StartTime: &timestamp.Timestamp{
301											Seconds: start.Unix(),
302											Nanos:   int32(start.Nanosecond()),
303										},
304										EndTime: &timestamp.Timestamp{
305											Seconds: end.Unix(),
306											Nanos:   int32(end.Nanosecond()),
307										},
308									},
309									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_DoubleValue{
310										DoubleValue: -11.1,
311									}},
312								},
313							},
314						},
315					},
316				},
317			},
318		},
319		{
320			name:   "last value agg",
321			projID: "proj-id",
322			vd:     newTestViewData(lastValueView, start, end, &last1, &last2),
323			want: []*monitoringpb.CreateTimeSeriesRequest{
324				{
325					Name: fmt.Sprintf("projects/%s", "proj-id"),
326					TimeSeries: []*monitoringpb.TimeSeries{
327						{
328							Metric: &metricpb.Metric{
329								Type: "custom.googleapis.com/opencensus/lasttestview",
330								Labels: map[string]string{
331									"test_key":        "test-value-1",
332									opencensusTaskKey: taskValue,
333								},
334							},
335							Resource: &monitoredrespb.MonitoredResource{
336								Type: "global",
337							},
338							Points: []*monitoringpb.Point{
339								{
340									Interval: &monitoringpb.TimeInterval{
341										EndTime: &timestamp.Timestamp{
342											Seconds: end.Unix(),
343											Nanos:   int32(end.Nanosecond()),
344										},
345									},
346									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_DoubleValue{
347										DoubleValue: 100,
348									}},
349								},
350							},
351						},
352						{
353							Metric: &metricpb.Metric{
354								Type: "custom.googleapis.com/opencensus/lasttestview",
355								Labels: map[string]string{
356									"test_key":        "test-value-2",
357									opencensusTaskKey: taskValue,
358								},
359							},
360							Resource: &monitoredrespb.MonitoredResource{
361								Type: "global",
362							},
363							Points: []*monitoringpb.Point{
364								{
365									Interval: &monitoringpb.TimeInterval{
366										EndTime: &timestamp.Timestamp{
367											Seconds: end.Unix(),
368											Nanos:   int32(end.Nanosecond()),
369										},
370									},
371									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_DoubleValue{
372										DoubleValue: 200,
373									}},
374								},
375							},
376						},
377					},
378				},
379			},
380		},
381		{
382			name:   "dist agg + time window - without zero bucket",
383			projID: "proj-id",
384			vd:     newTestDistViewData(distView, start, end),
385			want: []*monitoringpb.CreateTimeSeriesRequest{{
386				Name: fmt.Sprintf("projects/%s", "proj-id"),
387				TimeSeries: []*monitoringpb.TimeSeries{
388					{
389						Metric: &metricpb.Metric{
390							Type: "custom.googleapis.com/opencensus/distview",
391							Labels: map[string]string{
392								opencensusTaskKey: taskValue,
393							},
394						},
395						Resource: &monitoredrespb.MonitoredResource{
396							Type: "global",
397						},
398						Points: []*monitoringpb.Point{
399							{
400								Interval: &monitoringpb.TimeInterval{
401									StartTime: &timestamp.Timestamp{
402										Seconds: start.Unix(),
403										Nanos:   int32(start.Nanosecond()),
404									},
405									EndTime: &timestamp.Timestamp{
406										Seconds: end.Unix(),
407										Nanos:   int32(end.Nanosecond()),
408									},
409								},
410								Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_DistributionValue{
411									DistributionValue: &distribution.Distribution{
412										Count:                 5,
413										Mean:                  3.0,
414										SumOfSquaredDeviation: 1.5,
415										BucketOptions: &distribution.Distribution_BucketOptions{
416											Options: &distribution.Distribution_BucketOptions_ExplicitBuckets{
417												ExplicitBuckets: &distribution.Distribution_BucketOptions_Explicit{
418													Bounds: []float64{0.0, 2.0, 4.0, 7.0}}}},
419										BucketCounts: []int64{0, 2, 2, 1}},
420								}},
421							},
422						},
423					},
424				},
425			}},
426		},
427		{
428			name:   "dist agg + time window + zero bucket",
429			projID: "proj-id",
430			vd:     newTestDistViewData(distView, start, end),
431			want: []*monitoringpb.CreateTimeSeriesRequest{{
432				Name: fmt.Sprintf("projects/%s", "proj-id"),
433				TimeSeries: []*monitoringpb.TimeSeries{
434					{
435						Metric: &metricpb.Metric{
436							Type: "custom.googleapis.com/opencensus/distview",
437							Labels: map[string]string{
438								opencensusTaskKey: taskValue,
439							},
440						},
441						Resource: &monitoredrespb.MonitoredResource{
442							Type: "global",
443						},
444						Points: []*monitoringpb.Point{
445							{
446								Interval: &monitoringpb.TimeInterval{
447									StartTime: &timestamp.Timestamp{
448										Seconds: start.Unix(),
449										Nanos:   int32(start.Nanosecond()),
450									},
451									EndTime: &timestamp.Timestamp{
452										Seconds: end.Unix(),
453										Nanos:   int32(end.Nanosecond()),
454									},
455								},
456								Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_DistributionValue{
457									DistributionValue: &distribution.Distribution{
458										Count:                 5,
459										Mean:                  3.0,
460										SumOfSquaredDeviation: 1.5,
461										BucketOptions: &distribution.Distribution_BucketOptions{
462											Options: &distribution.Distribution_BucketOptions_ExplicitBuckets{
463												ExplicitBuckets: &distribution.Distribution_BucketOptions_Explicit{
464													Bounds: []float64{0.0, 2.0, 4.0, 7.0}}}},
465										BucketCounts: []int64{0, 2, 2, 1}},
466								}},
467							},
468						},
469					},
470				},
471			}},
472		},
473	}
474
475	for _, tt := range tests {
476		t.Run(tt.name, func(t *testing.T) {
477			opts := tt.opts
478			opts.ProjectID = tt.projID
479			opts.MonitoringClientOptions = authOptions
480			e, err := newStatsExporter(opts)
481			if err != nil {
482				t.Fatal(err)
483			}
484			resps := e.makeReq([]*view.Data{tt.vd}, maxTimeSeriesPerUpload)
485			if got, want := len(resps), len(tt.want); got != want {
486				t.Fatalf("%v: Exporter.makeReq() returned %d responses; want %d", tt.name, got, want)
487			}
488			if len(tt.want) == 0 {
489				return
490			}
491			if diff := cmp.Diff(resps, tt.want, protocmp.Transform()); diff != "" {
492				t.Errorf("Values differ -got +want: %s", diff)
493			}
494		})
495	}
496}
497
498func TestTimeIntervalStaggering(t *testing.T) {
499	now := time.Now()
500
501	interval := toValidTimeIntervalpb(now, now)
502
503	start, err := ptypes.Timestamp(interval.StartTime)
504	if err != nil {
505		t.Fatalf("unable to convert start time from PB: %v", err)
506	}
507
508	end, err := ptypes.Timestamp(interval.EndTime)
509	if err != nil {
510		t.Fatalf("unable to convert end time to PB: %v", err)
511	}
512
513	if end.Before(start.Add(time.Millisecond)) {
514		t.Fatalf("expected end=%v to be at least %v after start=%v, but it wasn't", end, time.Millisecond, start)
515	}
516}
517
518func TestExporter_makeReq_batching(t *testing.T) {
519	m := stats.Float64("test-measure/makeReq_batching", "measure desc", "unit")
520
521	key, err := tag.NewKey("test_key")
522	if err != nil {
523		t.Fatal(err)
524	}
525
526	v := &view.View{
527		Name:        "view",
528		Description: "desc",
529		TagKeys:     []tag.Key{key},
530		Measure:     m,
531		Aggregation: view.Count(),
532	}
533
534	tests := []struct {
535		name      string
536		iter      int
537		limit     int
538		wantReqs  int
539		wantTotal int
540	}{
541		{
542			name:      "4 vds; 3 limit",
543			iter:      2,
544			limit:     3,
545			wantReqs:  3,
546			wantTotal: 4,
547		},
548		{
549			name:      "4 vds; 4 limit",
550			iter:      2,
551			limit:     4,
552			wantReqs:  2,
553			wantTotal: 4,
554		},
555		{
556			name:      "4 vds; 5 limit",
557			iter:      2,
558			limit:     5,
559			wantReqs:  2,
560			wantTotal: 4,
561		},
562	}
563
564	count1 := &view.CountData{Value: 10}
565	count2 := &view.CountData{Value: 16}
566
567	for _, tt := range tests {
568		var vds []*view.Data
569		for i := 0; i < tt.iter; i++ {
570			vds = append(vds, newTestViewData(v, time.Now(), time.Now(), count1, count2))
571		}
572
573		e, err := newStatsExporter(testOptions)
574		if err != nil {
575			t.Fatal(err)
576		}
577		resps := e.makeReq(vds, tt.limit)
578		if len(resps) != tt.wantReqs {
579			t.Errorf("%v:\ngot %d:: %v;\n\nwant %d requests\n\n", tt.name, len(resps), resps, tt.wantReqs)
580		}
581
582		var total int
583		for _, resp := range resps {
584			total += len(resp.TimeSeries)
585		}
586		if got, want := total, tt.wantTotal; got != want {
587			t.Errorf("%v: len(resps[...].TimeSeries) = %d; want %d", tt.name, got, want)
588		}
589	}
590}
591
592func TestExporter_createMetricDescriptorFromView(t *testing.T) {
593	oldCreateMetricDescriptor := createMetricDescriptor
594
595	defer func() {
596		createMetricDescriptor = oldCreateMetricDescriptor
597	}()
598
599	key, _ := tag.NewKey("test-key-one")
600	m := stats.Float64("test-measure/TestExporter_createMetricDescriptorFromView", "measure desc", stats.UnitMilliseconds)
601
602	v := &view.View{
603		Name:        "test_view_sum",
604		Description: "view_description",
605		TagKeys:     []tag.Key{key},
606		Measure:     m,
607		Aggregation: view.Sum(),
608	}
609
610	data := &view.CountData{Value: 0}
611	vd := newTestViewData(v, time.Now(), time.Now(), data, data)
612
613	var customLabels Labels
614	customLabels.Set("pid", "1234", "Local process identifier")
615	customLabels.Set("hostname", "test.example.com", "Local hostname")
616	customLabels.Set("a/b/c/host-name", "test.example.com", "Local hostname")
617
618	tests := []struct {
619		name string
620		opts Options
621	}{
622		{
623			name: "default",
624		},
625		{
626			name: "no default labels",
627			opts: Options{DefaultMonitoringLabels: &Labels{}},
628		},
629		{
630			name: "custom default labels",
631			opts: Options{DefaultMonitoringLabels: &customLabels},
632		},
633	}
634
635	for _, tt := range tests {
636		t.Run(tt.name, func(t *testing.T) {
637			opts := tt.opts
638			opts.MonitoringClientOptions = authOptions
639			opts.ProjectID = "test_project"
640			e, err := newStatsExporter(opts)
641			if err != nil {
642				t.Fatal(err)
643			}
644
645			var createCalls int
646			createMetricDescriptor = func(ctx context.Context, c *monitoring.MetricClient, mdr *monitoringpb.CreateMetricDescriptorRequest) (*metricpb.MetricDescriptor, error) {
647				createCalls++
648				if got, want := mdr.MetricDescriptor.Name, "projects/test_project/metricDescriptors/custom.googleapis.com/opencensus/test_view_sum"; got != want {
649					t.Errorf("MetricDescriptor.Name = %q; want %q", got, want)
650				}
651				if got, want := mdr.MetricDescriptor.Type, "custom.googleapis.com/opencensus/test_view_sum"; got != want {
652					t.Errorf("MetricDescriptor.Type = %q; want %q", got, want)
653				}
654				if got, want := mdr.MetricDescriptor.ValueType, metricpb.MetricDescriptor_DOUBLE; got != want {
655					t.Errorf("MetricDescriptor.ValueType = %q; want %q", got, want)
656				}
657				if got, want := mdr.MetricDescriptor.MetricKind, metricpb.MetricDescriptor_CUMULATIVE; got != want {
658					t.Errorf("MetricDescriptor.MetricKind = %q; want %q", got, want)
659				}
660				if got, want := mdr.MetricDescriptor.Description, "view_description"; got != want {
661					t.Errorf("MetricDescriptor.Description = %q; want %q", got, want)
662				}
663				if got, want := mdr.MetricDescriptor.DisplayName, "OpenCensus/test_view_sum"; got != want {
664					t.Errorf("MetricDescriptor.DisplayName = %q; want %q", got, want)
665				}
666				if got, want := mdr.MetricDescriptor.Unit, stats.UnitMilliseconds; got != want {
667					t.Errorf("MetricDescriptor.Unit = %q; want %q", got, want)
668				}
669				return &metricpb.MetricDescriptor{
670					DisplayName: "OpenCensus/test_view_sum",
671					Description: "view_description",
672					Unit:        stats.UnitMilliseconds,
673					Type:        "custom.googleapis.com/opencensus/test_view_sum",
674					MetricKind:  metricpb.MetricDescriptor_CUMULATIVE,
675					ValueType:   metricpb.MetricDescriptor_DOUBLE,
676					Labels:      newLabelDescriptors(e.defaultLabels, vd.View.TagKeys),
677				}, nil
678			}
679
680			ctx := context.Background()
681			if err := e.createMetricDescriptorFromView(ctx, vd.View); err != nil {
682				t.Errorf("Exporter.createMetricDescriptorFromView() error = %v", err)
683			}
684			if err := e.createMetricDescriptorFromView(ctx, vd.View); err != nil {
685				t.Errorf("Exporter.createMetricDescriptorFromView() error = %v", err)
686			}
687			if count := createCalls; count != 1 {
688				t.Errorf("createMetricDescriptor needs to be called for once; called %v times", count)
689			}
690			if count := len(e.metricDescriptors); count != 1 {
691				t.Errorf("len(e.metricDescriptors) = %v; want 1", count)
692			}
693		})
694	}
695}
696
697func TestExporter_createMetricDescriptorFromView_CountAggregation(t *testing.T) {
698	oldCreateMetricDescriptor := createMetricDescriptor
699
700	defer func() {
701		createMetricDescriptor = oldCreateMetricDescriptor
702	}()
703
704	key, _ := tag.NewKey("test-key-one")
705	m := stats.Float64("test-measure/TestExporter_createMetricDescriptorFromView", "measure desc", stats.UnitMilliseconds)
706
707	v := &view.View{
708		Name:        "test_view_count",
709		Description: "view_description",
710		TagKeys:     []tag.Key{key},
711		Measure:     m,
712		Aggregation: view.Count(),
713	}
714
715	data := &view.CountData{Value: 0}
716	vd := newTestViewData(v, time.Now(), time.Now(), data, data)
717
718	e := &statsExporter{
719		metricDescriptors: make(map[string]bool),
720		o:                 Options{ProjectID: "test_project"},
721	}
722
723	createMetricDescriptor = func(ctx context.Context, c *monitoring.MetricClient, mdr *monitoringpb.CreateMetricDescriptorRequest) (*metricpb.MetricDescriptor, error) {
724		if got, want := mdr.MetricDescriptor.Name, "projects/test_project/metricDescriptors/custom.googleapis.com/opencensus/test_view_count"; got != want {
725			t.Errorf("MetricDescriptor.Name = %q; want %q", got, want)
726		}
727		if got, want := mdr.MetricDescriptor.Type, "custom.googleapis.com/opencensus/test_view_count"; got != want {
728			t.Errorf("MetricDescriptor.Type = %q; want %q", got, want)
729		}
730		if got, want := mdr.MetricDescriptor.ValueType, metricpb.MetricDescriptor_INT64; got != want {
731			t.Errorf("MetricDescriptor.ValueType = %q; want %q", got, want)
732		}
733		if got, want := mdr.MetricDescriptor.MetricKind, metricpb.MetricDescriptor_CUMULATIVE; got != want {
734			t.Errorf("MetricDescriptor.MetricKind = %q; want %q", got, want)
735		}
736		if got, want := mdr.MetricDescriptor.Description, "view_description"; got != want {
737			t.Errorf("MetricDescriptor.Description = %q; want %q", got, want)
738		}
739		if got, want := mdr.MetricDescriptor.DisplayName, "OpenCensus/test_view_count"; got != want {
740			t.Errorf("MetricDescriptor.DisplayName = %q; want %q", got, want)
741		}
742		if got, want := mdr.MetricDescriptor.Unit, stats.UnitDimensionless; got != want {
743			t.Errorf("MetricDescriptor.Unit = %q; want %q", got, want)
744		}
745		return &metricpb.MetricDescriptor{
746			DisplayName: "OpenCensus/test_view_sum",
747			Description: "view_description",
748			Unit:        stats.UnitDimensionless,
749			Type:        "custom.googleapis.com/opencensus/test_view_count",
750			MetricKind:  metricpb.MetricDescriptor_CUMULATIVE,
751			ValueType:   metricpb.MetricDescriptor_INT64,
752			Labels:      newLabelDescriptors(nil, vd.View.TagKeys),
753		}, nil
754	}
755	ctx := context.Background()
756	if err := e.createMetricDescriptorFromView(ctx, vd.View); err != nil {
757		t.Errorf("Exporter.createMetricDescriptorFromView() error = %v", err)
758	}
759}
760
761func TestExporter_makeReq_withCustomMonitoredResource(t *testing.T) {
762	m := stats.Float64("test-measure/TestExporter_makeReq_withCustomMonitoredResource", "measure desc", "unit")
763
764	key, err := tag.NewKey("test_key")
765	if err != nil {
766		t.Fatal(err)
767	}
768
769	v := &view.View{
770		Name:        "testview",
771		Description: "desc",
772		TagKeys:     []tag.Key{key},
773		Measure:     m,
774		Aggregation: view.Count(),
775	}
776	if err := view.Register(v); err != nil {
777		t.Fatal(err)
778	}
779	defer view.Unregister(v)
780
781	start := time.Now()
782	end := start.Add(time.Minute)
783	count1 := &view.CountData{Value: 10}
784	count2 := &view.CountData{Value: 16}
785	taskValue := getTaskValue()
786
787	resource := &monitoredrespb.MonitoredResource{
788		Type: "gce_instance",
789		Labels: map[string]string{
790			"project_id":  "proj-id",
791			"instance_id": "instance",
792			"zone":        "us-west-1a",
793		},
794	}
795
796	gceInst := &monitoredresource.GCEInstance{
797		ProjectID:  "proj-id",
798		InstanceID: "instance",
799		Zone:       "us-west-1a",
800	}
801
802	tests := []struct {
803		name string
804		opts Options
805		vd   *view.Data
806		want []*monitoringpb.CreateTimeSeriesRequest
807	}{
808		{
809			name: "count agg timeline",
810			opts: Options{Resource: resource},
811			vd:   newTestViewData(v, start, end, count1, count2),
812			want: []*monitoringpb.CreateTimeSeriesRequest{
813				{
814					Name: fmt.Sprintf("projects/%s", "proj-id"),
815					TimeSeries: []*monitoringpb.TimeSeries{
816						{
817							Metric: &metricpb.Metric{
818								Type: "custom.googleapis.com/opencensus/testview",
819								Labels: map[string]string{
820									"test_key":        "test-value-1",
821									opencensusTaskKey: taskValue,
822								},
823							},
824							Resource: resource,
825							Points: []*monitoringpb.Point{
826								{
827									Interval: &monitoringpb.TimeInterval{
828										StartTime: &timestamp.Timestamp{
829											Seconds: start.Unix(),
830											Nanos:   int32(start.Nanosecond()),
831										},
832										EndTime: &timestamp.Timestamp{
833											Seconds: end.Unix(),
834											Nanos:   int32(end.Nanosecond()),
835										},
836									},
837									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_Int64Value{
838										Int64Value: 10,
839									}},
840								},
841							},
842						},
843						{
844							Metric: &metricpb.Metric{
845								Type: "custom.googleapis.com/opencensus/testview",
846								Labels: map[string]string{
847									"test_key":        "test-value-2",
848									opencensusTaskKey: taskValue,
849								},
850							},
851							Resource: resource,
852							Points: []*monitoringpb.Point{
853								{
854									Interval: &monitoringpb.TimeInterval{
855										StartTime: &timestamp.Timestamp{
856											Seconds: start.Unix(),
857											Nanos:   int32(start.Nanosecond()),
858										},
859										EndTime: &timestamp.Timestamp{
860											Seconds: end.Unix(),
861											Nanos:   int32(end.Nanosecond()),
862										},
863									},
864									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_Int64Value{
865										Int64Value: 16,
866									}},
867								},
868							},
869						},
870					},
871				},
872			},
873		},
874		{
875			name: "with MonitoredResource and labels",
876			opts: func() Options {
877				var labels Labels
878				labels.Set("pid", "1234", "Process identifier")
879				return Options{
880					MonitoredResource:       gceInst,
881					DefaultMonitoringLabels: &labels,
882				}
883			}(),
884			vd: newTestViewData(v, start, end, count1, count2),
885			want: []*monitoringpb.CreateTimeSeriesRequest{
886				{
887					Name: fmt.Sprintf("projects/%s", "proj-id"),
888					TimeSeries: []*monitoringpb.TimeSeries{
889						{
890							Metric: &metricpb.Metric{
891								Type: "custom.googleapis.com/opencensus/testview",
892								Labels: map[string]string{
893									"test_key": "test-value-1",
894									"pid":      "1234",
895								},
896							},
897							Resource: resource,
898							Points: []*monitoringpb.Point{
899								{
900									Interval: &monitoringpb.TimeInterval{
901										StartTime: &timestamp.Timestamp{
902											Seconds: start.Unix(),
903											Nanos:   int32(start.Nanosecond()),
904										},
905										EndTime: &timestamp.Timestamp{
906											Seconds: end.Unix(),
907											Nanos:   int32(end.Nanosecond()),
908										},
909									},
910									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_Int64Value{
911										Int64Value: 10,
912									}},
913								},
914							},
915						},
916						{
917							Metric: &metricpb.Metric{
918								Type: "custom.googleapis.com/opencensus/testview",
919								Labels: map[string]string{
920									"test_key": "test-value-2",
921									"pid":      "1234",
922								},
923							},
924							Resource: resource,
925							Points: []*monitoringpb.Point{
926								{
927									Interval: &monitoringpb.TimeInterval{
928										StartTime: &timestamp.Timestamp{
929											Seconds: start.Unix(),
930											Nanos:   int32(start.Nanosecond()),
931										},
932										EndTime: &timestamp.Timestamp{
933											Seconds: end.Unix(),
934											Nanos:   int32(end.Nanosecond()),
935										},
936									},
937									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_Int64Value{
938										Int64Value: 16,
939									}},
940								},
941							},
942						},
943					},
944				},
945			},
946		},
947		{
948			name: "custom default monitoring labels",
949			opts: func() Options {
950				var labels Labels
951				labels.Set("pid", "1234", "Process identifier")
952				return Options{
953					Resource:                resource,
954					DefaultMonitoringLabels: &labels,
955				}
956			}(),
957			vd: newTestViewData(v, start, end, count1, count2),
958			want: []*monitoringpb.CreateTimeSeriesRequest{
959				{
960					Name: fmt.Sprintf("projects/%s", "proj-id"),
961					TimeSeries: []*monitoringpb.TimeSeries{
962						{
963							Metric: &metricpb.Metric{
964								Type: "custom.googleapis.com/opencensus/testview",
965								Labels: map[string]string{
966									"test_key": "test-value-1",
967									"pid":      "1234",
968								},
969							},
970							Resource: resource,
971							Points: []*monitoringpb.Point{
972								{
973									Interval: &monitoringpb.TimeInterval{
974										StartTime: &timestamp.Timestamp{
975											Seconds: start.Unix(),
976											Nanos:   int32(start.Nanosecond()),
977										},
978										EndTime: &timestamp.Timestamp{
979											Seconds: end.Unix(),
980											Nanos:   int32(end.Nanosecond()),
981										},
982									},
983									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_Int64Value{
984										Int64Value: 10,
985									}},
986								},
987							},
988						},
989						{
990							Metric: &metricpb.Metric{
991								Type: "custom.googleapis.com/opencensus/testview",
992								Labels: map[string]string{
993									"test_key": "test-value-2",
994									"pid":      "1234",
995								},
996							},
997							Resource: resource,
998							Points: []*monitoringpb.Point{
999								{
1000									Interval: &monitoringpb.TimeInterval{
1001										StartTime: &timestamp.Timestamp{
1002											Seconds: start.Unix(),
1003											Nanos:   int32(start.Nanosecond()),
1004										},
1005										EndTime: &timestamp.Timestamp{
1006											Seconds: end.Unix(),
1007											Nanos:   int32(end.Nanosecond()),
1008										},
1009									},
1010									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_Int64Value{
1011										Int64Value: 16,
1012									}},
1013								},
1014							},
1015						},
1016					},
1017				},
1018			},
1019		},
1020		{
1021			name: "count agg timeline",
1022			opts: Options{Resource: resource},
1023			vd:   newTestViewData(v, start, end, count1, count2),
1024			want: []*monitoringpb.CreateTimeSeriesRequest{
1025				{
1026					Name: fmt.Sprintf("projects/%s", "proj-id"),
1027					TimeSeries: []*monitoringpb.TimeSeries{
1028						{
1029							Metric: &metricpb.Metric{
1030								Type: "custom.googleapis.com/opencensus/testview",
1031								Labels: map[string]string{
1032									"test_key":        "test-value-1",
1033									opencensusTaskKey: taskValue,
1034								},
1035							},
1036							Resource: resource,
1037							Points: []*monitoringpb.Point{
1038								{
1039									Interval: &monitoringpb.TimeInterval{
1040										StartTime: &timestamp.Timestamp{
1041											Seconds: start.Unix(),
1042											Nanos:   int32(start.Nanosecond()),
1043										},
1044										EndTime: &timestamp.Timestamp{
1045											Seconds: end.Unix(),
1046											Nanos:   int32(end.Nanosecond()),
1047										},
1048									},
1049									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_Int64Value{
1050										Int64Value: 10,
1051									}},
1052								},
1053							},
1054						},
1055						{
1056							Metric: &metricpb.Metric{
1057								Type: "custom.googleapis.com/opencensus/testview",
1058								Labels: map[string]string{
1059									"test_key":        "test-value-2",
1060									opencensusTaskKey: taskValue,
1061								},
1062							},
1063							Resource: resource,
1064							Points: []*monitoringpb.Point{
1065								{
1066									Interval: &monitoringpb.TimeInterval{
1067										StartTime: &timestamp.Timestamp{
1068											Seconds: start.Unix(),
1069											Nanos:   int32(start.Nanosecond()),
1070										},
1071										EndTime: &timestamp.Timestamp{
1072											Seconds: end.Unix(),
1073											Nanos:   int32(end.Nanosecond()),
1074										},
1075									},
1076									Value: &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_Int64Value{
1077										Int64Value: 16,
1078									}},
1079								},
1080							},
1081						},
1082					},
1083				},
1084			},
1085		},
1086	}
1087	for _, tt := range tests {
1088		t.Run(tt.name, func(t *testing.T) {
1089			opts := tt.opts
1090			opts.MonitoringClientOptions = authOptions
1091			opts.ProjectID = "proj-id"
1092			e, err := NewExporter(opts)
1093			if err != nil {
1094				t.Fatal(err)
1095			}
1096			resps := e.statsExporter.makeReq([]*view.Data{tt.vd}, maxTimeSeriesPerUpload)
1097			if got, want := len(resps), len(tt.want); got != want {
1098				t.Fatalf("%v: Exporter.makeReq() returned %d responses; want %d", tt.name, got, want)
1099			}
1100			if len(tt.want) == 0 {
1101				return
1102			}
1103			if diff := cmp.Diff(resps, tt.want, protocmp.Transform()); diff != "" {
1104				t.Errorf("Requests differ, -got +want: %s", diff)
1105			}
1106		})
1107	}
1108}
1109
1110func TestExporter_customContext(t *testing.T) {
1111	oldCreateMetricDescriptor := createMetricDescriptor
1112	oldCreateTimeSeries := createTimeSeries
1113
1114	defer func() {
1115		createMetricDescriptor = oldCreateMetricDescriptor
1116		createTimeSeries = oldCreateTimeSeries
1117	}()
1118
1119	var timedOut = 0
1120	createMetricDescriptor = func(ctx context.Context, c *monitoring.MetricClient, mdr *monitoringpb.CreateMetricDescriptorRequest) (*metricpb.MetricDescriptor, error) {
1121		select {
1122		case <-time.After(1 * time.Second):
1123			fmt.Println("createMetricDescriptor did not time out")
1124		case <-ctx.Done():
1125			timedOut++
1126		}
1127		return &metricpb.MetricDescriptor{}, nil
1128	}
1129	createTimeSeries = func(ctx context.Context, c *monitoring.MetricClient, ts *monitoringpb.CreateTimeSeriesRequest) error {
1130		select {
1131		case <-time.After(1 * time.Second):
1132			fmt.Println("createTimeSeries did not time out")
1133		case <-ctx.Done():
1134			timedOut++
1135		}
1136		return nil
1137	}
1138
1139	v := &view.View{
1140		Name:        "test_view_count",
1141		Description: "view_description",
1142		Measure:     stats.Float64("test-measure/TestExporter_createMetricDescriptorFromView", "measure desc", stats.UnitMilliseconds),
1143		Aggregation: view.Count(),
1144	}
1145
1146	data := &view.CountData{Value: 0}
1147	vd := newTestViewData(v, time.Now(), time.Now(), data, data)
1148
1149	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
1150	defer cancel()
1151	e := &statsExporter{
1152		metricDescriptors: make(map[string]bool),
1153		o:                 Options{ProjectID: "test_project", Context: ctx},
1154	}
1155	if err := e.uploadStats([]*view.Data{vd}); err != nil {
1156		t.Errorf("Exporter.uploadStats() error = %v", err)
1157	}
1158	if ctx.Err() != context.DeadlineExceeded {
1159		t.Errorf("expected context to time out; got %v", ctx.Err())
1160	}
1161	if timedOut != 2 {
1162		t.Errorf("expected two functions to time out; got %d", timedOut)
1163	}
1164}
1165
1166func newTestViewData(v *view.View, start, end time.Time, data1, data2 view.AggregationData) *view.Data {
1167	key, _ := tag.NewKey("test-key")
1168	tag1 := tag.Tag{Key: key, Value: "test-value-1"}
1169	tag2 := tag.Tag{Key: key, Value: "test-value-2"}
1170	return &view.Data{
1171		View: v,
1172		Rows: []*view.Row{
1173			{
1174				Tags: []tag.Tag{tag1},
1175				Data: data1,
1176			},
1177			{
1178				Tags: []tag.Tag{tag2},
1179				Data: data2,
1180			},
1181		},
1182		Start: start,
1183		End:   end,
1184	}
1185}
1186
1187func newTestDistViewData(v *view.View, start, end time.Time) *view.Data {
1188	return &view.Data{
1189		View: v,
1190		Rows: []*view.Row{
1191			{Data: &view.DistributionData{
1192				Count:           5,
1193				Min:             1,
1194				Max:             7,
1195				Mean:            3,
1196				SumOfSquaredDev: 1.5,
1197				CountPerBucket:  []int64{2, 2, 1},
1198			}},
1199		},
1200		Start: start,
1201		End:   end,
1202	}
1203}
1204