Skip to content

Add Datadog metrics exporter #900

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

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions cmd/otelcontribcol/components.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/awsxrayexporter"
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azuremonitorexporter"
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/carbonexporter"
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter"
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/elasticexporter"
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/honeycombexporter"
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/jaegerthrifthttpexporter"
Expand Down Expand Up @@ -114,6 +115,7 @@ func components() (component.Factories, error) {
elasticexporter.NewFactory(),
alibabacloudlogserviceexporter.NewFactory(),
sentryexporter.NewFactory(),
datadogexporter.NewFactory(),
}
for _, exp := range factories.Exporters {
exporters = append(exporters, exp)
Expand Down
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
10 changes: 10 additions & 0 deletions exporter/datadogexporter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Datadog Exporter

This exporter sends metric data to [Datadog](https://datadoghq.com) using DogStatsD.

## Configuration

There are no required settings.
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 all available options.
111 changes: 111 additions & 0 deletions exporter/datadogexporter/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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 (
"fmt"

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

// 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"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The statsd library picks up which protocol to use automatically (udp or unixgram) based on the endpoint field shape, so the transport field would have to be ignored in some cases (in particular, if endpoint starts with unix:// it's considered UDS, and if certain environment variables are set it would be UDP). Would that behavior be okay you? Otherwise we would have to leave it as it is right now.


// Telemetry states whether to send internal telemetry metrics from the statsd client
Telemetry bool `mapstructure:"telemetry"`
}

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

// Percentiles states whether to report percentiles for summary metrics,
// including the minimum and maximum
Percentiles bool `mapstructure:"report_percentiles"`

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

// DogStatsD defines the DogStatsD configuration options.
DogStatsD DogStatsDConfig `mapstructure:"dogstatsd"`
}

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

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
}

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

// Metrics defines the Metrics exporter specific configuration
Metrics MetricsConfig `mapstructure:"metrics"`
}
107 changes: 107 additions & 0 deletions exporter/datadogexporter/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// 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 (
"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/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/dogstatsd"].(*Config)

assert.Equal(t, apiConfig, &Config{
ExporterSettings: configmodels.ExporterSettings{
NameVal: "datadog/dogstatsd",
TypeVal: "datadog",
},

Metrics: MetricsConfig{
Percentiles: true,

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

dogstatsdConfig := cfg.Exporters["datadog/dogstatsd/config"].(*Config)

assert.Equal(t, dogstatsdConfig, &Config{
ExporterSettings: configmodels.ExporterSettings{
NameVal: "datadog/dogstatsd/config",
TypeVal: "datadog",
},

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

Metrics: MetricsConfig{
Namespace: "opentelemetry",
Percentiles: false,
Buckets: true,
DogStatsD: DogStatsDConfig{
Endpoint: "localhost:5000",
Telemetry: false,
},
},
})

}

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

assert.Equal(t,
tc.GetTags(),
[]string{
"host:customhost",
"env:customenv",
"service:customservice",
"version:customversion",
"key1:val1",
"key2:val2",
},
)
}
100 changes: 100 additions & 0 deletions exporter/datadogexporter/dogstatsd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// 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 (
"context"
"fmt"

"github.com/DataDog/datadog-go/statsd"
"go.opentelemetry.io/collector/consumer/pdata"
"go.opentelemetry.io/collector/exporter/exporterhelper"
"go.opentelemetry.io/collector/translator/internaldata"
"go.uber.org/zap"
)

type dogStatsDExporter struct {
logger *zap.Logger
cfg *Config
client *statsd.Client
}

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.Metrics.DogStatsD.Endpoint,
options...,
)

if err != nil {
return nil, fmt.Errorf("failed to initialize DogStatsD client: %s", err)
}

return &dogStatsDExporter{logger, cfg, client}, nil
}

func (exp *dogStatsDExporter) PushMetricsData(_ context.Context, md pdata.Metrics) (int, error) {
data := internaldata.MetricsToOC(md)
metrics, droppedTimeSeries := MapMetrics(exp, data)

for name, data := range metrics {
for _, metric := range data {

tags := metric.GetTags()

// Send the hostname if it has not been overridden
if exp.GetConfig().Hostname == "" && metric.GetHost() != "" {
tags = append(tags, fmt.Sprintf("host:%s", metric.GetHost()))
}

var err error
switch metric.GetType() {
case Gauge:
err = exp.client.Gauge(name, metric.GetValue(), tags, metric.GetRate())
}

if err != nil {
exp.GetLogger().Warn("Could not send metric to statsd", zap.String("metric", name), zap.Error(err))
}
}
}

return droppedTimeSeries, nil
}

func (exp *dogStatsDExporter) GetLogger() *zap.Logger {
return exp.logger
}

func (exp *dogStatsDExporter) GetConfig() *Config {
return exp.cfg
}

func (exp *dogStatsDExporter) GetQueueSettings() exporterhelper.QueueSettings {
return exporterhelper.QueueSettings{Enabled: false}
}

func (exp *dogStatsDExporter) GetRetrySettings() exporterhelper.RetrySettings {
return exporterhelper.RetrySettings{Enabled: false}
}
Loading