Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
256
plugins/outputs/dynatrace/README.md
Normal file
256
plugins/outputs/dynatrace/README.md
Normal file
|
@ -0,0 +1,256 @@
|
|||
# Dynatrace Output Plugin
|
||||
|
||||
This plugin writes metrics to [Dynatrace][dynatrace] via the
|
||||
[Dynatrace Metrics API V2][api-v2]. It may be run alongside the Dynatrace
|
||||
OneAgent for automatic authentication or it may be run standalone on a host
|
||||
without OneAgent by specifying a URL and API Token.
|
||||
|
||||
More information on the plugin can be found in the
|
||||
[Dynatrace documentation][docs].
|
||||
|
||||
> [!NOTE]
|
||||
> All metrics are reported as gauges, unless they are specified to be delta
|
||||
> counters using the `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.
|
||||
|
||||
⭐ Telegraf v1.16.0
|
||||
🏷️ cloud, datastore
|
||||
💻 all
|
||||
|
||||
[api-v2]: https://docs.dynatrace.com/docs/shortlink/api-metrics-v2
|
||||
[docs]: https://docs.dynatrace.com/docs/shortlink/telegraf
|
||||
[dynatrace]: https://www.dynatrace.com
|
||||
[proto-docs]: https://docs.dynatrace.com/docs/shortlink/metric-ingestion-protocol
|
||||
|
||||
## 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.
|
||||
|
||||
- Telegraf minimum version: Telegraf 1.16
|
||||
|
||||
## Getting Started
|
||||
|
||||
Setting up Telegraf is explained in the [Telegraf
|
||||
Documentation][getting-started].
|
||||
The Dynatrace exporter may be enabled by adding an `[[outputs.dynatrace]]`
|
||||
section to your `telegraf.conf` config file. 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.
|
||||
|
||||
[getting-started]: https://docs.influxdata.com/telegraf/latest/introduction/getting-started/
|
||||
|
||||
### 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][docs].
|
||||
|
||||
Note: The name and identifier of the host running Telegraf will be added as a
|
||||
dimension to every metric. If this is undesirable, then the output plugin may be
|
||||
used in standalone mode using the directions below.
|
||||
|
||||
```toml
|
||||
[[outputs.dynatrace]]
|
||||
## No options are required. By default, metrics will be exported via the OneAgent on the local host.
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
You will also need to configure an API token for secure access. Find out how to
|
||||
create a token in the [Dynatrace documentation][api-auth] or simply navigate to
|
||||
**Settings > Integration > Dynatrace API** in your Dynatrace environment and
|
||||
create a token with Dynatrace API and create a new token with 'Ingest metrics'
|
||||
(`metrics.ingest`) scope enabled. It is recommended to limit Token scope to only
|
||||
this permission.
|
||||
|
||||
The endpoint for the Dynatrace Metrics API v2 is
|
||||
|
||||
- on Dynatrace Managed:
|
||||
`https://{your-domain}/e/{your-environment-id}/api/v2/metrics/ingest`
|
||||
- on Dynatrace SaaS:
|
||||
`https://{your-environment-id}.live.dynatrace.com/api/v2/metrics/ingest`
|
||||
|
||||
```toml
|
||||
[[outputs.dynatrace]]
|
||||
## If no OneAgent is running on the host, url and api_token need to be set
|
||||
|
||||
## Dynatrace Metrics Ingest v2 endpoint to receive metrics
|
||||
url = "https://{your-environment-id}.live.dynatrace.com/api/v2/metrics/ingest"
|
||||
|
||||
## API token is required if a URL is specified and should be restricted to the 'Ingest metrics' scope
|
||||
api_token = "your API token here" // hard-coded for illustration only, should be read from environment
|
||||
```
|
||||
|
||||
You can learn more about how to use the Dynatrace API
|
||||
[here](https://docs.dynatrace.com/docs/shortlink/section-api).
|
||||
|
||||
[api-auth]: https://docs.dynatrace.com/docs/shortlink/api-authentication
|
||||
|
||||
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
|
||||
|
||||
In addition to the plugin-specific configuration settings, plugins support
|
||||
additional global and plugin configuration settings. These settings are used to
|
||||
modify metrics, tags, and field or create aliases and configure ordering, etc.
|
||||
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||
|
||||
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
|
||||
|
||||
## Secret-store support
|
||||
|
||||
This plugin supports secrets from secret-stores for the `api_token` option.
|
||||
See the [secret-store documentation][SECRETSTORE] for more details on how
|
||||
to use them.
|
||||
|
||||
[SECRETSTORE]: ../../../docs/CONFIGURATION.md#secret-store-secrets
|
||||
|
||||
## Configuration
|
||||
|
||||
```toml @sample.conf
|
||||
# Send telegraf metrics to a Dynatrace environment
|
||||
[[outputs.dynatrace]]
|
||||
## For usage with the Dynatrace OneAgent you can omit any configuration,
|
||||
## the only requirement is that the OneAgent is running on the same host.
|
||||
## Only setup environment url and token if you want to monitor a Host without the OneAgent present.
|
||||
##
|
||||
## Your Dynatrace environment URL.
|
||||
## For Dynatrace OneAgent you can leave this empty or set it to "http://127.0.0.1:14499/metrics/ingest" (default)
|
||||
## For Dynatrace SaaS environments the URL scheme is "https://{your-environment-id}.live.dynatrace.com/api/v2/metrics/ingest"
|
||||
## For Dynatrace Managed environments the URL scheme is "https://{your-domain}/e/{your-environment-id}/api/v2/metrics/ingest"
|
||||
url = ""
|
||||
|
||||
## Your Dynatrace API token.
|
||||
## Create an API token within your Dynatrace environment, by navigating to Settings > Integration > Dynatrace API
|
||||
## The API token needs data ingest scope permission. When using OneAgent, no API token is required.
|
||||
api_token = ""
|
||||
|
||||
## Optional prefix for metric names (e.g.: "telegraf")
|
||||
prefix = "telegraf"
|
||||
|
||||
## Optional TLS Config
|
||||
# tls_ca = "/etc/telegraf/ca.pem"
|
||||
# tls_cert = "/etc/telegraf/cert.pem"
|
||||
# tls_key = "/etc/telegraf/key.pem"
|
||||
## Optional flag for ignoring tls certificate check
|
||||
# insecure_skip_verify = false
|
||||
|
||||
## Connection timeout, defaults to "5s" if not set.
|
||||
timeout = "5s"
|
||||
|
||||
## 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
|
||||
|
||||
## 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][post-ingest] 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"
|
||||
```
|
||||
|
||||
[post-ingest]: https://docs.dynatrace.com/docs/shortlink/api-metrics-v2-post-datapoints
|
||||
|
||||
### `api_token`
|
||||
|
||||
*required*: `false` unless `url` is specified
|
||||
|
||||
API token is required if a URL other than the OneAgent endpoint is specified and
|
||||
it should be restricted to the 'Ingest metrics' scope.
|
||||
|
||||
```toml
|
||||
api_token = "your API token here"
|
||||
```
|
||||
|
||||
### `prefix`
|
||||
|
||||
*required*: `false`
|
||||
|
||||
Optional prefix to be prepended to all metric names (will be separated with a
|
||||
`.`).
|
||||
|
||||
```toml
|
||||
prefix = "telegraf"
|
||||
```
|
||||
|
||||
### `insecure_skip_verify`
|
||||
|
||||
*required*: `false`
|
||||
|
||||
Setting this option to true skips TLS verification for testing or when using
|
||||
self-signed certificates.
|
||||
|
||||
```toml
|
||||
insecure_skip_verify = false
|
||||
```
|
||||
|
||||
### `additional_counters`
|
||||
|
||||
*required*: `false`
|
||||
|
||||
If you want a metric to be treated and reported as a delta counter, add its name
|
||||
to this list.
|
||||
|
||||
```toml
|
||||
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`
|
||||
|
||||
Default dimensions that will be added to every exported metric.
|
||||
|
||||
```toml
|
||||
[outputs.dynatrace.default_dimensions]
|
||||
default_key = "default value"
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
Telegraf measurements which can't be converted to a number are skipped.
|
281
plugins/outputs/dynatrace/dynatrace.go
Normal file
281
plugins/outputs/dynatrace/dynatrace.go
Normal file
|
@ -0,0 +1,281 @@
|
|||
//go:generate ../../../tools/readme_config_includer/generator
|
||||
package dynatrace
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dynatrace_metric "github.com/dynatrace-oss/dynatrace-metric-utils-go/metric"
|
||||
"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/plugins/common/tls"
|
||||
"github.com/influxdata/telegraf/plugins/outputs"
|
||||
)
|
||||
|
||||
//go:embed sample.conf
|
||||
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"`
|
||||
AddCounterMetricsPatterns []string `toml:"additional_counters_patterns"`
|
||||
|
||||
DefaultDimensions map[string]string `toml:"default_dimensions"`
|
||||
|
||||
normalizedDefaultDimensions dimensions.NormalizedDimensionList
|
||||
normalizedStaticDimensions dimensions.NormalizedDimensionList
|
||||
|
||||
tls.ClientConfig
|
||||
|
||||
client *http.Client
|
||||
|
||||
loggedMetrics map[string]bool // New empty set
|
||||
}
|
||||
|
||||
func (*Dynatrace) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
// Connect Connects the Dynatrace output plugin to the Telegraf stream
|
||||
func (*Dynatrace) Connect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close Closes the Dynatrace output plugin
|
||||
func (d *Dynatrace) Close() error {
|
||||
d.client = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Dynatrace) Write(metrics []telegraf.Metric) error {
|
||||
if len(metrics) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
lines := make([]string, 0, len(metrics))
|
||||
for _, tm := range metrics {
|
||||
dims := make([]dimensions.Dimension, 0, len(tm.TagList()))
|
||||
for _, tag := range tm.TagList() {
|
||||
// Ignore special tags for histogram and summary types.
|
||||
switch tm.Type() {
|
||||
case telegraf.Histogram:
|
||||
if tag.Key == "le" || tag.Key == "gt" {
|
||||
continue
|
||||
}
|
||||
case telegraf.Summary:
|
||||
if tag.Key == "quantile" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
dims = append(dims, dimensions.NewDimension(tag.Key, tag.Value))
|
||||
}
|
||||
|
||||
for _, field := range tm.FieldList() {
|
||||
metricName := tm.Name() + "." + field.Key
|
||||
|
||||
typeOpt := d.getTypeOption(tm, field)
|
||||
|
||||
if typeOpt == nil {
|
||||
// Unsupported type. Log only once per unsupported metric name
|
||||
if !d.loggedMetrics[metricName] {
|
||||
d.Log.Warnf("Unsupported type for %s", metricName)
|
||||
d.loggedMetrics[metricName] = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
name := tm.Name() + "." + field.Key
|
||||
dm, err := dynatrace_metric.NewMetric(
|
||||
name,
|
||||
dynatrace_metric.WithPrefix(d.Prefix),
|
||||
dynatrace_metric.WithDimensions(
|
||||
dimensions.MergeLists(
|
||||
d.normalizedDefaultDimensions,
|
||||
dimensions.NewNormalizedDimensionList(dims...),
|
||||
d.normalizedStaticDimensions,
|
||||
),
|
||||
),
|
||||
dynatrace_metric.WithTimestamp(tm.Time()),
|
||||
typeOpt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
d.Log.Warn(fmt.Sprintf("failed to normalize metric: %s - %s", name, err.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
line, err := dm.Serialize()
|
||||
|
||||
if err != nil {
|
||||
d.Log.Warn(fmt.Sprintf("failed to serialize metric: %s - %s", name, err.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
|
||||
limit := apiconstants.GetPayloadLinesLimit()
|
||||
for i := 0; i < len(lines); i += limit {
|
||||
batch := lines[i:min(i+limit, len(lines))]
|
||||
|
||||
output := strings.Join(batch, "\n")
|
||||
if output != "" {
|
||||
if err := d.send(output); err != nil {
|
||||
return fmt.Errorf("error processing data: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Dynatrace) send(msg string) error {
|
||||
var err error
|
||||
req, err := http.NewRequest("POST", d.URL, bytes.NewBufferString(msg))
|
||||
if err != nil {
|
||||
d.Log.Errorf("Dynatrace error: %s", err.Error())
|
||||
return fmt.Errorf("error while creating HTTP request: %w", err)
|
||||
}
|
||||
req.Header.Add("Content-Type", "text/plain; charset=UTF-8")
|
||||
|
||||
if !d.APIToken.Empty() {
|
||||
token, err := d.APIToken.Get()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting token failed: %w", err)
|
||||
}
|
||||
req.Header.Add("Authorization", "Api-Token "+token.String())
|
||||
token.Destroy()
|
||||
}
|
||||
// add user-agent header to identify metric source
|
||||
req.Header.Add("User-Agent", "telegraf")
|
||||
|
||||
resp, err := d.client.Do(req)
|
||||
if err != nil {
|
||||
d.Log.Errorf("Dynatrace error: %s", err.Error())
|
||||
return fmt.Errorf("error while sending HTTP request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusBadRequest {
|
||||
return fmt.Errorf("request failed with response code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// print metric line results as info log
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
d.Log.Errorf("Dynatrace error reading response")
|
||||
}
|
||||
bodyString := string(bodyBytes)
|
||||
d.Log.Debugf("Dynatrace returned: %s", bodyString)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Dynatrace) Init() error {
|
||||
if len(d.URL) == 0 {
|
||||
d.Log.Infof("Dynatrace URL is empty, defaulting to OneAgent metrics interface")
|
||||
d.URL = apiconstants.GetDefaultOneAgentEndpoint()
|
||||
}
|
||||
if d.URL != apiconstants.GetDefaultOneAgentEndpoint() && d.APIToken.Empty() {
|
||||
d.Log.Errorf("Dynatrace api_token is a required field for Dynatrace output")
|
||||
return errors.New("api_token is a required field for Dynatrace output")
|
||||
}
|
||||
|
||||
tlsCfg, err := d.ClientConfig.TLSConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: tlsCfg,
|
||||
},
|
||||
Timeout: time.Duration(d.Timeout),
|
||||
}
|
||||
|
||||
dims := make([]dimensions.Dimension, 0, len(d.DefaultDimensions))
|
||||
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"))
|
||||
d.loggedMetrics = make(map[string]bool)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
outputs.Add("dynatrace", func() telegraf.Output {
|
||||
return &Dynatrace{
|
||||
Timeout: config.Duration(time.Second * 5),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Dynatrace) getTypeOption(metric telegraf.Metric, field *telegraf.Field) dynatrace_metric.MetricOption {
|
||||
metricName := metric.Name() + "." + field.Key
|
||||
if isCounterMetricsMatch(d.AddCounterMetrics, metricName) ||
|
||||
isCounterMetricsPatternsMatch(d.AddCounterMetricsPatterns, metricName) {
|
||||
switch v := field.Value.(type) {
|
||||
case float64:
|
||||
return dynatrace_metric.WithFloatCounterValueDelta(v)
|
||||
case uint64:
|
||||
return dynatrace_metric.WithIntCounterValueDelta(int64(v))
|
||||
case int64:
|
||||
return dynatrace_metric.WithIntCounterValueDelta(v)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
switch v := field.Value.(type) {
|
||||
case float64:
|
||||
return dynatrace_metric.WithFloatGaugeValue(v)
|
||||
case uint64:
|
||||
return dynatrace_metric.WithIntGaugeValue(int64(v))
|
||||
case int64:
|
||||
return dynatrace_metric.WithIntGaugeValue(v)
|
||||
case bool:
|
||||
if v {
|
||||
return dynatrace_metric.WithIntGaugeValue(1)
|
||||
}
|
||||
return dynatrace_metric.WithIntGaugeValue(0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isCounterMetricsMatch(counterMetrics []string, metricName string) bool {
|
||||
for _, i := range counterMetrics {
|
||||
if i == metricName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func 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
|
||||
}
|
901
plugins/outputs/dynatrace/dynatrace_test.go
Normal file
901
plugins/outputs/dynatrace/dynatrace_test.go
Normal file
|
@ -0,0 +1,901 @@
|
|||
package dynatrace
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dynatrace-oss/dynatrace-metric-utils-go/metric/apiconstants"
|
||||
"github.com/dynatrace-oss/dynatrace-metric-utils-go/metric/dimensions"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/metric"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
)
|
||||
|
||||
func TestNilMetrics(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(`{"linesOk":10,"linesInvalid":0,"error":null}`); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := &Dynatrace{
|
||||
Timeout: config.Duration(time.Second * 5),
|
||||
}
|
||||
|
||||
d.URL = ts.URL
|
||||
d.APIToken = config.NewSecret([]byte("123"))
|
||||
d.Log = testutil.Logger{}
|
||||
err := d.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = d.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = d.Write(nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEmptyMetricsSlice(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(`{"linesOk":10,"linesInvalid":0,"error":null}`); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := &Dynatrace{}
|
||||
|
||||
d.URL = ts.URL
|
||||
d.APIToken = config.NewSecret([]byte("123"))
|
||||
d.Log = testutil.Logger{}
|
||||
|
||||
err := d.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = d.Connect()
|
||||
require.NoError(t, err)
|
||||
err = d.Write(nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMockURL(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(`{"linesOk":10,"linesInvalid":0,"error":null}`); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := &Dynatrace{}
|
||||
|
||||
d.URL = ts.URL
|
||||
d.APIToken = config.NewSecret([]byte("123"))
|
||||
d.Log = testutil.Logger{}
|
||||
|
||||
err := d.Init()
|
||||
require.NoError(t, err)
|
||||
err = d.Connect()
|
||||
require.NoError(t, err)
|
||||
err = d.Write(testutil.MockMetrics())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMissingURL(t *testing.T) {
|
||||
d := &Dynatrace{}
|
||||
|
||||
d.Log = testutil.Logger{}
|
||||
err := d.Init()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, apiconstants.GetDefaultOneAgentEndpoint(), d.URL)
|
||||
err = d.Connect()
|
||||
require.Equal(t, apiconstants.GetDefaultOneAgentEndpoint(), d.URL)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMissingAPITokenMissingURL(t *testing.T) {
|
||||
d := &Dynatrace{}
|
||||
|
||||
d.Log = testutil.Logger{}
|
||||
err := d.Init()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, apiconstants.GetDefaultOneAgentEndpoint(), d.URL)
|
||||
err = d.Connect()
|
||||
require.Equal(t, apiconstants.GetDefaultOneAgentEndpoint(), d.URL)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMissingAPIToken(t *testing.T) {
|
||||
d := &Dynatrace{}
|
||||
|
||||
d.URL = "test"
|
||||
d.Log = testutil.Logger{}
|
||||
err := d.Init()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSendMetrics(t *testing.T) {
|
||||
var expected []string
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// check the encoded result
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err = json.NewEncoder(w).Encode(fmt.Sprintf(`{"linesOk":%d,"linesInvalid":0,"error":null}`, len(lines))); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := &Dynatrace{
|
||||
URL: ts.URL,
|
||||
APIToken: config.NewSecret([]byte("123")),
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
err := d.Init()
|
||||
require.NoError(t, err)
|
||||
err = d.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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",
|
||||
"simple_metric.counter,dt.metrics.source=telegraf count,delta=5 1289430000000",
|
||||
)
|
||||
d.AddCounterMetrics = append(d.AddCounterMetrics, "simple_metric.counter")
|
||||
m1 := metric.New(
|
||||
"simple_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 explicitly added to additional_counters
|
||||
expected = append(expected,
|
||||
"counter_type.value,dt.metrics.source=telegraf gauge,3.14 1289430000000",
|
||||
"counter_type.counter,dt.metrics.source=telegraf count,delta=5 1289430000000",
|
||||
)
|
||||
d.AddCounterMetrics = append(d.AddCounterMetrics, "counter_type.counter")
|
||||
m2 := metric.New(
|
||||
"counter_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",
|
||||
)
|
||||
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)
|
||||
}
|
||||
|
||||
func TestSendMetricsWithPatterns(t *testing.T) {
|
||||
var expected []string
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// check the encoded result
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err = json.NewEncoder(w).Encode(fmt.Sprintf(`{"linesOk":%d,"linesInvalid":0,"error":null}`, len(lines))); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := &Dynatrace{
|
||||
URL: ts.URL,
|
||||
APIToken: config.NewSecret([]byte("123")),
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
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
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
bodyString := string(bodyBytes)
|
||||
// use regex because dimension order isn't guaranteed
|
||||
if len(bodyString) != 94 {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("'bodyString' should have %d item(s), but has %d", 94, len(bodyString))
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile(`^mymeasurement\.myfield`).FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, `^mymeasurement\.myfield`)
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile(`a=test`).FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, `a=test`)
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile(`b=test`).FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, `a=test`)
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile(`c=test`).FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, `a=test`)
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile("dt.metrics.source=telegraf").FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, "dt.metrics.source=telegraf")
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile("gauge,3.14 1289430000000$").FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, "gauge,3.14 1289430000000$")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err = json.NewEncoder(w).Encode(`{"linesOk":1,"linesInvalid":0,"error":null}`); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := &Dynatrace{}
|
||||
|
||||
d.URL = ts.URL
|
||||
d.APIToken = config.NewSecret([]byte("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{"a": "test", "c": "test", "b": "test"},
|
||||
map[string]interface{}{"myfield": float64(3.14)},
|
||||
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 TestSendMetricWithoutTags(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// check the encoded result
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
bodyString := string(bodyBytes)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err = json.NewEncoder(w).Encode(`{"linesOk":1,"linesInvalid":0,"error":null}`); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := &Dynatrace{}
|
||||
|
||||
d.URL = ts.URL
|
||||
d.APIToken = config.NewSecret([]byte("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{}{"myfield": float64(3.14)},
|
||||
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 TestSendMetricWithUpperCaseTagKeys(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// check the encoded result
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
bodyString := string(bodyBytes)
|
||||
|
||||
// use regex because dimension order isn't guaranteed
|
||||
if len(bodyString) != 100 {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("'bodyString' should have %d item(s), but has %d", 100, len(bodyString))
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile(`^mymeasurement\.myfield`).FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, `^mymeasurement\.myfield`)
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile(`aaa=test`).FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, `aaa=test`)
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile(`b_b=test`).FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, `b_b=test`)
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile(`ccc=test`).FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, `ccc=test`)
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile("dt.metrics.source=telegraf").FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, "dt.metrics.source=telegraf")
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile("gauge,3.14 1289430000000$").FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, "gauge,3.14 1289430000000$")
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err = json.NewEncoder(w).Encode(`{"linesOk":1,"linesInvalid":0,"error":null}`); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := &Dynatrace{}
|
||||
|
||||
d.URL = ts.URL
|
||||
d.APIToken = config.NewSecret([]byte("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{"AAA": "test", "CcC": "test", "B B": "test"},
|
||||
map[string]interface{}{"myfield": float64(3.14)},
|
||||
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 TestSendBooleanMetricWithoutTags(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// check the encoded result
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
bodyString := string(bodyBytes)
|
||||
// use regex because field order isn't guaranteed
|
||||
if len(bodyString) != 132 {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("'bodyString' should have %d item(s), but has %d", 132, len(bodyString))
|
||||
return
|
||||
}
|
||||
if !strings.Contains(bodyString, "mymeasurement.yes,dt.metrics.source=telegraf gauge,1 1289430000000") {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("'bodyString' should contain %q", "mymeasurement.yes,dt.metrics.source=telegraf gauge,1 1289430000000")
|
||||
return
|
||||
}
|
||||
if !strings.Contains(bodyString, "mymeasurement.no,dt.metrics.source=telegraf gauge,0 1289430000000") {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("'bodyString' should contain %q", "mymeasurement.no,dt.metrics.source=telegraf gauge,0 1289430000000")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err = json.NewEncoder(w).Encode(`{"linesOk":1,"linesInvalid":0,"error":null}`); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := &Dynatrace{}
|
||||
|
||||
d.URL = ts.URL
|
||||
d.APIToken = config.NewSecret([]byte("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{}{"yes": true, "no": false},
|
||||
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 TestSendMetricWithDefaultDimensions(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// check the encoded result
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
bodyString := string(bodyBytes)
|
||||
// use regex because field order isn't guaranteed
|
||||
if len(bodyString) != 78 {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("'bodyString' should have %d item(s), but has %d", 78, len(bodyString))
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile("^mymeasurement.value").FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, "^mymeasurement.value")
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile("dt.metrics.source=telegraf").FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, "dt.metrics.source=telegraf")
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile("dim=value").FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, "dim=metric")
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile("gauge,2 1289430000000$").FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, "gauge,2 1289430000000$")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err = json.NewEncoder(w).Encode(`{"linesOk":1,"linesInvalid":0,"error":null}`); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := &Dynatrace{DefaultDimensions: map[string]string{"dim": "value"}}
|
||||
|
||||
d.URL = ts.URL
|
||||
d.APIToken = config.NewSecret([]byte("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": 2},
|
||||
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) {
|
||||
// check the encoded result
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
bodyString := string(bodyBytes)
|
||||
// use regex because field order isn't guaranteed
|
||||
if len(bodyString) != 80 {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("'bodyString' should have %d item(s), but has %d", 80, len(bodyString))
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile("^mymeasurement.value").FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, "^mymeasurement.value")
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile("dt.metrics.source=telegraf").FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, "dt.metrics.source=telegraf")
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile("dim=metric").FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, "dim=metric")
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile("gauge,32 1289430000000$").FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, "gauge,32 1289430000000$")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err = json.NewEncoder(w).Encode(`{"linesOk":1,"linesInvalid":0,"error":null}`); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := &Dynatrace{DefaultDimensions: map[string]string{"dim": "default"}}
|
||||
|
||||
d.URL = ts.URL
|
||||
d.APIToken = config.NewSecret([]byte("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) {
|
||||
// check the encoded result
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
bodyString := string(bodyBytes)
|
||||
// use regex because field order isn't guaranteed
|
||||
if len(bodyString) != 53 {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("'bodyString' should have %d item(s), but has %d", 53, len(bodyString))
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile("^mymeasurement.value").FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, "^mymeasurement.value")
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile("dim=static").FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, "dim=static")
|
||||
return
|
||||
}
|
||||
if regexp.MustCompile("gauge,32 1289430000000$").FindStringIndex(bodyString) == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Expect \"%v\" to match \"%v\"", bodyString, "gauge,32 1289430000000$")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err = json.NewEncoder(w).Encode(`{"linesOk":1,"linesInvalid":0,"error":null}`); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := &Dynatrace{DefaultDimensions: map[string]string{"dim": "default"}}
|
||||
|
||||
d.URL = ts.URL
|
||||
d.APIToken = config.NewSecret([]byte("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)
|
||||
}
|
||||
|
||||
var warnfCalledTimes int
|
||||
|
||||
type loggerStub struct {
|
||||
testutil.Logger
|
||||
}
|
||||
|
||||
func (loggerStub) Warnf(string, ...interface{}) {
|
||||
warnfCalledTimes++
|
||||
}
|
||||
|
||||
func TestSendUnsupportedMetric(t *testing.T) {
|
||||
warnfCalledTimes = 0
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
t.Fatal("should not export because the only metric is an invalid type")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := &Dynatrace{}
|
||||
|
||||
logStub := loggerStub{}
|
||||
|
||||
d.URL = ts.URL
|
||||
d.APIToken = config.NewSecret([]byte("123"))
|
||||
d.Log = logStub
|
||||
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{}{"metric1": "unsupported_type"},
|
||||
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC),
|
||||
)
|
||||
|
||||
metrics := []telegraf.Metric{m1}
|
||||
|
||||
err = d.Write(metrics)
|
||||
require.NoError(t, err)
|
||||
// Warnf called for invalid export
|
||||
require.Equal(t, 1, warnfCalledTimes)
|
||||
|
||||
err = d.Write(metrics)
|
||||
require.NoError(t, err)
|
||||
// Warnf skipped for more invalid exports with the same name
|
||||
require.Equal(t, 1, warnfCalledTimes)
|
||||
|
||||
m2 := metric.New(
|
||||
"mymeasurement",
|
||||
map[string]string{},
|
||||
map[string]interface{}{"metric2": "unsupported_type"},
|
||||
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC),
|
||||
)
|
||||
|
||||
metrics = []telegraf.Metric{m2}
|
||||
|
||||
err = d.Write(metrics)
|
||||
require.NoError(t, err)
|
||||
// Warnf called again for invalid export with a new metric name
|
||||
require.Equal(t, 2, warnfCalledTimes)
|
||||
|
||||
err = d.Write(metrics)
|
||||
require.NoError(t, err)
|
||||
// Warnf skipped for more invalid exports with the same name
|
||||
require.Equal(t, 2, warnfCalledTimes)
|
||||
}
|
44
plugins/outputs/dynatrace/sample.conf
Normal file
44
plugins/outputs/dynatrace/sample.conf
Normal file
|
@ -0,0 +1,44 @@
|
|||
# Send telegraf metrics to a Dynatrace environment
|
||||
[[outputs.dynatrace]]
|
||||
## For usage with the Dynatrace OneAgent you can omit any configuration,
|
||||
## the only requirement is that the OneAgent is running on the same host.
|
||||
## Only setup environment url and token if you want to monitor a Host without the OneAgent present.
|
||||
##
|
||||
## Your Dynatrace environment URL.
|
||||
## For Dynatrace OneAgent you can leave this empty or set it to "http://127.0.0.1:14499/metrics/ingest" (default)
|
||||
## For Dynatrace SaaS environments the URL scheme is "https://{your-environment-id}.live.dynatrace.com/api/v2/metrics/ingest"
|
||||
## For Dynatrace Managed environments the URL scheme is "https://{your-domain}/e/{your-environment-id}/api/v2/metrics/ingest"
|
||||
url = ""
|
||||
|
||||
## Your Dynatrace API token.
|
||||
## Create an API token within your Dynatrace environment, by navigating to Settings > Integration > Dynatrace API
|
||||
## The API token needs data ingest scope permission. When using OneAgent, no API token is required.
|
||||
api_token = ""
|
||||
|
||||
## Optional prefix for metric names (e.g.: "telegraf")
|
||||
prefix = "telegraf"
|
||||
|
||||
## Optional TLS Config
|
||||
# tls_ca = "/etc/telegraf/ca.pem"
|
||||
# tls_cert = "/etc/telegraf/cert.pem"
|
||||
# tls_key = "/etc/telegraf/key.pem"
|
||||
## Optional flag for ignoring tls certificate check
|
||||
# insecure_skip_verify = false
|
||||
|
||||
## Connection timeout, defaults to "5s" if not set.
|
||||
timeout = "5s"
|
||||
|
||||
## 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
|
||||
|
||||
## Optional dimensions to be added to every metric
|
||||
# [outputs.dynatrace.default_dimensions]
|
||||
# default_key = "default value"
|
Loading…
Add table
Add a link
Reference in a new issue