diff --git a/metrics.go b/metrics.go new file mode 100644 index 0000000..96e67cf --- /dev/null +++ b/metrics.go @@ -0,0 +1,135 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stackdriver + +/* +The code in this file is responsible for converting OpenCensus Proto metrics +directly to Stackdriver Metrics. +*/ + +import ( + "fmt" + + "github.com/golang/protobuf/ptypes/timestamp" + + distributionpb "google.golang.org/genproto/googleapis/api/distribution" + monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3" + + metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1" +) + +func fromProtoPoint(startTime *timestamp.Timestamp, pt *metricspb.Point) (*monitoringpb.Point, error) { + if pt == nil { + return nil, nil + } + + mptv, err := protoToMetricPoint(pt.Value) + if err != nil { + return nil, err + } + + mpt := &monitoringpb.Point{ + Value: mptv, + Interval: &monitoringpb.TimeInterval{ + StartTime: startTime, + EndTime: pt.Timestamp, + }, + } + return mpt, nil +} + +func protoToMetricPoint(value interface{}) (*monitoringpb.TypedValue, error) { + if value == nil { + return nil, nil + } + + var err error + var tval *monitoringpb.TypedValue + switch v := value.(type) { + default: + // All the other types are not yet handled. + // TODO: (@odeke-em, @songy23) talk to the Stackdriver team to determine + // the use cases for: + // + // *TypedValue_BoolValue + // *TypedValue_StringValue + // + // and then file feature requests on OpenCensus-Specs and then OpenCensus-Proto, + // lest we shall error here. + // + // TODO: Add conversion from SummaryValue when + // https://github.com/census-ecosystem/opencensus-go-exporter-stackdriver/issues/66 + // has been figured out. + err = fmt.Errorf("protoToMetricPoint: unknown Data type: %T", value) + + case *metricspb.Point_Int64Value: + tval = &monitoringpb.TypedValue{ + Value: &monitoringpb.TypedValue_Int64Value{ + Int64Value: v.Int64Value, + }, + } + + case *metricspb.Point_DoubleValue: + tval = &monitoringpb.TypedValue{ + Value: &monitoringpb.TypedValue_DoubleValue{ + DoubleValue: v.DoubleValue, + }, + } + + case *metricspb.Point_DistributionValue: + dv := v.DistributionValue + var mv *monitoringpb.TypedValue_DistributionValue + if dv != nil { + var mean float64 + if dv.Count > 0 { + mean = float64(dv.Sum) / float64(dv.Count) + } + mv = &monitoringpb.TypedValue_DistributionValue{ + DistributionValue: &distributionpb.Distribution{ + Count: dv.Count, + Mean: mean, + SumOfSquaredDeviation: dv.SumOfSquaredDeviation, + BucketCounts: bucketCounts(dv.Buckets), + }, + } + + if bopts := dv.BucketOptions; bopts != nil && bopts.Type != nil { + bexp, ok := bopts.Type.(*metricspb.DistributionValue_BucketOptions_Explicit_) + if ok && bexp != nil && bexp.Explicit != nil { + mv.DistributionValue.BucketOptions = &distributionpb.Distribution_BucketOptions{ + Options: &distributionpb.Distribution_BucketOptions_ExplicitBuckets{ + ExplicitBuckets: &distributionpb.Distribution_BucketOptions_Explicit{ + Bounds: bexp.Explicit.Bounds[:], + }, + }, + } + } + } + } + tval = &monitoringpb.TypedValue{Value: mv} + } + + return tval, err +} + +func bucketCounts(buckets []*metricspb.DistributionValue_Bucket) []int64 { + bucketCounts := make([]int64, len(buckets)) + for i, bucket := range buckets { + if bucket != nil { + bucketCounts[i] = bucket.Count + } + } + return bucketCounts +} diff --git a/metrics_test.go b/metrics_test.go new file mode 100644 index 0000000..fe4b8e9 --- /dev/null +++ b/metrics_test.go @@ -0,0 +1,146 @@ +// Copyright 2018, OpenCensus Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stackdriver + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/golang/protobuf/ptypes/timestamp" + distributionpb "google.golang.org/genproto/googleapis/api/distribution" + monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3" + + metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1" +) + +func TestProtoMetricsToMonitoringMetrics_fromProtoPoint(t *testing.T) { + startTimestamp := ×tamp.Timestamp{ + Seconds: 1543160298, + Nanos: 100000090, + } + endTimestamp := ×tamp.Timestamp{ + Seconds: 1543160298, + Nanos: 100000997, + } + + tests := []struct { + in *metricspb.Point + want *monitoringpb.Point + wantErr string + }{ + { + in: &metricspb.Point{ + Timestamp: endTimestamp, + Value: &metricspb.Point_DistributionValue{ + DistributionValue: &metricspb.DistributionValue{ + Count: 1, + Sum: 11.9, + SumOfSquaredDeviation: 0, + Buckets: []*metricspb.DistributionValue_Bucket{ + {}, {Count: 1}, {}, {}, {}, + }, + BucketOptions: &metricspb.DistributionValue_BucketOptions{ + Type: &metricspb.DistributionValue_BucketOptions_Explicit_{ + Explicit: &metricspb.DistributionValue_BucketOptions_Explicit{ + Bounds: []float64{0, 10, 20, 30, 40}, + }, + }, + }, + }, + }, + }, + want: &monitoringpb.Point{ + Interval: &monitoringpb.TimeInterval{ + StartTime: startTimestamp, + EndTime: endTimestamp, + }, + Value: &monitoringpb.TypedValue{ + Value: &monitoringpb.TypedValue_DistributionValue{ + DistributionValue: &distributionpb.Distribution{ + Count: 1, + Mean: 11.9, + SumOfSquaredDeviation: 0, + BucketCounts: []int64{0, 1, 0, 0, 0}, + BucketOptions: &distributionpb.Distribution_BucketOptions{ + Options: &distributionpb.Distribution_BucketOptions_ExplicitBuckets{ + ExplicitBuckets: &distributionpb.Distribution_BucketOptions_Explicit{ + Bounds: []float64{0, 10, 20, 30, 40}, + }, + }, + }, + }, + }, + }, + }, + }, + { + in: &metricspb.Point{ + Timestamp: endTimestamp, + Value: &metricspb.Point_DoubleValue{DoubleValue: 50}, + }, + want: &monitoringpb.Point{ + Interval: &monitoringpb.TimeInterval{ + StartTime: startTimestamp, + EndTime: endTimestamp, + }, + Value: &monitoringpb.TypedValue{ + Value: &monitoringpb.TypedValue_DoubleValue{DoubleValue: 50}, + }, + }, + }, + { + in: &metricspb.Point{ + Timestamp: endTimestamp, + Value: &metricspb.Point_Int64Value{Int64Value: 17}, + }, + want: &monitoringpb.Point{ + Interval: &monitoringpb.TimeInterval{ + StartTime: startTimestamp, + EndTime: endTimestamp, + }, + Value: &monitoringpb.TypedValue{ + Value: &monitoringpb.TypedValue_Int64Value{Int64Value: 17}, + }, + }, + }, + } + + for i, tt := range tests { + mpt, err := fromProtoPoint(startTimestamp, tt.in) + if tt.wantErr != "" { + continue + } + + if err != nil { + t.Errorf("#%d: unexpected error: %v", i, err) + continue + } + + if g, w := mpt, tt.want; !reflect.DeepEqual(g, w) { + // Our saving grace is serialization equality since some + // unexported fields could be present in the various values. + gj, wj := serializeAsJSON(g), serializeAsJSON(w) + if gj != wj { + t.Errorf("#%d: Unmatched JSON\nGot:\n\t%s\nWant:\n\t%s", i, gj, wj) + } + } + } +} + +func serializeAsJSON(v interface{}) string { + blob, _ := json.MarshalIndent(v, "", " ") + return string(blob) +}