From 95ef67445668010841a6ed70140fded0b472cd94 Mon Sep 17 00:00:00 2001 From: Daniel Dyla Date: Wed, 8 Sep 2021 14:31:42 -0400 Subject: [PATCH] feat(dynatrace-output): remove special handling from counters (#9675) Co-authored-by: Armin Ruech --- go.mod | 2 +- go.sum | 4 +- plugins/outputs/dynatrace/README.md | 8 +- plugins/outputs/dynatrace/dynatrace.go | 24 +++-- plugins/outputs/dynatrace/dynatrace_test.go | 102 +++++++++----------- 5 files changed, 67 insertions(+), 73 deletions(-) diff --git a/go.mod b/go.mod index 8dd6c8f7a..c133b72dd 100644 --- a/go.mod +++ b/go.mod @@ -85,7 +85,7 @@ require ( github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/doclambda/protobufquery v0.0.0-20210317203640-88ffabe06a60 - github.com/dynatrace-oss/dynatrace-metric-utils-go v0.2.0 + github.com/dynatrace-oss/dynatrace-metric-utils-go v0.3.0 github.com/eapache/go-resiliency v1.2.0 // indirect github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect github.com/eapache/queue v1.1.0 // indirect diff --git a/go.sum b/go.sum index 1d373bad3..01266f3e9 100644 --- a/go.sum +++ b/go.sum @@ -507,8 +507,8 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dynatrace-oss/dynatrace-metric-utils-go v0.2.0 h1:TEG5Jj7RYM2JBCUH3nLqCmSZy6srnaefvXxjUTCuHyA= -github.com/dynatrace-oss/dynatrace-metric-utils-go v0.2.0/go.mod h1:qw0E9EJ0PnSlhWawDNuqE0zhc1hqOBUCFIAj3dd9DNw= +github.com/dynatrace-oss/dynatrace-metric-utils-go v0.3.0 h1:q2Ayh9s6Cr75bS5URiOUAoyFXemgKQaBJphbhAaJHCY= +github.com/dynatrace-oss/dynatrace-metric-utils-go v0.3.0/go.mod h1:qw0E9EJ0PnSlhWawDNuqE0zhc1hqOBUCFIAj3dd9DNw= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-resiliency v1.2.0 h1:v7g92e/KSN71Rq7vSThKaWIq68fL4YHvWyiUKorFR1Q= github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= diff --git a/plugins/outputs/dynatrace/README.md b/plugins/outputs/dynatrace/README.md index 666f821f6..f25b87089 100644 --- a/plugins/outputs/dynatrace/README.md +++ b/plugins/outputs/dynatrace/README.md @@ -2,10 +2,12 @@ This plugin sends Telegraf metrics to [Dynatrace](https://www.dynatrace.com) via the [Dynatrace Metrics API V2](https://www.dynatrace.com/support/help/dynatrace-api/environment-api/metric-v2/). It may be run alongside the Dynatrace OneAgent for automatic authentication or it may be run standalone on a host without a OneAgent by specifying a URL and API Token. More information on the plugin can be found in the [Dynatrace documentation](https://www.dynatrace.com/support/help/how-to-use-dynatrace/metrics/metric-ingestion/ingestion-methods/telegraf/). +All metrics are reported as gauges, unless they are specified to be delta counters using the `additional_counters` config option (see below). +See the [Dynatrace Metrics ingestion protocol documentation](https://www.dynatrace.com/support/help/how-to-use-dynatrace/metrics/metric-ingestion/metric-ingestion-protocol) for details on the types defined there. ## Requirements -You will either need a Dynatrace OneAgent (version 1.201 or higher) installed on the same host as Telegraf; or a Dynatrace environment with version 1.202 or higher. Monotonic counters (e.g. `diskio.reads`, `system.uptime`) require Dynatrace 208 or later. +You will either need a Dynatrace OneAgent (version 1.201 or higher) installed on the same host as Telegraf; or a Dynatrace environment with version 1.202 or higher. - Telegraf minimum version: Telegraf 1.16 @@ -65,7 +67,7 @@ You can learn more about how to use the Dynatrace API [here](https://www.dynatra prefix = "telegraf" ## Flag for skipping the tls certificate check, just for testing purposes, should be false by default insecure_skip_verify = false - ## If you want to convert values represented as gauges to counters, add the metric names here + ## If you want metrics to be treated and reported as delta counters, add the metric names here additional_counters = [ ] ## Optional dimensions to be added to every metric @@ -119,7 +121,7 @@ insecure_skip_verify = false *required*: `false` -If you want to convert values represented as gauges to counters, add the metric names here. +If you want a metric to be treated and reported as a delta counter, add its name to this list. ```toml additional_counters = [ ] diff --git a/plugins/outputs/dynatrace/dynatrace.go b/plugins/outputs/dynatrace/dynatrace.go index 470eb0e2c..11796e8e1 100644 --- a/plugins/outputs/dynatrace/dynatrace.go +++ b/plugins/outputs/dynatrace/dynatrace.go @@ -69,7 +69,7 @@ const sampleConfig = ` ## Connection timeout, defaults to "5s" if not set. timeout = "5s" - ## If you want to convert values represented as gauges to counters, add the metric names here + ## If you want metrics to be treated and reported as delta counters, add the metric names here additional_counters = [ ] ## Optional dimensions to be added to every metric @@ -122,16 +122,10 @@ func (d *Dynatrace) Write(metrics []telegraf.Metric) error { dims = append(dims, dimensions.NewDimension(tag.Key, tag.Value)) } - metricType := tm.Type() for _, field := range tm.FieldList() { metricName := tm.Name() + "." + field.Key - for _, i := range d.AddCounterMetrics { - if metricName == i { - metricType = telegraf.Counter - } - } - typeOpt := getTypeOption(metricType, field) + typeOpt := d.getTypeOption(tm, field) if typeOpt == nil { // Unsupported type. Log only once per unsupported metric name @@ -267,15 +261,19 @@ func init() { }) } -func getTypeOption(metricType telegraf.ValueType, field *telegraf.Field) dtMetric.MetricOption { - if metricType == telegraf.Counter { +func (d *Dynatrace) getTypeOption(metric telegraf.Metric, field *telegraf.Field) dtMetric.MetricOption { + metricName := metric.Name() + "." + field.Key + for _, i := range d.AddCounterMetrics { + if metricName != i { + continue + } switch v := field.Value.(type) { case float64: - return dtMetric.WithFloatCounterValueTotal(v) + return dtMetric.WithFloatCounterValueDelta(v) case uint64: - return dtMetric.WithIntCounterValueTotal(int64(v)) + return dtMetric.WithIntCounterValueDelta(int64(v)) case int64: - return dtMetric.WithIntCounterValueTotal(v) + return dtMetric.WithIntCounterValueDelta(v) default: return nil } diff --git a/plugins/outputs/dynatrace/dynatrace_test.go b/plugins/outputs/dynatrace/dynatrace_test.go index 65cd3d2a8..c3cb091cb 100644 --- a/plugins/outputs/dynatrace/dynatrace_test.go +++ b/plugins/outputs/dynatrace/dynatrace_test.go @@ -2,10 +2,13 @@ package dynatrace import ( "encoding/json" + "fmt" "io/ioutil" "net/http" "net/http/httptest" "regexp" + "sort" + "strings" "testing" "time" @@ -123,26 +126,37 @@ func TestMissingAPIToken(t *testing.T) { } func TestSendMetrics(t *testing.T) { + expected := []string{} + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // check the encoded result bodyBytes, err := ioutil.ReadAll(r.Body) require.NoError(t, err) bodyString := string(bodyBytes) - expected := "mymeasurement.myfield,dt.metrics.source=telegraf gauge,3.14 1289430000000\nmymeasurement.value,dt.metrics.source=telegraf count,3.14 1289430000000" - if bodyString != expected { - t.Errorf("Metric encoding failed. expected: %#v but got: %#v", expected, bodyString) + + lines := strings.Split(bodyString, "\n") + + sort.Strings(lines) + sort.Strings(expected) + + expectedString := strings.Join(expected, "\n") + foundString := strings.Join(lines, "\n") + if foundString != expectedString { + t.Errorf("Metric encoding failed. expected: %#v but got: %#v", expectedString, foundString) } w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(`{"linesOk":10,"linesInvalid":0,"error":null}`) + err = json.NewEncoder(w).Encode(fmt.Sprintf(`{"linesOk":%d,"linesInvalid":0,"error":null}`, len(lines))) require.NoError(t, err) })) defer ts.Close() - d := &Dynatrace{} + d := &Dynatrace{ + URL: ts.URL, + APIToken: "123", + Log: testutil.Logger{}, + AddCounterMetrics: []string{}, + } - d.URL = ts.URL - d.APIToken = "123" - d.Log = testutil.Logger{} err := d.Init() require.NoError(t, err) err = d.Connect() @@ -150,22 +164,43 @@ func TestSendMetrics(t *testing.T) { // Init metrics + // Simple metrics are exported as a gauge unless in additional_counters + expected = append(expected, "simple_metric.value,dt.metrics.source=telegraf gauge,3.14 1289430000000") + expected = append(expected, "simple_metric.counter,dt.metrics.source=telegraf count,delta=5 1289430000000") + d.AddCounterMetrics = append(d.AddCounterMetrics, "simple_metric.counter") m1 := metric.New( - "mymeasurement", + "simple_metric", map[string]string{}, - map[string]interface{}{"myfield": float64(3.14)}, + map[string]interface{}{"value": float64(3.14), "counter": 5}, time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC), ) + // Even if Type() returns counter, all metrics are treated as a gauge unless explicitly added to additional_counters + expected = append(expected, "counter_type.value,dt.metrics.source=telegraf gauge,3.14 1289430000000") + expected = append(expected, "counter_type.counter,dt.metrics.source=telegraf count,delta=5 1289430000000") + d.AddCounterMetrics = append(d.AddCounterMetrics, "counter_type.counter") m2 := metric.New( - "mymeasurement", + "counter_type", map[string]string{}, - map[string]interface{}{"value": float64(3.14)}, + map[string]interface{}{"value": float64(3.14), "counter": 5}, time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC), telegraf.Counter, ) - metrics := []telegraf.Metric{m1, m2} + expected = append(expected, "complex_metric.int,dt.metrics.source=telegraf gauge,1 1289430000000") + expected = append(expected, "complex_metric.int64,dt.metrics.source=telegraf gauge,2 1289430000000") + expected = append(expected, "complex_metric.float,dt.metrics.source=telegraf gauge,3 1289430000000") + expected = append(expected, "complex_metric.float64,dt.metrics.source=telegraf gauge,4 1289430000000") + expected = append(expected, "complex_metric.true,dt.metrics.source=telegraf gauge,1 1289430000000") + expected = append(expected, "complex_metric.false,dt.metrics.source=telegraf gauge,0 1289430000000") + m3 := metric.New( + "complex_metric", + map[string]string{}, + map[string]interface{}{"int": 1, "int64": int64(2), "float": 3.0, "float64": float64(4.0), "true": true, "false": false}, + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC), + ) + + metrics := []telegraf.Metric{m1, m2, m3} err = d.Write(metrics) require.NoError(t, err) @@ -475,47 +510,6 @@ func TestStaticDimensionsOverrideMetric(t *testing.T) { require.NoError(t, err) } -func TestSendCounterMetricWithoutTags(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - // check the encoded result - bodyBytes, err := ioutil.ReadAll(r.Body) - require.NoError(t, err) - bodyString := string(bodyBytes) - expected := "mymeasurement.value,dt.metrics.source=telegraf gauge,32 1289430000000" - if bodyString != expected { - t.Errorf("Metric encoding failed. expected: %#v but got: %#v", expected, bodyString) - } - err = json.NewEncoder(w).Encode(`{"linesOk":1,"linesInvalid":0,"error":null}`) - require.NoError(t, err) - })) - defer ts.Close() - - d := &Dynatrace{} - - d.URL = ts.URL - d.APIToken = "123" - d.Log = testutil.Logger{} - err := d.Init() - require.NoError(t, err) - err = d.Connect() - require.NoError(t, err) - - // Init metrics - - m1 := metric.New( - "mymeasurement", - map[string]string{}, - map[string]interface{}{"value": 32}, - time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC), - ) - - metrics := []telegraf.Metric{m1} - - err = d.Write(metrics) - require.NoError(t, err) -} - var warnfCalledTimes int type loggerStub struct {