Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Datadog exporter overall structure #1142

Merged
merged 17 commits into from
Sep 30, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions exporter/datadogexporter/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../../Makefile.Common
17 changes: 17 additions & 0 deletions exporter/datadogexporter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Datadog Exporter

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).
```yaml
datadog:
api:
key: "<API key>"
# site: datadoghq.eu for sending data to the Datadog EU site
mx-psi marked this conversation as resolved.
Show resolved Hide resolved
```

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.
170 changes: 170 additions & 0 deletions exporter/datadogexporter/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package datadogexporter

import (
"errors"
"fmt"
"os"
"strings"

"go.opentelemetry.io/collector/config/configmodels"
"go.opentelemetry.io/collector/config/confignet"
)

var (
errUnsetAPIKey = errors.New("the Datadog API key is unset")
mx-psi marked this conversation as resolved.
Show resolved Hide resolved
)

// 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
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"`
}

// GetCensoredKey returns the API key censored for logging purposes
func (api *APIConfig) GetCensoredKey() string {
if len(api.Key) <= 5 {
return api.Key
}
return strings.Repeat("*", len(api.Key)-5) + api.Key[len(api.Key)-5:]
}

// 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"`

// Buckets states whether to report buckets from distribution metrics
Buckets bool `mapstructure:"report_buckets"`

// TCPAddr.Endpoint is the host of the Datadog intake server to send metrics to.
// If unset, the value is obtained from the Site.
confignet.TCPAddr `mapstructure:",squash"`
}

// 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"`
}

// UpdateWithEnv gets the unified service tagging information
// from the environment variables.
func (t *TagsConfig) UpdateWithEnv() {
mx-psi marked this conversation as resolved.
Show resolved Hide resolved
if t.Hostname == "" {
t.Hostname = os.Getenv("DD_HOST")
}

if t.Env == "" {
t.Env = os.Getenv("DD_ENV")
}

if t.Service == "" {
t.Service = os.Getenv("DD_SERVICE")
}

if t.Version == "" {
t.Version = os.Getenv("DD_VERSION")
}
}

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

vars := map[string]string{
"env": t.Env,
"service": t.Service,
"version": t.Version,
}

if addHost {
vars["host"] = t.Hostname
}

for name, val := range vars {
if val != "" {
tags = append(tags, fmt.Sprintf("%s:%s", name, val))
}
}

tags = append(tags, t.Tags...)

return tags
}

// Config defines configuration for the Datadog exporter.
type Config struct {
configmodels.ExporterSettings `mapstructure:",squash"` // squash ensures fields are correctly decoded in embedded struct.

TagsConfig `mapstructure:",squash"`

// API defines the Datadog API configuration.
API APIConfig `mapstructure:"api"`

// Metrics defines the Metrics exporter specific configuration
Metrics MetricsConfig `mapstructure:"metrics"`
}

// Sanitize tries to sanitize a given configuration
func (c *Config) Sanitize() error {
// Get info from environment variables
// if unset
c.TagsConfig.UpdateWithEnv()

// Add '.' at the end of namespace
if c.Metrics.Namespace != "" && !strings.HasSuffix(c.Metrics.Namespace, ".") {
c.Metrics.Namespace = c.Metrics.Namespace + "."
}

if c.API.Key == "" {
return errUnsetAPIKey
}

c.API.Key = strings.TrimSpace(c.API.Key)

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

return nil
}
165 changes: 165 additions & 0 deletions exporter/datadogexporter/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package datadogexporter

import (
"os"
"path"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/config/configmodels"
"go.opentelemetry.io/collector/config/confignet"
"go.opentelemetry.io/collector/config/configtest"
)

// TestLoadConfig tests that the configuration is loaded correctly
func TestLoadConfig(t *testing.T) {
factories, err := componenttest.ExampleComponents()
assert.NoError(t, err)

factory := NewFactory()
factories.Exporters[typeStr] = factory
cfg, err := configtest.LoadConfigFile(t, path.Join(".", "testdata", "config.yaml"), factories)

require.NoError(t, err)
require.NotNil(t, cfg)

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

require.NoError(t, err)
assert.Equal(t, &Config{
ExporterSettings: configmodels.ExporterSettings{
NameVal: "datadog/api",
TypeVal: typeStr,
},

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

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

Metrics: MetricsConfig{
Namespace: "opentelemetry.",
TCPAddr: confignet.TCPAddr{
Endpoint: "https://api.datadoghq.eu",
},
},
}, apiConfig)

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

}

func TestTags(t *testing.T) {
tc := TagsConfig{
Hostname: "customhost",
Env: "customenv",
Service: "customservice",
Version: "customversion",
Tags: []string{"key1:val1", "key2:val2"},
}

assert.ElementsMatch(t,
[]string{
"host:customhost",
"env:customenv",
"service:customservice",
"version:customversion",
"key1:val1",
"key2:val2",
},
tc.GetTags(true), // get host
)
}

// TestOverrideMetricsURL tests that the metrics URL is overridden
// correctly when set manually.
func TestOverrideMetricsURL(t *testing.T) {

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

cfg := Config{
API: APIConfig{Key: "notnull", Site: DefaultSite},
Metrics: MetricsConfig{
TCPAddr: confignet.TCPAddr{
Endpoint: DebugEndpoint,
},
},
}

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

func TestAPIKeyUnset(t *testing.T) {
cfg := Config{}
err := cfg.Sanitize()
assert.Equal(t, err, errUnsetAPIKey)
}

func TestCensorAPIKey(t *testing.T) {
cfg := APIConfig{
Key: "ddog_32_characters_long_api_key1",
}

assert.Equal(
t,
"***************************_key1",
cfg.GetCensoredKey(),
)
}

func TestUpdateWithEnv(t *testing.T) {
cfg := TagsConfig{
Hostname: "test_host",
Env: "test_env",
}

// Should be ignored since they were set
// on config
os.Setenv("DD_HOST", "env_host")
os.Setenv("DD_ENV", "env_env")

// Should be picked up
os.Setenv("DD_SERVICE", "env_service")
os.Setenv("DD_VERSION", "env_version")

cfg.UpdateWithEnv()

assert.Equal(t,
TagsConfig{
Hostname: "test_host",
Env: "test_env",
Service: "env_service",
Version: "env_version",
},
cfg,
)
}
Loading