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

feat(outputs.dynatrace): add support for metric to be treated and reported as a delta counter using regular expression #15668

Merged
merged 9 commits into from
Jul 26, 2024
23 changes: 20 additions & 3 deletions plugins/outputs/dynatrace/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand Down
41 changes: 30 additions & 11 deletions plugins/outputs/dynatrace/dynatrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
}
110 changes: 110 additions & 0 deletions plugins/outputs/dynatrace/dynatrace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions plugins/outputs/dynatrace/sample.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading