From 445a4eb0e9f2ff4e12a1717c9f7abe433acdb6bb Mon Sep 17 00:00:00 2001 From: Joshua Powers Date: Mon, 26 Jun 2023 07:30:11 -0600 Subject: [PATCH] fix(outputs.stackdriver): Options to use official path and types (#13454) --- plugins/outputs/stackdriver/README.md | 22 +- plugins/outputs/stackdriver/sample.conf | 15 ++ plugins/outputs/stackdriver/stackdriver.go | 88 ++++++- .../outputs/stackdriver/stackdriver_test.go | 222 +++++++++++++++++- 4 files changed, 340 insertions(+), 7 deletions(-) diff --git a/plugins/outputs/stackdriver/README.md b/plugins/outputs/stackdriver/README.md index 6699423dfdc0f..af0684c61487b 100644 --- a/plugins/outputs/stackdriver/README.md +++ b/plugins/outputs/stackdriver/README.md @@ -9,8 +9,11 @@ costs. Requires `project` to specify where Stackdriver metrics will be delivered to. -Metrics are grouped by the `namespace` variable and metric key - eg: -`custom.googleapis.com/telegraf/system/load5` +By default, Metrics are grouped by the `namespace` variable and metric key - +eg: `custom.googleapis.com/telegraf/system/load5`. However, this is not the +best practice. Setting `metric_name_format = "official"` will produce a more +easily queried format of: `metric_type_prefix/[namespace_]name_key/kind`. If +the global namespace is not set, it is omitted as well. [Resource type](https://cloud.google.com/monitoring/api/resources) is configured by the `resource_type` variable (default `global`). @@ -36,12 +39,27 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details. project = "erudite-bloom-151019" ## The namespace for the metric descriptor + ## This is optional and users are encouraged to set the namespace as a + ## resource label instead. If omitted it is not included in the metric name. namespace = "telegraf" ## Metric Type Prefix ## The DNS name used with the metric type as a prefix. # metric_type_prefix = "custom.googleapis.com" + ## Metric Name Format + ## Specifies the layout of the metric name, choose from: + ## * path: 'metric_type_prefix_namespace_name_key' + ## * official: 'metric_type_prefix/namespace_name_key/kind' + # metric_name_format = "path" + + ## Metric Data Type + ## By default, telegraf will use whatever type the metric comes in as. + ## However, for some use cases, forcing int64, may be preferred for values: + ## * source: use whatever was passed in + ## * double: preferred datatype to allow queries by PromQL. + # metric_data_type = "source" + ## Custom resource type # resource_type = "generic_node" diff --git a/plugins/outputs/stackdriver/sample.conf b/plugins/outputs/stackdriver/sample.conf index e0eac793bed52..4b9cbbdd208b9 100644 --- a/plugins/outputs/stackdriver/sample.conf +++ b/plugins/outputs/stackdriver/sample.conf @@ -4,12 +4,27 @@ project = "erudite-bloom-151019" ## The namespace for the metric descriptor + ## This is optional and users are encouraged to set the namespace as a + ## resource label instead. If omitted it is not included in the metric name. namespace = "telegraf" ## Metric Type Prefix ## The DNS name used with the metric type as a prefix. # metric_type_prefix = "custom.googleapis.com" + ## Metric Name Format + ## Specifies the layout of the metric name, choose from: + ## * path: 'metric_type_prefix_namespace_name_key' + ## * official: 'metric_type_prefix/namespace_name_key/kind' + # metric_name_format = "path" + + ## Metric Data Type + ## By default, telegraf will use whatever type the metric comes in as. + ## However, for some use cases, forcing int64, may be preferred for values: + ## * source: use whatever was passed in + ## * double: preferred datatype to allow queries by PromQL. + # metric_data_type = "source" + ## Custom resource type # resource_type = "generic_node" diff --git a/plugins/outputs/stackdriver/stackdriver.go b/plugins/outputs/stackdriver/stackdriver.go index c42e115fd90dd..619f56902dff3 100644 --- a/plugins/outputs/stackdriver/stackdriver.go +++ b/plugins/outputs/stackdriver/stackdriver.go @@ -32,6 +32,8 @@ type Stackdriver struct { ResourceType string `toml:"resource_type"` ResourceLabels map[string]string `toml:"resource_labels"` MetricTypePrefix string `toml:"metric_type_prefix"` + MetricNameFormat string `toml:"metric_name_format"` + MetricDataType string `toml:"metric_data_type"` Log telegraf.Logger `toml:"-"` client *monitoring.MetricClient @@ -62,6 +64,22 @@ func (s *Stackdriver) Init() error { s.MetricTypePrefix = "custom.googleapis.com" } + switch s.MetricNameFormat { + case "": + s.MetricNameFormat = "path" + case "path", "official": + default: + return fmt.Errorf("unrecognized metric name format: %s", s.MetricNameFormat) + } + + switch s.MetricDataType { + case "": + s.MetricDataType = "source" + case "source", "double": + default: + return fmt.Errorf("unrecognized metric data type: %s", s.MetricDataType) + } + return nil } @@ -76,7 +94,7 @@ func (s *Stackdriver) Connect() error { } if s.Namespace == "" { - return fmt.Errorf("namespace is a required field for stackdriver output") + s.Log.Warn("plugin-level namespace is empty") } if s.ResourceType == "" { @@ -175,7 +193,7 @@ func (s *Stackdriver) sendBatch(batch []telegraf.Metric) error { buckets := make(timeSeriesBuckets) for _, m := range batch { for _, f := range m.FieldList() { - value, err := getStackdriverTypedValue(f.Value) + value, err := s.getStackdriverTypedValue(f.Value) if err != nil { s.Log.Errorf("Get type failed: %q", err) continue @@ -208,7 +226,7 @@ func (s *Stackdriver) sendBatch(batch []telegraf.Metric) error { // Prepare time series. timeSeries := &monitoringpb.TimeSeries{ Metric: &metricpb.Metric{ - Type: path.Join(s.MetricTypePrefix, s.Namespace, m.Name(), f.Key), + Type: s.generateMetricName(m, f.Key), Labels: s.getStackdriverLabels(m.TagList()), }, MetricKind: metricKind, @@ -222,6 +240,28 @@ func (s *Stackdriver) sendBatch(batch []telegraf.Metric) error { } buckets.Add(m, f, timeSeries) + + // If the metric is untyped, it will end with unknown. We will also + // send another metric with the unknown:counter suffix. Google will + // do some heuristics to know which one to use for queries. This + // only occurs when using the official name format. + if s.MetricNameFormat == "official" && strings.HasSuffix(timeSeries.Metric.Type, "unknown") { + counterTimeSeries := &monitoringpb.TimeSeries{ + Metric: &metricpb.Metric{ + Type: s.generateMetricName(m, f.Key) + ":counter", + Labels: s.getStackdriverLabels(m.TagList()), + }, + MetricKind: metricpb.MetricDescriptor_CUMULATIVE, + Resource: &monitoredrespb.MonitoredResource{ + Type: s.ResourceType, + Labels: s.ResourceLabels, + }, + Points: []*monitoringpb.Point{ + dataPoint, + }, + } + buckets.Add(m, f, counterTimeSeries) + } } } @@ -273,6 +313,33 @@ func (s *Stackdriver) sendBatch(batch []telegraf.Metric) error { return nil } +func (s *Stackdriver) generateMetricName(m telegraf.Metric, key string) string { + if s.MetricNameFormat == "path" { + return path.Join(s.MetricTypePrefix, s.Namespace, m.Name(), key) + } + + name := m.Name() + "_" + key + if s.Namespace != "" { + name = s.Namespace + "_" + m.Name() + "_" + key + } + + var kind string + switch m.Type() { + case telegraf.Gauge: + kind = "gauge" + case telegraf.Untyped: + kind = "unknown" + case telegraf.Counter: + kind = "counter" + case telegraf.Histogram: + kind = "histogram" + default: + kind = "" + } + + return path.Join(s.MetricTypePrefix, name, kind) +} + func getStackdriverIntervalEndpoints( kind metricpb.MetricDescriptor_MetricKind, value *monitoringpb.TypedValue, @@ -328,7 +395,20 @@ func getStackdriverMetricKind(vt telegraf.ValueType) (metricpb.MetricDescriptor_ } } -func getStackdriverTypedValue(value interface{}) (*monitoringpb.TypedValue, error) { +func (s *Stackdriver) getStackdriverTypedValue(value interface{}) (*monitoringpb.TypedValue, error) { + if s.MetricDataType == "double" { + v, err := internal.ToFloat64(value) + if err != nil { + return nil, err + } + + return &monitoringpb.TypedValue{ + Value: &monitoringpb.TypedValue_DoubleValue{ + DoubleValue: v, + }, + }, nil + } + switch v := value.(type) { case uint64: if v <= uint64(MaxInt) { diff --git a/plugins/outputs/stackdriver/stackdriver_test.go b/plugins/outputs/stackdriver/stackdriver_test.go index 76a0c470becc4..c8142cdda9659 100644 --- a/plugins/outputs/stackdriver/stackdriver_test.go +++ b/plugins/outputs/stackdriver/stackdriver_test.go @@ -7,6 +7,7 @@ import ( "log" "net" "os" + "reflect" "strings" "testing" "time" @@ -24,6 +25,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/metric" "github.com/influxdata/telegraf/testutil" ) @@ -540,7 +542,7 @@ func TestGetStackdriverIntervalEndpoints(t *testing.T) { for idx, m := range metrics { for _, f := range m.FieldList() { - value, err := getStackdriverTypedValue(f.Value) + value, err := s.getStackdriverTypedValue(f.Value) require.NoError(t, err) require.NotNilf(t, value, "Got nil value for metric %q field %q", m, f) @@ -570,3 +572,221 @@ func TestGetStackdriverIntervalEndpoints(t *testing.T) { } } } + +func TestStackdriverTypedValuesSource(t *testing.T) { + s := &Stackdriver{ + Namespace: "namespace", + MetricTypePrefix: "foo", + MetricDataType: "source", + } + + tests := []struct { + name string + key string + expected string + value any + }{ + { + name: "float", + key: "key", + expected: "*monitoringpb.TypedValue_DoubleValue", + value: float64(44.0), + }, + { + name: "int64", + key: "key", + expected: "*monitoringpb.TypedValue_Int64Value", + value: int64(46), + }, + { + name: "uint", + key: "key", + expected: "*monitoringpb.TypedValue_Int64Value", + value: uint64(46), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + typedValue, err := s.getStackdriverTypedValue(tt.value) + require.NoError(t, err) + require.Equal(t, tt.expected, reflect.TypeOf(typedValue.Value).String()) + }) + } +} + +func TestStackdriverTypedValuesInt64(t *testing.T) { + s := &Stackdriver{ + Namespace: "namespace", + MetricTypePrefix: "foo", + MetricDataType: "double", + } + + tests := []struct { + name string + key string + expected string + value any + }{ + { + name: "int", + key: "key", + expected: "*monitoringpb.TypedValue_DoubleValue", + value: 42, + }, + { + name: "float", + key: "key", + expected: "*monitoringpb.TypedValue_DoubleValue", + value: float64(44.0), + }, + { + name: "int64", + key: "key", + expected: "*monitoringpb.TypedValue_DoubleValue", + value: int64(46), + }, + { + name: "uint", + key: "key", + expected: "*monitoringpb.TypedValue_DoubleValue", + value: uint64(46), + }, + { + name: "numeric string", + key: "key", + expected: "*monitoringpb.TypedValue_DoubleValue", + value: "3.2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + typedValue, err := s.getStackdriverTypedValue(tt.value) + require.NoError(t, err) + require.Equal(t, tt.expected, reflect.TypeOf(typedValue.Value).String()) + }) + } +} + +func TestStackdriverMetricNamePath(t *testing.T) { + s := &Stackdriver{ + Namespace: "namespace", + MetricTypePrefix: "foo", + MetricNameFormat: "path", + } + m := testutil.MustMetric("uptime", + map[string]string{ + "foo": "bar", + }, + map[string]interface{}{ + "value": 42, + }, + time.Now(), + telegraf.Gauge, + ) + require.Equal(t, "foo/namespace/uptime/key", s.generateMetricName(m, "key")) +} + +func TestStackdriverMetricNameOfficial(t *testing.T) { + s := &Stackdriver{ + Namespace: "namespace", + MetricTypePrefix: "prometheus.googleapis.com", + MetricNameFormat: "official", + } + + tests := []struct { + name string + key string + expected string + metric telegraf.Metric + }{ + { + name: "gauge", + key: "key", + expected: "prometheus.googleapis.com/namespace_uptime_key/gauge", + metric: metric.New( + "uptime", + map[string]string{}, + map[string]interface{}{ + "value": 42, + }, + time.Now(), + telegraf.Gauge, + ), + }, + { + name: "untyped", + key: "key", + expected: "prometheus.googleapis.com/namespace_uptime_key/unknown", + metric: metric.New( + "uptime", + map[string]string{}, + map[string]interface{}{ + "value": 42, + }, + time.Now(), + telegraf.Untyped, + ), + }, + { + name: "histogram", + key: "key", + expected: "prometheus.googleapis.com/namespace_uptime_key/histogram", + metric: metric.New( + "uptime", + map[string]string{}, + map[string]interface{}{ + "value": 42, + }, + time.Now(), + telegraf.Histogram, + ), + }, + { + name: "counter", + key: "key", + expected: "prometheus.googleapis.com/namespace_uptime_key/counter", + metric: metric.New( + "uptime", + map[string]string{}, + map[string]interface{}{ + "value": 42, + }, + time.Now(), + telegraf.Counter, + ), + }, + { + name: "summary", + key: "key", + expected: "prometheus.googleapis.com/namespace_uptime_key", + metric: metric.New( + "uptime", + map[string]string{}, + map[string]interface{}{ + "value": 42, + }, + time.Now(), + telegraf.Summary, + ), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, s.generateMetricName(tt.metric, tt.key)) + }) + } +} + +func TestStackdriverValueInvalid(t *testing.T) { + s := &Stackdriver{ + MetricDataType: "foobar", + } + require.Error(t, s.Init()) +} + +func TestStackdriverMetricNameInvalid(t *testing.T) { + s := &Stackdriver{ + MetricNameFormat: "foobar", + } + require.Error(t, s.Init()) +}