1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View 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.

View 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
}

View 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)
}

View 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"