Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(outputs.stackdriver): Options to use official path and types #13454

Merged
merged 8 commits into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions plugins/outputs/stackdriver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand All @@ -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"

Expand Down
15 changes: 15 additions & 0 deletions plugins/outputs/stackdriver/sample.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
88 changes: 84 additions & 4 deletions plugins/outputs/stackdriver/stackdriver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand All @@ -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 == "" {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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)
}
}
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
Loading