diff --git a/plugins/outputs/dynatrace/README.md b/plugins/outputs/dynatrace/README.md index 2f0fe3d48d8ee..3bda9dab8a49a 100644 --- a/plugins/outputs/dynatrace/README.md +++ b/plugins/outputs/dynatrace/README.md @@ -6,9 +6,10 @@ 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][docs]. 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][proto-docs] for details on the types defined -there. +`additional_counters` or `additional_counters_patterns` config option +(see below). +See the [Dynatrace Metrics ingestion protocol documentation][proto-docs] +for details on the types defined there. [api-v2]: https://docs.dynatrace.com/docs/shortlink/api-metrics-v2 @@ -144,6 +145,10 @@ to use them. ## If you want metrics to be treated and reported as delta counters, add the metric names here additional_counters = [ ] + ## In addition or as an alternative to additional_counters, if you want metrics to be treated and + ## reported as delta counters using regular expression pattern matching + additional_counters_patterns = [ ] + ## NOTE: Due to the way TOML is parsed, tables must be at the END of the ## plugin definition, otherwise additional config options are read as part of ## the table @@ -216,6 +221,18 @@ to this list. additional_counters = [ ] ``` +### `additional_counters_patterns` + +*required*: `false` + +In addition or as an alternative to additional_counters, if you want a metric +to be treated and reported as a delta counter using regular expression, +add its pattern to this list. + +```toml +additional_counters_patterns = [ ] +``` + ### `default_dimensions` *required*: `false` diff --git a/plugins/outputs/dynatrace/dynatrace.go b/plugins/outputs/dynatrace/dynatrace.go index ade9be8ca900e..357e9299a4e39 100644 --- a/plugins/outputs/dynatrace/dynatrace.go +++ b/plugins/outputs/dynatrace/dynatrace.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "regexp" "strings" "time" @@ -26,12 +27,14 @@ var sampleConfig string // Dynatrace Configuration for the Dynatrace output plugin type Dynatrace struct { - URL string `toml:"url"` - APIToken config.Secret `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 config.Secret `toml:"api_token"` + Prefix string `toml:"prefix"` + Log telegraf.Logger `toml:"-"` + Timeout config.Duration `toml:"timeout"` + AddCounterMetrics []string `toml:"additional_counters"` + AddCounterMetricsPatterns []string `toml:"additional_counters_patterns"` + DefaultDimensions map[string]string `toml:"default_dimensions"` normalizedDefaultDimensions dimensions.NormalizedDimensionList @@ -229,10 +232,8 @@ func init() { 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 - } + if d.isCounterMetricsMatch(d.AddCounterMetrics, metricName) || + d.isCounterMetricsPatternsMatch(d.AddCounterMetricsPatterns, metricName) { switch v := field.Value.(type) { case float64: return dtMetric.WithFloatCounterValueDelta(v) @@ -244,7 +245,6 @@ func (d *Dynatrace) getTypeOption(metric telegraf.Metric, field *telegraf.Field) return nil } } - switch v := field.Value.(type) { case float64: return dtMetric.WithFloatGaugeValue(v) @@ -261,3 +261,22 @@ func (d *Dynatrace) getTypeOption(metric telegraf.Metric, field *telegraf.Field) return nil } + +func (d *Dynatrace) isCounterMetricsMatch(counterMetrics []string, metricName string) bool { + for _, i := range counterMetrics { + if i == metricName { + return true + } + } + return false +} + +func (d *Dynatrace) isCounterMetricsPatternsMatch(counterPatterns []string, metricName string) bool { + for _, pattern := range counterPatterns { + regex, err := regexp.Compile(pattern) + if err == nil && regex.MatchString(metricName) { + return true + } + } + return false +} diff --git a/plugins/outputs/dynatrace/dynatrace_test.go b/plugins/outputs/dynatrace/dynatrace_test.go index 973524d335c47..ad95553d90798 100644 --- a/plugins/outputs/dynatrace/dynatrace_test.go +++ b/plugins/outputs/dynatrace/dynatrace_test.go @@ -213,6 +213,116 @@ func TestSendMetrics(t *testing.T) { require.NoError(t, err) } +func TestSendMetricsWithPatterns(t *testing.T) { + expected := []string{} + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // check the encoded result + bodyBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + bodyString := string(bodyBytes) + + 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(fmt.Sprintf(`{"linesOk":%d,"linesInvalid":0,"error":null}`, len(lines))) + require.NoError(t, err) + })) + defer ts.Close() + + d := &Dynatrace{ + URL: ts.URL, + APIToken: config.NewSecret([]byte("123")), + Log: testutil.Logger{}, + AddCounterMetrics: []string{}, + AddCounterMetricsPatterns: []string{}, + } + + err := d.Init() + require.NoError(t, err) + err = d.Connect() + require.NoError(t, err) + + // Init metrics + + // Simple metrics are exported as a gauge unless pattern match in additional_counters_patterns + expected = append(expected, + "simple_abc_metric.value,dt.metrics.source=telegraf gauge,3.14 1289430000000", + "simple_abc_metric.counter,dt.metrics.source=telegraf count,delta=5 1289430000000", + "simple_xyz_metric.value,dt.metrics.source=telegraf gauge,3.14 1289430000000", + "simple_xyz_metric.counter,dt.metrics.source=telegraf count,delta=5 1289430000000", + ) + // Add pattern to match all metrics that match simple_[a-z]+_metric.counter + d.AddCounterMetricsPatterns = append(d.AddCounterMetricsPatterns, "simple_[a-z]+_metric.counter") + + m1 := metric.New( + "simple_abc_metric", + map[string]string{}, + map[string]interface{}{"value": float64(3.14), "counter": 5}, + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC), + ) + + m2 := metric.New( + "simple_xyz_metric", + map[string]string{}, + 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 pattern match with additional_counters_patterns + expected = append(expected, + "counter_fan01_type.value,dt.metrics.source=telegraf gauge,3.14 1289430000000", + "counter_fan01_type.counter,dt.metrics.source=telegraf count,delta=5 1289430000000", + "counter_fanNaN_type.counter,dt.metrics.source=telegraf gauge,5 1289430000000", + "counter_fanNaN_type.value,dt.metrics.source=telegraf gauge,3.14 1289430000000", + ) + d.AddCounterMetricsPatterns = append(d.AddCounterMetricsPatterns, "counter_fan[0-9]+_type.counter") + m3 := metric.New( + "counter_fan01_type", + map[string]string{}, + map[string]interface{}{"value": float64(3.14), "counter": 5}, + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC), + telegraf.Counter, + ) + + m4 := metric.New( + "counter_fanNaN_type", + map[string]string{}, + map[string]interface{}{"value": float64(3.14), "counter": 5}, + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC), + telegraf.Counter, + ) + + expected = append(expected, + "complex_metric.int,dt.metrics.source=telegraf gauge,1 1289430000000", + "complex_metric.int64,dt.metrics.source=telegraf gauge,2 1289430000000", + "complex_metric.float,dt.metrics.source=telegraf gauge,3 1289430000000", + "complex_metric.float64,dt.metrics.source=telegraf gauge,4 1289430000000", + "complex_metric.true,dt.metrics.source=telegraf gauge,1 1289430000000", + "complex_metric.false,dt.metrics.source=telegraf gauge,0 1289430000000", + ) + + m5 := 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, m4, m5} + + err = d.Write(metrics) + require.NoError(t, err) +} + func TestSendSingleMetricWithUnorderedTags(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // check the encoded result diff --git a/plugins/outputs/dynatrace/sample.conf b/plugins/outputs/dynatrace/sample.conf index ff1eb1b2c2a1f..b940a3b757a07 100644 --- a/plugins/outputs/dynatrace/sample.conf +++ b/plugins/outputs/dynatrace/sample.conf @@ -31,6 +31,10 @@ ## If you want metrics to be treated and reported as delta counters, add the metric names here additional_counters = [ ] + ## In addition or as an alternative to additional_counters, if you want metrics to be treated and + ## reported as delta counters using regular expression pattern matching + additional_counters_patterns = [ ] + ## NOTE: Due to the way TOML is parsed, tables must be at the END of the ## plugin definition, otherwise additional config options are read as part of ## the table