From ea69b464e9b7bc24e9267b3f4e0ace09cab0698a Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Mon, 10 Dec 2018 18:38:12 -0800 Subject: [PATCH] metrics: Metrics to monitoring/v3.MetricDescriptor A converter from Metrics to a monitoring/v3.MetricDescriptor Updates #64. --- metrics.go | 105 ++++++++++++++++++++++++++++++++++++++++++++++++ metrics_test.go | 87 +++++++++++++++++++++++++++++++++++++++ stats.go | 14 ++++--- 3 files changed, 201 insertions(+), 5 deletions(-) diff --git a/metrics.go b/metrics.go index 96e67cf..d1f3386 100644 --- a/metrics.go +++ b/metrics.go @@ -20,16 +20,91 @@ directly to Stackdriver Metrics. */ import ( + "errors" "fmt" + "path" "github.com/golang/protobuf/ptypes/timestamp" distributionpb "google.golang.org/genproto/googleapis/api/distribution" + labelpb "google.golang.org/genproto/googleapis/api/label" + googlemetricpb "google.golang.org/genproto/googleapis/api/metric" monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3" metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1" ) +var errNilMetric = errors.New("expecting a non-nil metric") + +func (se *statsExporter) protoToMonitoringMetricDescriptor(metric *metricspb.Metric) (*googlemetricpb.MetricDescriptor, error) { + if metric == nil { + return nil, errNilMetric + } + + metricName, description, unit, _ := metricProseFromProto(metric) + metricType, _ := se.metricTypeFromProto(metricName) + displayName := se.displayName(metricName) + metricKind, valueType := protoMetricDescriptorTypeToMetricKind(metric) + + sdm := &googlemetricpb.MetricDescriptor{ + Name: fmt.Sprintf("projects/%s/metricDescriptors/%s", se.o.ProjectID, metricType), + DisplayName: displayName, + Description: description, + Unit: unit, + Type: metricType, + MetricKind: metricKind, + ValueType: valueType, + Labels: labelDescriptorsFromProto(se.defaultLabels, metric.GetMetricDescriptor().GetLabelKeys()), + } + + return sdm, nil +} + +func labelDescriptorsFromProto(defaults map[string]labelValue, protoLabelKeys []*metricspb.LabelKey) []*labelpb.LabelDescriptor { + labelDescriptors := make([]*labelpb.LabelDescriptor, 0, len(defaults)+len(protoLabelKeys)) + + // Fill in the defaults first. + for key, lbl := range defaults { + labelDescriptors = append(labelDescriptors, &labelpb.LabelDescriptor{ + Key: sanitize(key), + Description: lbl.desc, + ValueType: labelpb.LabelDescriptor_STRING, + }) + } + + // Now fill in those from the metric. + for _, protoKey := range protoLabelKeys { + labelDescriptors = append(labelDescriptors, &labelpb.LabelDescriptor{ + Key: sanitize(protoKey.GetKey()), + Description: protoKey.GetDescription(), + ValueType: labelpb.LabelDescriptor_STRING, // We only use string tags + }) + } + return labelDescriptors +} + +func metricProseFromProto(metric *metricspb.Metric) (name, description, unit string, ok bool) { + mname := metric.GetName() + if mname != "" { + name = mname + return + } + + md := metric.GetMetricDescriptor() + + name = md.GetName() + unit = md.GetUnit() + description = md.GetDescription() + + return +} + +func (se *statsExporter) metricTypeFromProto(name string) (string, bool) { + // TODO: (@odeke-em) support non-"custom.googleapis.com" metrics names. + name = path.Join("custom.googleapis.com", "opencensus", name) + return name, true +} + func fromProtoPoint(startTime *timestamp.Timestamp, pt *metricspb.Point) (*monitoringpb.Point, error) { if pt == nil { return nil, nil @@ -133,3 +208,33 @@ func bucketCounts(buckets []*metricspb.DistributionValue_Bucket) []int64 { } return bucketCounts } + +func protoMetricDescriptorTypeToMetricKind(m *metricspb.Metric) (googlemetricpb.MetricDescriptor_MetricKind, googlemetricpb.MetricDescriptor_ValueType) { + dt := m.GetMetricDescriptor() + if dt == nil { + return googlemetricpb.MetricDescriptor_METRIC_KIND_UNSPECIFIED, googlemetricpb.MetricDescriptor_VALUE_TYPE_UNSPECIFIED + } + + switch dt.Type { + case metricspb.MetricDescriptor_CUMULATIVE_INT64: + return googlemetricpb.MetricDescriptor_CUMULATIVE, googlemetricpb.MetricDescriptor_INT64 + + case metricspb.MetricDescriptor_CUMULATIVE_DOUBLE: + return googlemetricpb.MetricDescriptor_CUMULATIVE, googlemetricpb.MetricDescriptor_DOUBLE + + case metricspb.MetricDescriptor_CUMULATIVE_DISTRIBUTION: + return googlemetricpb.MetricDescriptor_CUMULATIVE, googlemetricpb.MetricDescriptor_DISTRIBUTION + + case metricspb.MetricDescriptor_GAUGE_DOUBLE: + return googlemetricpb.MetricDescriptor_GAUGE, googlemetricpb.MetricDescriptor_DOUBLE + + case metricspb.MetricDescriptor_GAUGE_INT64: + return googlemetricpb.MetricDescriptor_GAUGE, googlemetricpb.MetricDescriptor_INT64 + + case metricspb.MetricDescriptor_GAUGE_DISTRIBUTION: + return googlemetricpb.MetricDescriptor_GAUGE, googlemetricpb.MetricDescriptor_DISTRIBUTION + + default: + return googlemetricpb.MetricDescriptor_METRIC_KIND_UNSPECIFIED, googlemetricpb.MetricDescriptor_VALUE_TYPE_UNSPECIFIED + } +} diff --git a/metrics_test.go b/metrics_test.go index fe4b8e9..4e35f64 100644 --- a/metrics_test.go +++ b/metrics_test.go @@ -17,15 +17,102 @@ package stackdriver import ( "encoding/json" "reflect" + "strings" "testing" "github.com/golang/protobuf/ptypes/timestamp" distributionpb "google.golang.org/genproto/googleapis/api/distribution" + googlemetricpb "google.golang.org/genproto/googleapis/api/metric" monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3" metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1" ) +func TestProtoToMonitoringMetricDescriptor(t *testing.T) { + tests := []struct { + in *metricspb.Metric + want *googlemetricpb.MetricDescriptor + wantErr string + + statsExporter *statsExporter + }{ + {in: nil, wantErr: "non-nil metric"}, + { + in: &metricspb.Metric{}, + statsExporter: &statsExporter{ + o: Options{ProjectID: "test"}, + }, + want: &googlemetricpb.MetricDescriptor{ + Name: "projects/test/metricDescriptors/custom.googleapis.com/opencensus", + Type: "custom.googleapis.com/opencensus", + DisplayName: "OpenCensus", + }, + }, + { + in: &metricspb.Metric{ + Descriptor_: &metricspb.Metric_Name{Name: "with_name"}, + }, + statsExporter: &statsExporter{ + o: Options{ProjectID: "test"}, + }, + want: &googlemetricpb.MetricDescriptor{ + Name: "projects/test/metricDescriptors/custom.googleapis.com/opencensus/with_name", + Type: "custom.googleapis.com/opencensus/with_name", + DisplayName: "OpenCensus/with_name", + }, + }, + { + in: &metricspb.Metric{ + Descriptor_: &metricspb.Metric_MetricDescriptor{ + MetricDescriptor: &metricspb.MetricDescriptor{ + Name: "with_metric_descriptor", + Description: "This is with metric descriptor", + Unit: "By", + }, + }, + }, + statsExporter: &statsExporter{ + o: Options{ProjectID: "test"}, + }, + want: &googlemetricpb.MetricDescriptor{ + Name: "projects/test/metricDescriptors/custom.googleapis.com/opencensus/with_metric_descriptor", + Type: "custom.googleapis.com/opencensus/with_metric_descriptor", + DisplayName: "OpenCensus/with_metric_descriptor", + Description: "This is with metric descriptor", + Unit: "By", + }, + }, + } + + for i, tt := range tests { + se := tt.statsExporter + if se == nil { + se = new(statsExporter) + } + got, err := se.protoToMonitoringMetricDescriptor(tt.in) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Errorf("#%d: \nGot %v\nWanted error substring %q", i, err, tt.wantErr) + } + continue + } + + if err != nil { + t.Errorf("#%d: Unexpected error: %v", i, err) + continue + } + + if !reflect.DeepEqual(got, tt.want) { + // Our saving grace is serialization equality since some + // unexported fields could be present in the various values. + gj, wj := serializeAsJSON(got), serializeAsJSON(tt.want) + if gj != wj { + t.Errorf("#%d: Unmatched JSON\nGot:\n\t%s\nWant:\n\t%s", i, gj, wj) + } + } + } +} + func TestProtoMetricsToMonitoringMetrics_fromProtoPoint(t *testing.T) { startTimestamp := ×tamp.Timestamp{ Seconds: 1543160298, diff --git a/stats.go b/stats.go index 92ab801..05bdf9f 100644 --- a/stats.go +++ b/stats.go @@ -278,11 +278,7 @@ func (e *statsExporter) createMeasure(ctx context.Context, v *view.View) error { var displayName string if e.o.GetMetricDisplayName == nil { - displayNamePrefix := defaultDisplayNamePrefix - if e.o.MetricPrefix != "" { - displayNamePrefix = e.o.MetricPrefix - } - displayName = path.Join(displayNamePrefix, viewName) + displayName = e.displayName(viewName) } else { displayName = e.o.GetMetricDisplayName(v) } @@ -308,6 +304,14 @@ func (e *statsExporter) createMeasure(ctx context.Context, v *view.View) error { return nil } +func (e *statsExporter) displayName(suffix string) string { + displayNamePrefix := defaultDisplayNamePrefix + if e.o.MetricPrefix != "" { + displayNamePrefix = e.o.MetricPrefix + } + return path.Join(displayNamePrefix, suffix) +} + func newPoint(v *view.View, row *view.Row, start, end time.Time) *monitoringpb.Point { switch v.Aggregation.Type { case view.AggTypeLastValue: