Update dynatrace output (#9363)

- export timestamps
- enrich dimensions with OneAgent data
- Add default dimensions feature
This commit is contained in:
Daniel Dyla 2021-06-15 17:13:34 -04:00 committed by GitHub
parent d7afebf7e6
commit 769f582245
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 191 additions and 31 deletions

View File

@ -16,7 +16,7 @@ The Dynatrace exporter may be enabled by adding an `[[outputs.dynatrace]]` secti
All configurations are optional, but if a `url` other than the OneAgent metric ingestion endpoint is specified then an `api_token` is required.
To see all available options, see [Configuration](#configuration) below.
### Running alongside Dynatrace OneAgent
### Running alongside Dynatrace OneAgent (preferred)
If you run the Telegraf agent on a host or VM that is monitored by the Dynatrace OneAgent then you only need to enable the plugin, but need no further configuration. The Dynatrace Telegraf output plugin will send all metrics to the OneAgent which will use its secure and load balanced connection to send the metrics to your Dynatrace SaaS or Managed environment.
Depending on your environment, you might have to enable metrics ingestion on the OneAgent first as described in the [Dynatrace documentation](https://www.dynatrace.com/support/help/how-to-use-dynatrace/metrics/metric-ingestion/ingestion-methods/telegraf/).
@ -28,7 +28,7 @@ Note: The name and identifier of the host running Telegraf will be added as a di
## No options are required. By default, metrics will be exported via the OneAgent on the local host.
```
## Running standalone
### Running standalone
If you run the Telegraf agent on a host or VM without a OneAgent you will need to configure the environment API endpoint to send the metrics to and an API token for security.
@ -55,14 +55,6 @@ You can learn more about how to use the Dynatrace API [here](https://www.dynatra
## Configuration
### `url`
*required*: `false`
*default*: Local OneAgent endpoint
Set your Dynatrace environment URL (e.g.: `https://{your-environment-id}.live.dynatrace.com/api/v2/metrics/ingest`) if you do not use a OneAgent or wish to export metrics directly to a Dynatrace metrics v2 endpoint. If a URL is set to anything other than the local OneAgent endpoint, then an API token is required.
```toml
[[outputs.dynatrace]]
## Leave empty or use the local ingest endpoint of your OneAgent monitored host (e.g.: http://127.0.0.1:14499/metrics/ingest).
@ -75,6 +67,21 @@ Set your Dynatrace environment URL (e.g.: `https://{your-environment-id}.live.dy
insecure_skip_verify = false
## If you want to convert values represented as gauges to counters, add the metric names here
additional_counters = [ ]
## Optional dimensions to be added to every metric
[outputs.dynatrace.default_dimensions]
default_key = "default value"
```
### `url`
*required*: `false`
*default*: Local OneAgent endpoint
Set your Dynatrace environment URL (e.g.: `https://{your-environment-id}.live.dynatrace.com/api/v2/metrics/ingest`, see the [Dynatrace documentation](https://www.dynatrace.com/support/help/dynatrace-api/environment-api/metric-v2/post-ingest-metrics/) for details) if you do not use a OneAgent or wish to export metrics directly to a Dynatrace metrics v2 endpoint. If a URL is set to anything other than the local OneAgent endpoint, then an API token is required.
```toml
url = "https://{your-environment-id}.live.dynatrace.com/api/v2/metrics/ingest"
```
@ -125,9 +132,8 @@ additional_counters = [ ]
Default dimensions that will be added to every exported metric.
```toml
default_dimensions = {
key = "value"
}
[outputs.dynatrace.default_dimensions]
default_key = "default value"
```
## Limitations

View File

@ -20,12 +20,16 @@ import (
// Dynatrace Configuration for the Dynatrace output plugin
type Dynatrace struct {
URL string `toml:"url"`
APIToken string `toml:"api_token"`
Prefix string `toml:"prefix"`
Log telegraf.Logger `toml:"-"`
Timeout config.Duration `toml:"timeout"`
AddCounterMetrics []string `toml:"additional_counters"`
URL string `toml:"url"`
APIToken string `toml:"api_token"`
Prefix string `toml:"prefix"`
Log telegraf.Logger `toml:"-"`
Timeout config.Duration `toml:"timeout"`
AddCounterMetrics []string `toml:"additional_counters"`
DefaultDimensions map[string]string `toml:"default_dimensions"`
normalizedDefaultDimensions dimensions.NormalizedDimensionList
normalizedStaticDimensions dimensions.NormalizedDimensionList
tls.ClientConfig
@ -67,6 +71,10 @@ const sampleConfig = `
## If you want to convert values represented as gauges to counters, add the metric names here
additional_counters = [ ]
## Optional dimensions to be added to every metric
# [outputs.dynatrace.default_dimensions]
# default_key = "default value"
`
// Connect Connects the Dynatrace output plugin to the Telegraf stream
@ -140,10 +148,12 @@ func (d *Dynatrace) Write(metrics []telegraf.Metric) error {
dtMetric.WithPrefix(d.Prefix),
dtMetric.WithDimensions(
dimensions.MergeLists(
// dimensions.NewNormalizedDimensionList(e.opts.DefaultDimensions...),
d.normalizedDefaultDimensions,
dimensions.NewNormalizedDimensionList(dims...),
d.normalizedStaticDimensions,
),
),
dtMetric.WithTimestamp(tm.Time()),
typeOpt,
)
@ -236,6 +246,14 @@ func (d *Dynatrace) Init() error {
},
Timeout: time.Duration(d.Timeout),
}
dims := []dimensions.Dimension{}
for key, value := range d.DefaultDimensions {
dims = append(dims, dimensions.NewDimension(key, value))
}
d.normalizedDefaultDimensions = dimensions.NewNormalizedDimensionList(dims...)
d.normalizedStaticDimensions = dimensions.NewNormalizedDimensionList(dimensions.NewDimension("dt.metrics.source", "telegraf"))
return nil
}

View File

@ -10,6 +10,7 @@ import (
"time"
"github.com/dynatrace-oss/dynatrace-metric-utils-go/metric/apiconstants"
"github.com/dynatrace-oss/dynatrace-metric-utils-go/metric/dimensions"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/metric"
@ -121,13 +122,13 @@ func TestMissingAPIToken(t *testing.T) {
require.Error(t, err)
}
func TestSendMetric(t *testing.T) {
func TestSendMetrics(t *testing.T) {
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,host=192.168.0.1 gauge,3.14\nmymeasurement.value,host=192.168.0.2 count,3.14"
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)
}
@ -151,14 +152,14 @@ func TestSendMetric(t *testing.T) {
m1 := metric.New(
"mymeasurement",
map[string]string{"host": "192.168.0.1"},
map[string]string{},
map[string]interface{}{"myfield": float64(3.14)},
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC),
)
m2 := metric.New(
"mymeasurement",
map[string]string{"host": "192.168.0.2"},
map[string]string{},
map[string]interface{}{"value": float64(3.14)},
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC),
telegraf.Counter,
@ -176,11 +177,14 @@ func TestSendSingleMetricWithUnorderedTags(t *testing.T) {
bodyBytes, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
bodyString := string(bodyBytes)
// use regex because dimension order isn't guaranteed
require.Equal(t, len(bodyString), 94)
require.Regexp(t, regexp.MustCompile(`^mymeasurement\.myfield`), bodyString)
require.Regexp(t, regexp.MustCompile(`a=test`), bodyString)
require.Regexp(t, regexp.MustCompile(`b=test`), bodyString)
require.Regexp(t, regexp.MustCompile(`c=test`), bodyString)
require.Regexp(t, regexp.MustCompile(`gauge,3.14$`), bodyString)
require.Regexp(t, regexp.MustCompile(`dt.metrics.source=telegraf`), bodyString)
require.Regexp(t, regexp.MustCompile(`gauge,3.14 1289430000000$`), bodyString)
w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(`{"linesOk":1,"linesInvalid":0,"error":null}`)
require.NoError(t, err)
@ -219,7 +223,7 @@ func TestSendMetricWithoutTags(t *testing.T) {
bodyBytes, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
bodyString := string(bodyBytes)
expected := "mymeasurement.myfield gauge,3.14"
expected := "mymeasurement.myfield,dt.metrics.source=telegraf gauge,3.14 1289430000000"
if bodyString != expected {
t.Errorf("Metric encoding failed. expected: %#v but got: %#v", expected, bodyString)
}
@ -261,13 +265,14 @@ func TestSendMetricWithUpperCaseTagKeys(t *testing.T) {
require.NoError(t, err)
bodyString := string(bodyBytes)
// expected := "mymeasurement.myfield,b_b=test,ccc=test,aaa=test gauge,3.14"
// use regex because dimension order isn't guaranteed
require.Equal(t, len(bodyString), 100)
require.Regexp(t, regexp.MustCompile(`^mymeasurement\.myfield`), bodyString)
require.Regexp(t, regexp.MustCompile(`aaa=test`), bodyString)
require.Regexp(t, regexp.MustCompile(`b_b=test`), bodyString)
require.Regexp(t, regexp.MustCompile(`ccc=test`), bodyString)
require.Regexp(t, regexp.MustCompile(`gauge,3.14$`), bodyString)
require.Regexp(t, regexp.MustCompile(`dt.metrics.source=telegraf`), bodyString)
require.Regexp(t, regexp.MustCompile(`gauge,3.14 1289430000000$`), bodyString)
err = json.NewEncoder(w).Encode(`{"linesOk":1,"linesInvalid":0,"error":null}`)
require.NoError(t, err)
@ -307,8 +312,9 @@ func TestSendBooleanMetricWithoutTags(t *testing.T) {
require.NoError(t, err)
bodyString := string(bodyBytes)
// use regex because field order isn't guaranteed
require.Contains(t, bodyString, "mymeasurement.yes gauge,1")
require.Contains(t, bodyString, "mymeasurement.no gauge,0")
require.Equal(t, len(bodyString), 132)
require.Contains(t, bodyString, "mymeasurement.yes,dt.metrics.source=telegraf gauge,1 1289430000000")
require.Contains(t, bodyString, "mymeasurement.no,dt.metrics.source=telegraf gauge,0 1289430000000")
err = json.NewEncoder(w).Encode(`{"linesOk":1,"linesInvalid":0,"error":null}`)
require.NoError(t, err)
}))
@ -339,6 +345,136 @@ func TestSendBooleanMetricWithoutTags(t *testing.T) {
require.NoError(t, err)
}
func TestSendMetricWithDefaultDimensions(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)
// use regex because field order isn't guaranteed
require.Equal(t, len(bodyString), 79)
require.Regexp(t, regexp.MustCompile("^mymeasurement.value"), bodyString)
require.Regexp(t, regexp.MustCompile("dt.metrics.source=telegraf"), bodyString)
require.Regexp(t, regexp.MustCompile("dim=value"), bodyString)
require.Regexp(t, regexp.MustCompile("gauge,32 1289430000000$"), bodyString)
err = json.NewEncoder(w).Encode(`{"linesOk":1,"linesInvalid":0,"error":null}`)
require.NoError(t, err)
}))
defer ts.Close()
d := &Dynatrace{DefaultDimensions: map[string]string{"dim": "value"}}
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)
}
func TestMetricDimensionsOverrideDefault(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)
// use regex because field order isn't guaranteed
require.Equal(t, len(bodyString), 80)
require.Regexp(t, regexp.MustCompile("^mymeasurement.value"), bodyString)
require.Regexp(t, regexp.MustCompile("dt.metrics.source=telegraf"), bodyString)
require.Regexp(t, regexp.MustCompile("dim=metric"), bodyString)
require.Regexp(t, regexp.MustCompile("gauge,32 1289430000000$"), bodyString)
err = json.NewEncoder(w).Encode(`{"linesOk":1,"linesInvalid":0,"error":null}`)
require.NoError(t, err)
}))
defer ts.Close()
d := &Dynatrace{DefaultDimensions: map[string]string{"dim": "default"}}
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{"dim": "metric"},
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)
}
func TestStaticDimensionsOverrideMetric(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)
// use regex because field order isn't guaranteed
require.Equal(t, len(bodyString), 53)
require.Regexp(t, regexp.MustCompile("^mymeasurement.value"), bodyString)
require.Regexp(t, regexp.MustCompile("dim=static"), bodyString)
require.Regexp(t, regexp.MustCompile("gauge,32 1289430000000$"), bodyString)
err = json.NewEncoder(w).Encode(`{"linesOk":1,"linesInvalid":0,"error":null}`)
require.NoError(t, err)
}))
defer ts.Close()
d := &Dynatrace{DefaultDimensions: map[string]string{"dim": "default"}}
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)
d.normalizedStaticDimensions = dimensions.NewNormalizedDimensionList(dimensions.NewDimension("dim", "static"))
// Init metrics
m1 := metric.New(
"mymeasurement",
map[string]string{"dim": "metric"},
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)
}
func TestSendCounterMetricWithoutTags(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
@ -346,7 +482,7 @@ func TestSendCounterMetricWithoutTags(t *testing.T) {
bodyBytes, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
bodyString := string(bodyBytes)
expected := "mymeasurement.value gauge,32"
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)
}