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 ocagent
16
17import (
18	"errors"
19	"time"
20
21	"go.opencensus.io/stats"
22	"go.opencensus.io/stats/view"
23	"go.opencensus.io/tag"
24
25	"github.com/golang/protobuf/ptypes/timestamp"
26
27	metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1"
28)
29
30var (
31	errNilMeasure  = errors.New("expecting a non-nil stats.Measure")
32	errNilView     = errors.New("expecting a non-nil view.View")
33	errNilViewData = errors.New("expecting a non-nil view.Data")
34)
35
36func viewDataToMetric(vd *view.Data) (*metricspb.Metric, error) {
37	if vd == nil {
38		return nil, errNilViewData
39	}
40
41	descriptor, err := viewToMetricDescriptor(vd.View)
42	if err != nil {
43		return nil, err
44	}
45
46	timeseries, err := viewDataToTimeseries(vd)
47	if err != nil {
48		return nil, err
49	}
50
51	metric := &metricspb.Metric{
52		MetricDescriptor: descriptor,
53		Timeseries:       timeseries,
54	}
55	return metric, nil
56}
57
58func viewToMetricDescriptor(v *view.View) (*metricspb.MetricDescriptor, error) {
59	if v == nil {
60		return nil, errNilView
61	}
62	if v.Measure == nil {
63		return nil, errNilMeasure
64	}
65
66	desc := &metricspb.MetricDescriptor{
67		Name:        stringOrCall(v.Name, v.Measure.Name),
68		Description: stringOrCall(v.Description, v.Measure.Description),
69		Unit:        v.Measure.Unit(),
70		Type:        aggregationToMetricDescriptorType(v),
71		LabelKeys:   tagKeysToLabelKeys(v.TagKeys),
72	}
73	return desc, nil
74}
75
76func stringOrCall(first string, call func() string) string {
77	if first != "" {
78		return first
79	}
80	return call()
81}
82
83type measureType uint
84
85const (
86	measureUnknown measureType = iota
87	measureInt64
88	measureFloat64
89)
90
91func measureTypeFromMeasure(m stats.Measure) measureType {
92	switch m.(type) {
93	default:
94		return measureUnknown
95	case *stats.Float64Measure:
96		return measureFloat64
97	case *stats.Int64Measure:
98		return measureInt64
99	}
100}
101
102func aggregationToMetricDescriptorType(v *view.View) metricspb.MetricDescriptor_Type {
103	if v == nil || v.Aggregation == nil {
104		return metricspb.MetricDescriptor_UNSPECIFIED
105	}
106	if v.Measure == nil {
107		return metricspb.MetricDescriptor_UNSPECIFIED
108	}
109
110	switch v.Aggregation.Type {
111	case view.AggTypeCount:
112		// Cumulative on int64
113		return metricspb.MetricDescriptor_CUMULATIVE_INT64
114
115	case view.AggTypeDistribution:
116		// Cumulative types
117		return metricspb.MetricDescriptor_CUMULATIVE_DISTRIBUTION
118
119	case view.AggTypeLastValue:
120		// Gauge types
121		switch measureTypeFromMeasure(v.Measure) {
122		case measureFloat64:
123			return metricspb.MetricDescriptor_GAUGE_DOUBLE
124		case measureInt64:
125			return metricspb.MetricDescriptor_GAUGE_INT64
126		}
127
128	case view.AggTypeSum:
129		// Cumulative types
130		switch measureTypeFromMeasure(v.Measure) {
131		case measureFloat64:
132			return metricspb.MetricDescriptor_CUMULATIVE_DOUBLE
133		case measureInt64:
134			return metricspb.MetricDescriptor_CUMULATIVE_INT64
135		}
136	}
137
138	// For all other cases, return unspecified.
139	return metricspb.MetricDescriptor_UNSPECIFIED
140}
141
142func tagKeysToLabelKeys(tagKeys []tag.Key) []*metricspb.LabelKey {
143	labelKeys := make([]*metricspb.LabelKey, 0, len(tagKeys))
144	for _, tagKey := range tagKeys {
145		labelKeys = append(labelKeys, &metricspb.LabelKey{
146			Key: tagKey.Name(),
147		})
148	}
149	return labelKeys
150}
151
152func viewDataToTimeseries(vd *view.Data) ([]*metricspb.TimeSeries, error) {
153	if vd == nil || len(vd.Rows) == 0 {
154		return nil, nil
155	}
156
157	// Given that view.Data only contains Start, End
158	// the timestamps for all the row data will be the exact same
159	// per aggregation. However, the values will differ.
160	// Each row has its own tags.
161	startTimestamp := timeToProtoTimestamp(vd.Start)
162	endTimestamp := timeToProtoTimestamp(vd.End)
163
164	mType := measureTypeFromMeasure(vd.View.Measure)
165	timeseries := make([]*metricspb.TimeSeries, 0, len(vd.Rows))
166	// It is imperative that the ordering of "LabelValues" matches those
167	// of the Label keys in the metric descriptor.
168	for _, row := range vd.Rows {
169		labelValues := labelValuesFromTags(row.Tags)
170		point := rowToPoint(vd.View, row, endTimestamp, mType)
171		timeseries = append(timeseries, &metricspb.TimeSeries{
172			StartTimestamp: startTimestamp,
173			LabelValues:    labelValues,
174			Points:         []*metricspb.Point{point},
175		})
176	}
177
178	if len(timeseries) == 0 {
179		return nil, nil
180	}
181
182	return timeseries, nil
183}
184
185func timeToProtoTimestamp(t time.Time) *timestamp.Timestamp {
186	unixNano := t.UnixNano()
187	return &timestamp.Timestamp{
188		Seconds: int64(unixNano / 1e9),
189		Nanos:   int32(unixNano % 1e9),
190	}
191}
192
193func rowToPoint(v *view.View, row *view.Row, endTimestamp *timestamp.Timestamp, mType measureType) *metricspb.Point {
194	pt := &metricspb.Point{
195		Timestamp: endTimestamp,
196	}
197
198	switch data := row.Data.(type) {
199	case *view.CountData:
200		pt.Value = &metricspb.Point_Int64Value{Int64Value: data.Value}
201
202	case *view.DistributionData:
203		pt.Value = &metricspb.Point_DistributionValue{
204			DistributionValue: &metricspb.DistributionValue{
205				Count: data.Count,
206				Sum:   float64(data.Count) * data.Mean, // because Mean := Sum/Count
207				// TODO: Add Exemplar
208				Buckets: bucketsToProtoBuckets(data.CountPerBucket),
209				BucketOptions: &metricspb.DistributionValue_BucketOptions{
210					Type: &metricspb.DistributionValue_BucketOptions_Explicit_{
211						Explicit: &metricspb.DistributionValue_BucketOptions_Explicit{
212							Bounds: v.Aggregation.Buckets,
213						},
214					},
215				},
216				SumOfSquaredDeviation: data.SumOfSquaredDev,
217			}}
218
219	case *view.LastValueData:
220		setPointValue(pt, data.Value, mType)
221
222	case *view.SumData:
223		setPointValue(pt, data.Value, mType)
224	}
225
226	return pt
227}
228
229// Not returning anything from this function because metricspb.Point.is_Value is an unexported
230// interface hence we just have to set its value by pointer.
231func setPointValue(pt *metricspb.Point, value float64, mType measureType) {
232	if mType == measureInt64 {
233		pt.Value = &metricspb.Point_Int64Value{Int64Value: int64(value)}
234	} else {
235		pt.Value = &metricspb.Point_DoubleValue{DoubleValue: value}
236	}
237}
238
239func bucketsToProtoBuckets(countPerBucket []int64) []*metricspb.DistributionValue_Bucket {
240	distBuckets := make([]*metricspb.DistributionValue_Bucket, len(countPerBucket))
241	for i := 0; i < len(countPerBucket); i++ {
242		count := countPerBucket[i]
243
244		distBuckets[i] = &metricspb.DistributionValue_Bucket{
245			Count: count,
246		}
247	}
248
249	return distBuckets
250}
251
252func labelValuesFromTags(tags []tag.Tag) []*metricspb.LabelValue {
253	if len(tags) == 0 {
254		return nil
255	}
256
257	labelValues := make([]*metricspb.LabelValue, 0, len(tags))
258	for _, tag_ := range tags {
259		labelValues = append(labelValues, &metricspb.LabelValue{
260			Value: tag_.Value,
261
262			// It is imperative that we set the "HasValue" attribute,
263			// in order to distinguish missing a label from the empty string.
264			// https://godoc.org/github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1#LabelValue.HasValue
265			//
266			// OpenCensus-Go uses non-pointers for tags as seen by this function's arguments,
267			// so the best case that we can use to distinguish missing labels/tags from the
268			// empty string is by checking if the Tag.Key.Name() != "" to indicate that we have
269			// a value.
270			HasValue: tag_.Key.Name() != "",
271		})
272	}
273	return labelValues
274}
275