Skip to content

Commit

Permalink
Refactor configuration (#45)
Browse files Browse the repository at this point in the history
* Refactor configuration
* Implement telemetry and tags configuration handling
* Update example configuration and README file
Co-authored-by: Kylian Serrania <[email protected]>
  • Loading branch information
mx-psi committed Aug 31, 2020
1 parent fdc98b5 commit 20afb0e
Show file tree
Hide file tree
Showing 9 changed files with 355 additions and 118 deletions.
19 changes: 14 additions & 5 deletions exporter/datadogexporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@ This exporter sends metric data to [Datadog](https://datadoghq.com).

## Configuration

The only required setting is a [Datadog API key](https://app.datadoghq.com/account/settings#api).
To send your Agent data to the Datadog EU site, set the site parameter to:
``` yaml
site: datadoghq.eu
```
The metrics exporter has two modes:
- If sending metrics through DogStatsD (the default mode), there are no required settings.
- If sending metrics without an Agent the mode must be set explicitly and
you must provide a [Datadog API key](https://app.datadoghq.com/account/settings#api).
```yaml
datadog:
api:
key: "<API key>"
# site: datadoghq.eu for sending data to the Datadog EU site
metrics:
mode: agentless
```
The hostname, environment, service and version can be set in the configuration for unified service tagging.
See the sample configuration file under the `example` folder for other available options.
146 changes: 114 additions & 32 deletions exporter/datadogexporter/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,61 +24,143 @@ import (

var (
errUnsetAPIKey = errors.New("the Datadog API key is unset")
errConflict = errors.New("'site' and 'api_key' must not be set with agent mode")
)

// Config defines configuration for the Datadog exporter.
type Config struct {
configmodels.ExporterSettings `mapstructure:",squash"` // squash ensures fields are correctly decoded in embedded struct.
const (
NoneMode = "none"
AgentlessMode = "agentless"
DogStatsDMode = "dogstatsd"
)

// ApiKey is the Datadog API key to associate your Agent's data with your organization.
// APIConfig defines the API configuration options
type APIConfig struct {
// Key is the Datadog API key to associate your Agent's data with your organization.
// Create a new API key here: https://app.datadoghq.com/account/settings
APIKey string `mapstructure:"api_key"`
Key string `mapstructure:"key"`

// Site is the site of the Datadog intake to send data to.
// The default value is "datadoghq.com".
Site string `mapstructure:"site"`
}

// DogStatsDConfig defines the DogStatsd related configuration
type DogStatsDConfig struct {
// Endpoint is the DogStatsD address.
// The default value is 127.0.0.1:8125
// A Unix address is supported
Endpoint string `mapstructure:"endpoint"`

// Telemetry states whether to send metrics
Telemetry bool `mapstructure:"telemetry"`
}

// AgentlessConfig defines the Agentless related configuration
type AgentlessConfig struct {
// Endpoint is the host of the Datadog intake server to send metrics to.
// If unset, the value is obtained from the Site.
Endpoint string `mapstructure:"endpoint"`
}

// MetricsConfig defines the metrics exporter specific configuration options
type MetricsConfig struct {
// Namespace is the namespace under which the metrics are sent
// By default metrics are not namespaced
Namespace string `mapstructure:"namespace"`

// MetricsURL is the host of the Datadog intake server or Dogstatsd server to send metrics to.
// If not set, the value is obtained from the Site and the sending method.
MetricsURL string `mapstructure:"metrics_url"`
// Mode is the metrics sending mode: either 'dogstatsd' or 'agentless'
Mode string `mapstructure:"mode"`

// Tags is the list of default tags to add to every metric or trace
// DogStatsD defines the DogStatsD configuration options.
DogStatsD DogStatsDConfig `mapstructure:"dogstatsd"`

// Agentless defines the Agentless configuration options.
Agentless AgentlessConfig `mapstructure:"agentless"`
}

// TagsConfig defines the tag-related configuration
// It is embedded in the configuration
type TagsConfig struct {
// Hostname is the host name for unified service tagging.
// If unset, it is determined automatically.
// See https://docs.datadoghq.com/agent/faq/how-datadog-agent-determines-the-hostname
// for more details.
Hostname string `mapstructure:"hostname"`

// Env is the environment for unified service tagging.
// It can also be set through the `DD_ENV` environment variable.
Env string `mapstructure:"env"`

// Service is the service for unified service tagging.
// It can also be set through the `DD_SERVICE` environment variable.
Service string `mapstructure:"service"`

// Version is the version for unified service tagging.
// It can also be set through the `DD_VERSION` version variable.
Version string `mapstructure:"version"`

// Tags is the list of default tags to add to every metric or trace.
Tags []string `mapstructure:"tags"`
}

// GetTags gets the default tags extracted from the configuration
func (t *TagsConfig) GetTags() []string {
tags := make([]string, 0, 4)

if t.Hostname != "" {
tags = append(tags, fmt.Sprintf("host:%s", t.Hostname))
}

// Mode states the mode for sending metrics and traces.
// The possible values are "api" and "agent".
// The default value is "agent".
Mode string `mapstructure:"sending_method"`
if t.Env != "" {
tags = append(tags, fmt.Sprintf("env:%s", t.Env))
}

if t.Service != "" {
tags = append(tags, fmt.Sprintf("service:%s", t.Service))
}

if t.Version != "" {
tags = append(tags, fmt.Sprintf("version:%s", t.Version))
}

if len(t.Tags) > 0 {
tags = append(tags, t.Tags...)
}

return tags
}

// Sanitize tries to sanitize a given configuration
func (c *Config) Sanitize() error {
if c.Mode == AgentMode {
// Config defines configuration for the Datadog exporter.
type Config struct {
configmodels.ExporterSettings `mapstructure:",squash"` // squash ensures fields are correctly decoded in embedded struct.

if c.APIKey != "" || c.Site != DefaultSite {
return errConflict
}
TagsConfig `mapstructure:",squash"`

if c.MetricsURL == "" {
c.MetricsURL = "127.0.0.1:8125"
}
// API defines the Datadog API configuration.
API APIConfig `mapstructure:"api"`

} else if c.Mode == APIMode {
// Metrics defines the Metrics exporter specific configuration
Metrics MetricsConfig `mapstructure:"metrics"`
}

if c.APIKey == "" {
// Sanitize tries to sanitize a given configuration
func (c *Config) Sanitize() error {

if c.Metrics.Mode != AgentlessMode && c.Metrics.Mode != DogStatsDMode {
return fmt.Errorf("Metrics mode '%s' is not recognized", c.Metrics.Mode)
}

// Exactly one configuration for metrics must be set
if c.Metrics.Mode == AgentlessMode {
if c.API.Key == "" {
return errUnsetAPIKey
}

// Sanitize API key
c.APIKey = strings.TrimSpace(c.APIKey)
c.API.Key = strings.TrimSpace(c.API.Key)

if c.MetricsURL == "" {
c.MetricsURL = fmt.Sprintf("https://api.%s", c.Site)
// Set the endpoint based on the Site
if c.Metrics.Agentless.Endpoint == "" {
c.Metrics.Agentless.Endpoint = fmt.Sprintf("https://api.%s", c.API.Site)
}

} else {
return fmt.Errorf("Selected mode '%s' is invalid", c.Mode)
}

return nil
Expand Down
108 changes: 67 additions & 41 deletions exporter/datadogexporter/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,40 +37,75 @@ func TestLoadConfig(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, cfg)

e0 := cfg.Exporters["datadog"].(*Config)
err = e0.Sanitize()
apiConfig := cfg.Exporters["datadog/api"].(*Config)
err = apiConfig.Sanitize()

require.NoError(t, err)
assert.Equal(t, e0, &Config{
assert.Equal(t, apiConfig, &Config{
ExporterSettings: configmodels.ExporterSettings{
NameVal: "datadog",
NameVal: "datadog/api",
TypeVal: "datadog",
},
Site: DefaultSite,
MetricsURL: "127.0.0.1:8125",
Tags: []string{"tool:opentelemetry", "version:0.1.0"},
Mode: AgentMode,

TagsConfig: TagsConfig{
Hostname: "customhostname",
Env: "prod",
Service: "myservice",
Version: "myversion",
Tags: []string{"example:tag"},
},

API: APIConfig{
Key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Site: "datadoghq.eu",
},

Metrics: MetricsConfig{
Mode: AgentlessMode,
Namespace: "opentelemetry",

DogStatsD: DogStatsDConfig{
Endpoint: "127.0.0.1:8125",
Telemetry: true,
},

Agentless: AgentlessConfig{
Endpoint: "https://api.datadoghq.eu",
},
},
})

e1 := cfg.Exporters["datadog/2"].(*Config)
err = e1.Sanitize()
dogstatsdConfig := cfg.Exporters["datadog/dogstatsd"].(*Config)
err = dogstatsdConfig.Sanitize()

require.NoError(t, err)
assert.Equal(t, e1,
&Config{
ExporterSettings: configmodels.ExporterSettings{
NameVal: "datadog/2",
TypeVal: "datadog",
assert.Equal(t, dogstatsdConfig, &Config{
ExporterSettings: configmodels.ExporterSettings{
NameVal: "datadog/dogstatsd",
TypeVal: "datadog",
},

TagsConfig: TagsConfig{},
API: APIConfig{Site: "datadoghq.com"},

Metrics: MetricsConfig{
Mode: DogStatsDMode,

DogStatsD: DogStatsDConfig{
Endpoint: "127.0.0.1:8125",
Telemetry: true,
},
APIKey: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Site: "datadoghq.eu",
MetricsURL: "https://api.datadoghq.eu",
Tags: DefaultTags,
Mode: APIMode,
})

e3 := cfg.Exporters["datadog/invalid"].(*Config)
err = e3.Sanitize()

Agentless: AgentlessConfig{},
},
})

invalidConfig := cfg.Exporters["datadog/invalid"].(*Config)
err = invalidConfig.Sanitize()
require.Error(t, err)

invalidConfig2 := cfg.Exporters["datadog/agentless/invalid"].(*Config)
err = invalidConfig2.Sanitize()
require.Error(t, err)

}
Expand All @@ -81,24 +116,15 @@ func TestOverrideMetricsURL(t *testing.T) {

const DebugEndpoint string = "http://localhost:8080"

cfg := &Config{
APIKey: "notnull",
Site: DefaultSite,
MetricsURL: DebugEndpoint,
Mode: APIMode,
cfg := Config{
API: APIConfig{Key: "notnull", Site: DefaultSite},
Metrics: MetricsConfig{
Mode: AgentlessMode,
Agentless: AgentlessConfig{Endpoint: DebugEndpoint},
},
}

err := cfg.Sanitize()
require.NoError(t, err)
assert.Equal(t, cfg.MetricsURL, DebugEndpoint)
}

// TestUnsetAPIKey tests that the config sanitizing throws an error
// when the API key has not been set
func TestUnsetAPIKey(t *testing.T) {

cfg := &Config{}
err := cfg.Sanitize()

require.Error(t, err)
}
assert.Equal(t, cfg.Metrics.Agentless.Endpoint, DebugEndpoint)
}
14 changes: 11 additions & 3 deletions exporter/datadogexporter/dogstatsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,18 @@ type dogStatsDExporter struct {

func newDogStatsDExporter(logger *zap.Logger, cfg *Config) (*dogStatsDExporter, error) {

options := []statsd.Option{
statsd.WithNamespace(cfg.Metrics.Namespace),
statsd.WithTags(cfg.TagsConfig.GetTags()),
}

if !cfg.Metrics.DogStatsD.Telemetry {
options = append(options, statsd.WithoutTelemetry())
}

client, err := statsd.New(
cfg.MetricsURL,
statsd.WithNamespace("opentelemetry."),
statsd.WithTags(cfg.Tags),
cfg.Metrics.DogStatsD.Endpoint,
options...,
)

if err != nil {
Expand Down
Loading

0 comments on commit 20afb0e

Please sign in to comment.