diff --git a/exporter/datadogexporter/README.md b/exporter/datadogexporter/README.md index b8f2d03693ff..23df0cda6f37 100644 --- a/exporter/datadogexporter/README.md +++ b/exporter/datadogexporter/README.md @@ -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: "" + # 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. diff --git a/exporter/datadogexporter/config.go b/exporter/datadogexporter/config.go index f1f3455f64d8..696efc50b2e6 100644 --- a/exporter/datadogexporter/config.go +++ b/exporter/datadogexporter/config.go @@ -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 diff --git a/exporter/datadogexporter/config_test.go b/exporter/datadogexporter/config_test.go index dc25ba74c39b..eb19b392e73e 100644 --- a/exporter/datadogexporter/config_test.go +++ b/exporter/datadogexporter/config_test.go @@ -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) } @@ -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) +} diff --git a/exporter/datadogexporter/dogstatsd.go b/exporter/datadogexporter/dogstatsd.go index 23fb68d64832..682077361900 100644 --- a/exporter/datadogexporter/dogstatsd.go +++ b/exporter/datadogexporter/dogstatsd.go @@ -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 { diff --git a/exporter/datadogexporter/example/config.yaml b/exporter/datadogexporter/example/config.yaml index 1284e03b5512..40f841ee4a7e 100644 --- a/exporter/datadogexporter/example/config.yaml +++ b/exporter/datadogexporter/example/config.yaml @@ -6,30 +6,97 @@ processors: exporters: datadog: - ## @param api_key - string - required - ## 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 + ## @param hostname - string - optional + ## A custom hostname. + ## If unset this will be determined automatically if possible. # - api_key: "" + # hostname: customhostname - ## @param site - string - optional - default: datadoghq.com - ## The site of the Datadog intake to send Agent data to. - ## Set to 'datadoghq.eu' to send data to the EU site. + ## @param env - string - optional + ## The environment for unified service tagging. + ## If unset it will be determined from the `DD_ENV` environment variable. # - # site: datadoghq.com + # env: prod - ## @param metrics_url - string - optional - default: https://api.datadoghq.com - ## The host of the Datadog intake server to send metrics to, only set this option - ## if you need the Agent to send metrics to a custom URL, it overrides the site - ## setting defined in "site". + ## @param service - string - optional + ## The service for unified service tagging. + ## If unset it will be determined from the `DD_SERVICE` environment variable. # - # metrics_url: https://api.datadoghq.com + # service: myservice + + ## @param version - string - optional + ## The version for unified service tagging. + ## If unset it will be determined from the `DD_VERSION` environment variable. + # + # version: myversion ## @param tags - list of strings - optional - default: [] - ## The list of default tags to add to every metric or trace + ## The list of default tags to add to every metric or trace. # # tags: [] + ## @param api - custom object - optional. + ## Specific API configuration for the agentless exporters. + # + # api: + ## @ param key - string - optional + ## 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 + ## + ## This is required if using any agentless exporter. + # + # key: + + ## @param site - string - optional - default: datadoghq.com + ## The site of the Datadog intake to send Agent data to. + ## Set to 'datadoghq.eu' to send data to the EU site. + # + # site: datadoghq.com + + ## @param metrics - custom object - optional + ## Metric exporter specific configuration. + # + # metrics: + ## @param mode - string - optional - default: dogstatsd + ## The mode with which to send metrics. + ## It can be one of + ## - 'none' (to explicitly disable it) + ## - 'dogstatsd' (to send metrics to a DogStatsD endpoint) + ## - 'agentless' (to send metrics directly through the API) + ## + ## If the mode is 'agentless' the API key must be set. + # + # mode: dogstatsd + + ## @param namespace - string - optional + ## The namespace with which to prefix all metrics. + ## By default metrics are not namespaced. + # + # namespace: "" + + ## @param dogstatsd - custom object - optional + ## DogStatSD mode specific configuration. + # + # dogstatsd: + ## @param endpoint - string - default: 127.0.0.1:8125 + ## The DogStatsD endpoint. It can be an UDP or UDS endpoint + # + # endpoint: 127.0.0.1:8125 + + ## @param telemetry - boolean - default: true + ## Whether to send telemetry metrics about the number of custom metrics sent. + # + # telemetry: true + + ## @param agentless - custom object - optional + ## Agentless metrics exporter specific configuration + # + # agentless: + ## @param endpoint - string - optional + ## The host of the Datadog intake server to send metrics to. + ## If unset the value is obtained through the `site` parameter in the `api` section. + # + # endpoint: https://api.datadoghq.com service: pipelines: traces: diff --git a/exporter/datadogexporter/factory.go b/exporter/datadogexporter/factory.go index 91b18bf8690b..6ead341268aa 100644 --- a/exporter/datadogexporter/factory.go +++ b/exporter/datadogexporter/factory.go @@ -53,9 +53,23 @@ func NewFactory() component.ExporterFactory { // createDefaultConfig creates the default exporter configuration func createDefaultConfig() configmodels.Exporter { return &Config{ - Site: DefaultSite, - Tags: DefaultTags, - Mode: DefaultMode, + API: APIConfig{ + Key: "", // must be set if using API + Site: "datadoghq.com", + }, + + Metrics: MetricsConfig{ + Mode: DogStatsDMode, + + DogStatsD: DogStatsDConfig{ + Endpoint: "127.0.0.1:8125", + Telemetry: true, + }, + + Agentless: AgentlessConfig{ + Endpoint: "", // set during config sanitization + }, + }, } } diff --git a/exporter/datadogexporter/factory_test.go b/exporter/datadogexporter/factory_test.go index 6af269272812..42202fc73a1b 100644 --- a/exporter/datadogexporter/factory_test.go +++ b/exporter/datadogexporter/factory_test.go @@ -34,9 +34,14 @@ func TestCreateDefaultConfig(t *testing.T) { cfg := factory.CreateDefaultConfig() assert.Equal(t, cfg, &Config{ - Site: DefaultSite, - Tags: DefaultTags, - Mode: DefaultMode, + API: APIConfig{Site: "datadoghq.com"}, + Metrics: MetricsConfig{ + Mode: DogStatsDMode, + DogStatsD: DogStatsDConfig{ + Endpoint: "127.0.0.1:8125", + Telemetry: true, + }, + }, }, "failed to create default config") assert.NoError(t, configcheck.ValidateConfig(cfg)) @@ -59,7 +64,7 @@ func TestCreateAgentMetricsExporter(t *testing.T) { exp, err := factory.CreateMetricsExporter( ctx, component.ExporterCreateParams{Logger: logger}, - cfg.Exporters["datadog"], + cfg.Exporters["datadog/dogstatsd"], ) assert.Nil(t, err) assert.NotNil(t, exp) @@ -82,7 +87,7 @@ func TestCreateAPIMetricsExporter(t *testing.T) { exp, err := factory.CreateMetricsExporter( ctx, component.ExporterCreateParams{Logger: logger}, - cfg.Exporters["datadog/2"], + cfg.Exporters["datadog/api"], ) // Not implemented @@ -107,7 +112,7 @@ func TestCreateAPITraceExporter(t *testing.T) { exp, err := factory.CreateTraceExporter( ctx, component.ExporterCreateParams{Logger: logger}, - cfg.Exporters["datadog/2"], + cfg.Exporters["datadog/api"], ) // Not implemented diff --git a/exporter/datadogexporter/metrics.go b/exporter/datadogexporter/metrics.go index 723cbe381e52..ab68e40420e9 100644 --- a/exporter/datadogexporter/metrics.go +++ b/exporter/datadogexporter/metrics.go @@ -34,12 +34,14 @@ type MetricsExporter interface { } func newMetricsExporter(logger *zap.Logger, cfg *Config) (MetricsExporter, error) { - switch cfg.Mode { - case AgentMode: + switch cfg.Metrics.Mode { + case DogStatsDMode: return newDogStatsDExporter(logger, cfg) + case NoneMode: + return nil, fmt.Errorf("Metrics exporter disabled for Datadog exporter") } - return nil, fmt.Errorf("Unsupported mode: '%s'", cfg.Mode) + return nil, fmt.Errorf("Unsupported mode: '%s'", cfg.Metrics.Mode) } type MetricType int diff --git a/exporter/datadogexporter/testdata/config.yaml b/exporter/datadogexporter/testdata/config.yaml index 213cff1385c8..b7731e0e0cdf 100644 --- a/exporter/datadogexporter/testdata/config.yaml +++ b/exporter/datadogexporter/testdata/config.yaml @@ -5,21 +5,45 @@ processors: exampleprocessor: exporters: - datadog: - sending_method: agent + datadog/api: + hostname: customhostname + env: prod + service: myservice + version: myversion + tags: - - tool:opentelemetry - - version:0.1.0 - datadog/2: - api_key: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - site: datadoghq.eu + - example:tag + + api: + key: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + site: datadoghq.eu + + metrics: + mode: agentless + namespace: opentelemetry + + datadog/dogstatsd: + metrics: + mode: dogstatsd + + datadog/dogstatsd/config: + metrics: + mode: dogstatsd + dogstatsd: + endpoint: "localhost:5000" + telemetry: false + datadog/invalid: - api_key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - sending_method: agent + metrics: + mode: invalid + + datadog/agentless/invalid: + metrics: + mode: agentless service: pipelines: - traces: + metrics: receivers: [examplereceiver] processors: [exampleprocessor] - exporters: [datadog, datadog/2] + exporters: [datadog/api, datadog/dogstatsd]