diff --git a/internal/telemetry/export/ocagent/metrics.go b/internal/telemetry/export/ocagent/metrics.go new file mode 100644 index 0000000000..d49151fd70 --- /dev/null +++ b/internal/telemetry/export/ocagent/metrics.go @@ -0,0 +1,149 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ocagent + +import ( + "fmt" + + "golang.org/x/tools/internal/telemetry" + "golang.org/x/tools/internal/telemetry/export/ocagent/wire" + "golang.org/x/tools/internal/telemetry/metric" +) + +// dataToMetricDescriptor return a *wire.MetricDescriptor based on data. +func dataToMetricDescriptor(data telemetry.MetricData) *wire.MetricDescriptor { + if data == nil { + return nil + } + descriptor := &wire.MetricDescriptor{ + Name: data.Handle(), + Description: getDescription(data), + // TODO: Unit? + Type: dataToMetricDescriptorType(data), + LabelKeys: getLabelKeys(data), + } + + return descriptor +} + +// getDescription returns the description of data. +func getDescription(data telemetry.MetricData) string { + switch d := data.(type) { + case *metric.Int64Data: + return d.Info.Description + + case *metric.Float64Data: + return d.Info.Description + } + + return "" +} + +// getLabelKeys returns a slice of *wire.LabelKeys based on the keys +// in data. +func getLabelKeys(data telemetry.MetricData) []*wire.LabelKey { + switch d := data.(type) { + case *metric.Int64Data: + return infoKeysToLabelKeys(d.Info.Keys) + + case *metric.Float64Data: + return infoKeysToLabelKeys(d.Info.Keys) + } + + return nil +} + +// dataToMetricDescriptorType returns a wire.MetricDescriptor_Type based on the +// underlying type of data. +func dataToMetricDescriptorType(data telemetry.MetricData) wire.MetricDescriptor_Type { + switch d := data.(type) { + case *metric.Int64Data: + if d.IsGauge { + return wire.MetricDescriptor_GAUGE_INT64 + } + return wire.MetricDescriptor_CUMULATIVE_INT64 + + case *metric.Float64Data: + if d.IsGauge { + return wire.MetricDescriptor_GAUGE_DOUBLE + } + return wire.MetricDescriptor_CUMULATIVE_DOUBLE + } + + return wire.MetricDescriptor_UNSPECIFIED +} + +// dataToTimeseries returns a slice of *wire.TimeSeries based on the +// points in data. +func dataToTimeseries(data telemetry.MetricData) []*wire.TimeSeries { + if data == nil { + return nil + } + + numRows := numRows(data) + timeseries := make([]*wire.TimeSeries, 0, numRows) + + for i := 0; i < numRows; i++ { + timeseries = append(timeseries, &wire.TimeSeries{ + // TODO: attach StartTimestamp + // TODO: labels? + Points: dataToPoints(data, i), + }) + } + + return timeseries +} + +// numRows returns the number of rows in data. +func numRows(data telemetry.MetricData) int { + switch d := data.(type) { + case *metric.Int64Data: + return len(d.Rows) + case *metric.Float64Data: + return len(d.Rows) + } + + return 0 +} + +// dataToPoints returns an array of *wire.Points based on the point(s) +// in data at index i. +func dataToPoints(data telemetry.MetricData, i int) []*wire.Point { + switch d := data.(type) { + case *metric.Int64Data: + return []*wire.Point{ + { + Value: wire.PointInt64Value{ + Int64Value: d.Rows[i], + }, + // TODO: attach Timestamp + }, + } + case *metric.Float64Data: + return []*wire.Point{ + { + Value: wire.PointDoubleValue{ + DoubleValue: d.Rows[i], + }, + // TODO: attach Timestamp + }, + } + } + + return nil +} + +// infoKeysToLabelKeys returns an array of *wire.LabelKeys containing the +// string values of the elements of labelKeys. +func infoKeysToLabelKeys(infoKeys []interface{}) []*wire.LabelKey { + labelKeys := make([]*wire.LabelKey, 0, len(infoKeys)) + for _, key := range infoKeys { + labelKeys = append(labelKeys, &wire.LabelKey{ + Key: fmt.Sprintf("%v", key), + }) + } + + return labelKeys +} diff --git a/internal/telemetry/export/ocagent/metrics_test.go b/internal/telemetry/export/ocagent/metrics_test.go new file mode 100644 index 0000000000..6ec986f381 --- /dev/null +++ b/internal/telemetry/export/ocagent/metrics_test.go @@ -0,0 +1,483 @@ +package ocagent + +import ( + "reflect" + "testing" + + "golang.org/x/tools/internal/telemetry" + "golang.org/x/tools/internal/telemetry/export/ocagent/wire" + "golang.org/x/tools/internal/telemetry/metric" +) + +func TestDataToMetricDescriptor(t *testing.T) { + tests := []struct { + name string + data telemetry.MetricData + want *wire.MetricDescriptor + }{ + { + "nil data", + nil, + nil, + }, + { + "Int64Data gauge", + &metric.Int64Data{ + Info: &metric.Scalar{ + Name: "int", + Description: "int metric", + Keys: []interface{}{"hello"}, + }, + IsGauge: true, + }, + &wire.MetricDescriptor{ + Name: "int", + Description: "int metric", + Type: wire.MetricDescriptor_GAUGE_INT64, + LabelKeys: []*wire.LabelKey{ + &wire.LabelKey{ + Key: "hello", + }, + }, + }, + }, + { + "Float64Data cumulative", + &metric.Float64Data{ + Info: &metric.Scalar{ + Name: "float", + Description: "float metric", + Keys: []interface{}{"world"}, + }, + IsGauge: false, + }, + &wire.MetricDescriptor{ + Name: "float", + Description: "float metric", + Type: wire.MetricDescriptor_CUMULATIVE_DOUBLE, + LabelKeys: []*wire.LabelKey{ + &wire.LabelKey{ + Key: "world", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := dataToMetricDescriptor(tt.data) + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("Got:\n%s\nWant:\n%s", marshaled(got), marshaled(tt.want)) + } + }) + } +} + +func TestGetDescription(t *testing.T) { + tests := []struct { + name string + data telemetry.MetricData + want string + }{ + { + "nil data", + nil, + "", + }, + { + "Int64Data description", + &metric.Int64Data{ + Info: &metric.Scalar{ + Description: "int metric", + }, + }, + "int metric", + }, + { + "Float64Data description", + &metric.Float64Data{ + Info: &metric.Scalar{ + Description: "float metric", + }, + }, + "float metric", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getDescription(tt.data) + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("Got:\n%s\nWant:\n%s", marshaled(got), marshaled(tt.want)) + } + }) + } + +} + +func TestGetLabelKeys(t *testing.T) { + tests := []struct { + name string + data telemetry.MetricData + want []*wire.LabelKey + }{ + { + "nil label keys", + nil, + nil, + }, + { + "Int64Data label keys", + &metric.Int64Data{ + Info: &metric.Scalar{ + Keys: []interface{}{ + "hello", + }, + }, + }, + []*wire.LabelKey{ + &wire.LabelKey{ + Key: "hello", + }, + }, + }, + { + "Float64Data label keys", + &metric.Float64Data{ + Info: &metric.Scalar{ + Keys: []interface{}{ + "world", + }, + }, + }, + []*wire.LabelKey{ + &wire.LabelKey{ + Key: "world", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getLabelKeys(tt.data) + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("Got:\n%s\nWant:\n%s", marshaled(got), marshaled(tt.want)) + } + }) + } +} + +func TestDataToMetricDescriptorType(t *testing.T) { + tests := []struct { + name string + data telemetry.MetricData + want wire.MetricDescriptor_Type + }{ + { + "Nil data", + nil, + wire.MetricDescriptor_UNSPECIFIED, + }, + { + "Gauge Int64", + &metric.Int64Data{ + IsGauge: true, + }, + wire.MetricDescriptor_GAUGE_INT64, + }, + { + "Cumulative Int64", + &metric.Int64Data{ + IsGauge: false, + }, + wire.MetricDescriptor_CUMULATIVE_INT64, + }, + { + "Gauge Float64", + &metric.Float64Data{ + IsGauge: true, + }, + wire.MetricDescriptor_GAUGE_DOUBLE, + }, + { + "Cumulative Float64", + &metric.Float64Data{ + IsGauge: false, + }, + wire.MetricDescriptor_CUMULATIVE_DOUBLE, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := dataToMetricDescriptorType(tt.data) + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("Got:\n%s\nWant:\n%s", marshaled(got), marshaled(tt.want)) + } + }) + } +} + +func TestDataToTimeseries(t *testing.T) { + tests := []struct { + name string + data telemetry.MetricData + want []*wire.TimeSeries + }{ + { + "nil data", + nil, + nil, + }, + { + "Int64Data", + &metric.Int64Data{ + Rows: []int64{ + 1, + 2, + 3, + }, + }, + []*wire.TimeSeries{ + &wire.TimeSeries{ + Points: []*wire.Point{ + &wire.Point{ + Value: wire.PointInt64Value{Int64Value: 1}, + }, + }, + }, + &wire.TimeSeries{ + Points: []*wire.Point{ + &wire.Point{ + Value: wire.PointInt64Value{Int64Value: 2}, + }, + }, + }, + &wire.TimeSeries{ + Points: []*wire.Point{ + &wire.Point{ + Value: wire.PointInt64Value{Int64Value: 3}, + }, + }, + }, + }, + }, + { + "Float64Data", + &metric.Float64Data{ + Rows: []float64{ + 1.5, + 4.5, + }, + }, + []*wire.TimeSeries{ + &wire.TimeSeries{ + Points: []*wire.Point{ + &wire.Point{ + Value: wire.PointDoubleValue{DoubleValue: 1.5}, + }, + }, + }, + &wire.TimeSeries{ + Points: []*wire.Point{ + &wire.Point{ + Value: wire.PointDoubleValue{DoubleValue: 4.5}, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := dataToTimeseries(tt.data) + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("Got:\n%s\nWant:\n%s", marshaled(got), marshaled(tt.want)) + } + }) + } +} + +func TestNumRows(t *testing.T) { + tests := []struct { + name string + data telemetry.MetricData + want int + }{ + { + "nil data", + nil, + 0, + }, + { + "1 row Int64Data", + &metric.Int64Data{ + Rows: []int64{ + 0, + }, + }, + 1, + }, + { + "2 row Float64Data", + &metric.Float64Data{ + Rows: []float64{ + 0, + 1.0, + }, + }, + 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := numRows(tt.data) + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("Got:\n%s\nWant:\n%s", marshaled(got), marshaled(tt.want)) + } + }) + } +} + +func TestDataToPoints(t *testing.T) { + int64Data := &metric.Int64Data{ + Rows: []int64{ + 0, + 10, + }, + } + + float64Data := &metric.Float64Data{ + Rows: []float64{ + 0.5, + 0.25, + }, + } + + tests := []struct { + name string + data telemetry.MetricData + i int + want []*wire.Point + }{ + { + "nil data", + nil, + 0, + nil, + }, + { + "Int64data index 0", + int64Data, + 0, + []*wire.Point{ + { + Value: wire.PointInt64Value{ + Int64Value: 0, + }, + }, + }, + }, + { + "Int64data index 1", + int64Data, + 1, + []*wire.Point{ + { + Value: wire.PointInt64Value{ + Int64Value: 10, + }, + }, + }, + }, + { + "Float64Data index 0", + float64Data, + 0, + []*wire.Point{ + { + Value: wire.PointDoubleValue{ + DoubleValue: 0.5, + }, + }, + }, + }, + { + "Float64Data index 1", + float64Data, + 1, + []*wire.Point{ + { + Value: wire.PointDoubleValue{ + DoubleValue: 0.25, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := dataToPoints(tt.data, tt.i) + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("Got:\n%s\nWant:\n%s", marshaled(got), marshaled(tt.want)) + } + }) + } +} + +func TestInfoKeysToLabelKeys(t *testing.T) { + tests := []struct { + name string + infoKeys []interface{} + want []*wire.LabelKey + }{ + { + "empty infoKeys", + []interface{}{}, + []*wire.LabelKey{}, + }, + { + "empty string infoKey", + []interface{}{""}, + []*wire.LabelKey{ + &wire.LabelKey{ + Key: "", + }, + }, + }, + { + "non-empty string infoKey", + []interface{}{"hello"}, + []*wire.LabelKey{ + &wire.LabelKey{ + Key: "hello", + }, + }, + }, + { + "multiple element infoKey", + []interface{}{"hello", "world"}, + []*wire.LabelKey{ + &wire.LabelKey{ + Key: "hello", + }, + &wire.LabelKey{ + Key: "world", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := infoKeysToLabelKeys(tt.infoKeys) + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("Got:\n%s\nWant:\n%s", marshaled(got), marshaled(tt.want)) + } + }) + } +} diff --git a/internal/telemetry/export/ocagent/ocagent.go b/internal/telemetry/export/ocagent/ocagent.go index bd2ac708d4..d1ebfd2c43 100644 --- a/internal/telemetry/export/ocagent/ocagent.go +++ b/internal/telemetry/export/ocagent/ocagent.go @@ -203,7 +203,19 @@ func convertSpan(span *telemetry.Span) *wire.Span { } func convertMetric(data telemetry.MetricData) *wire.Metric { - return nil //TODO: + descriptor := dataToMetricDescriptor(data) + timeseries := dataToTimeseries(data) + + if descriptor == nil && timeseries == nil { + return nil + } + + // TODO: handle Histogram metrics + return &wire.Metric{ + MetricDescriptor: descriptor, + Timeseries: timeseries, + // TODO: attach Resource? + } } func convertAttributes(tags telemetry.TagList) *wire.Attributes {