Skip to content

Commit fdc98b5

Browse files
committed
Initial DogStatsD implementation (#15)
Initial metrics exporter through DogStatsD with support for all metric types but summary and distribution
1 parent e848a60 commit fdc98b5

File tree

12 files changed

+774
-32
lines changed

12 files changed

+774
-32
lines changed

exporter/datadogexporter/config.go

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

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

2930
// Config defines configuration for the Datadog exporter.
@@ -38,25 +39,46 @@ type Config struct {
3839
// The default value is "datadoghq.com".
3940
Site string `mapstructure:"site"`
4041

41-
// MetricsURL is the host of the Datadog intake server to send metrics to.
42-
// If not set, the value is obtained from the Site.
42+
// MetricsURL is the host of the Datadog intake server or Dogstatsd server to send metrics to.
43+
// If not set, the value is obtained from the Site and the sending method.
4344
MetricsURL string `mapstructure:"metrics_url"`
45+
46+
// Tags is the list of default tags to add to every metric or trace
47+
Tags []string `mapstructure:"tags"`
48+
49+
// Mode states the mode for sending metrics and traces.
50+
// The possible values are "api" and "agent".
51+
// The default value is "agent".
52+
Mode string `mapstructure:"sending_method"`
4453
}
4554

4655
// Sanitize tries to sanitize a given configuration
4756
func (c *Config) Sanitize() error {
57+
if c.Mode == AgentMode {
4858

49-
// Check API key is set
50-
if c.APIKey == "" {
51-
return errUnsetAPIKey
52-
}
59+
if c.APIKey != "" || c.Site != DefaultSite {
60+
return errConflict
61+
}
62+
63+
if c.MetricsURL == "" {
64+
c.MetricsURL = "127.0.0.1:8125"
65+
}
66+
67+
} else if c.Mode == APIMode {
68+
69+
if c.APIKey == "" {
70+
return errUnsetAPIKey
71+
}
72+
73+
// Sanitize API key
74+
c.APIKey = strings.TrimSpace(c.APIKey)
5375

54-
// Sanitize API key
55-
c.APIKey = strings.TrimSpace(c.APIKey)
76+
if c.MetricsURL == "" {
77+
c.MetricsURL = fmt.Sprintf("https://api.%s", c.Site)
78+
}
5679

57-
// Set Endpoint based on site if unset
58-
if c.MetricsURL == "" {
59-
c.MetricsURL = fmt.Sprintf("https://api.%s", c.Site)
80+
} else {
81+
return fmt.Errorf("Selected mode '%s' is invalid", c.Mode)
6082
}
6183

6284
return nil

exporter/datadogexporter/config_test.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ func TestLoadConfig(t *testing.T) {
4646
NameVal: "datadog",
4747
TypeVal: "datadog",
4848
},
49-
APIKey: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
5049
Site: DefaultSite,
51-
MetricsURL: "https://api.datadoghq.com",
50+
MetricsURL: "127.0.0.1:8125",
51+
Tags: []string{"tool:opentelemetry", "version:0.1.0"},
52+
Mode: AgentMode,
5253
})
5354

5455
e1 := cfg.Exporters["datadog/2"].(*Config)
@@ -61,10 +62,17 @@ func TestLoadConfig(t *testing.T) {
6162
NameVal: "datadog/2",
6263
TypeVal: "datadog",
6364
},
64-
APIKey: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
65+
APIKey: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
6566
Site: "datadoghq.eu",
6667
MetricsURL: "https://api.datadoghq.eu",
68+
Tags: DefaultTags,
69+
Mode: APIMode,
6770
})
71+
72+
e3 := cfg.Exporters["datadog/invalid"].(*Config)
73+
err = e3.Sanitize()
74+
require.Error(t, err)
75+
6876
}
6977

7078
// TestOverrideMetricsURL tests that the metrics URL is overridden
@@ -77,6 +85,7 @@ func TestOverrideMetricsURL(t *testing.T) {
7785
APIKey: "notnull",
7886
Site: DefaultSite,
7987
MetricsURL: DebugEndpoint,
88+
Mode: APIMode,
8089
}
8190

8291
err := cfg.Sanitize()
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package datadogexporter
16+
17+
import (
18+
"context"
19+
"fmt"
20+
21+
"github.com/DataDog/datadog-go/statsd"
22+
"go.opentelemetry.io/collector/consumer/pdata"
23+
"go.opentelemetry.io/collector/exporter/exporterhelper"
24+
"go.uber.org/zap"
25+
)
26+
27+
type dogStatsDExporter struct {
28+
logger *zap.Logger
29+
cfg *Config
30+
client *statsd.Client
31+
}
32+
33+
func newDogStatsDExporter(logger *zap.Logger, cfg *Config) (*dogStatsDExporter, error) {
34+
35+
client, err := statsd.New(
36+
cfg.MetricsURL,
37+
statsd.WithNamespace("opentelemetry."),
38+
statsd.WithTags(cfg.Tags),
39+
)
40+
41+
if err != nil {
42+
return nil, fmt.Errorf("Failed to initialize DogStatsD client: %s", err)
43+
}
44+
45+
return &dogStatsDExporter{logger, cfg, client}, nil
46+
}
47+
48+
func (exp *dogStatsDExporter) PushMetricsData(_ context.Context, md pdata.Metrics) (int, error) {
49+
metrics, droppedTimeSeries, err := MapMetrics(exp, md)
50+
51+
if err != nil {
52+
return droppedTimeSeries, err
53+
}
54+
55+
for name, data := range metrics {
56+
for _, metric := range data {
57+
switch metric.GetType() {
58+
case Count:
59+
err = exp.client.Count(name, metric.GetValue().(int64), metric.GetTags(), metric.GetRate())
60+
case Gauge:
61+
err = exp.client.Gauge(name, metric.GetValue().(float64), metric.GetTags(), metric.GetRate())
62+
}
63+
64+
if err != nil {
65+
return droppedTimeSeries, err
66+
}
67+
}
68+
}
69+
70+
return droppedTimeSeries, nil
71+
}
72+
73+
func (exp *dogStatsDExporter) GetLogger() *zap.Logger {
74+
return exp.logger
75+
}
76+
77+
func (exp *dogStatsDExporter) GetConfig() *Config {
78+
return exp.cfg
79+
}
80+
81+
func (exp *dogStatsDExporter) GetQueueSettings() exporterhelper.QueueSettings {
82+
return exporterhelper.CreateDefaultQueueSettings()
83+
}
84+
85+
func (exp *dogStatsDExporter) GetRetrySettings() exporterhelper.RetrySettings {
86+
return exporterhelper.CreateDefaultRetrySettings()
87+
}

exporter/datadogexporter/example/config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ exporters:
2525
#
2626
# metrics_url: https://api.datadoghq.com
2727

28+
## @param tags - list of strings - optional - default: []
29+
## The list of default tags to add to every metric or trace
30+
#
31+
# tags: []
32+
2833
service:
2934
pipelines:
3035
traces:

exporter/datadogexporter/factory.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ const (
2828

2929
// DefaultSite is the default site of the Datadog intake to send data to
3030
DefaultSite = "datadoghq.com"
31+
32+
// List the different sending methods
33+
AgentMode = "agent"
34+
APIMode = "api"
35+
DefaultMode = APIMode
36+
)
37+
38+
var (
39+
// DefaultTags is the default set of tags to add to every metric or trace
40+
DefaultTags = []string{}
3141
)
3242

3343
// NewFactory creates a Datadog exporter factory
@@ -44,6 +54,8 @@ func NewFactory() component.ExporterFactory {
4454
func createDefaultConfig() configmodels.Exporter {
4555
return &Config{
4656
Site: DefaultSite,
57+
Tags: DefaultTags,
58+
Mode: DefaultMode,
4759
}
4860
}
4961

@@ -61,8 +73,17 @@ func CreateMetricsExporter(
6173
return nil, err
6274
}
6375

64-
//Metrics are not yet supported
65-
return nil, configerror.ErrDataTypeIsNotSupported
76+
exp, err := newMetricsExporter(params.Logger, cfg)
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
return exporterhelper.NewMetricsExporter(
82+
cfg,
83+
exp.PushMetricsData,
84+
exporterhelper.WithQueue(exp.GetQueueSettings()),
85+
exporterhelper.WithRetry(exp.GetRetrySettings()),
86+
)
6687
}
6788

6889
// CreateTracesExporter creates a traces exporter based on this config.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package datadogexporter
15+
16+
import (
17+
"context"
18+
"path"
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
"go.opentelemetry.io/collector/component"
24+
"go.opentelemetry.io/collector/component/componenttest"
25+
"go.opentelemetry.io/collector/config/configcheck"
26+
"go.opentelemetry.io/collector/config/configmodels"
27+
"go.opentelemetry.io/collector/config/configtest"
28+
"go.uber.org/zap"
29+
)
30+
31+
// Test that the factory creates the default configuration
32+
func TestCreateDefaultConfig(t *testing.T) {
33+
factory := NewFactory()
34+
cfg := factory.CreateDefaultConfig()
35+
36+
assert.Equal(t, cfg, &Config{
37+
Site: DefaultSite,
38+
Tags: DefaultTags,
39+
Mode: DefaultMode,
40+
}, "failed to create default config")
41+
42+
assert.NoError(t, configcheck.ValidateConfig(cfg))
43+
}
44+
45+
func TestCreateAgentMetricsExporter(t *testing.T) {
46+
logger := zap.NewNop()
47+
48+
factories, err := componenttest.ExampleComponents()
49+
assert.NoError(t, err)
50+
51+
factory := NewFactory()
52+
factories.Exporters[configmodels.Type(typeStr)] = factory
53+
cfg, err := configtest.LoadConfigFile(t, path.Join(".", "testdata", "config.yaml"), factories)
54+
55+
require.NoError(t, err)
56+
require.NotNil(t, cfg)
57+
58+
ctx := context.Background()
59+
exp, err := factory.CreateMetricsExporter(
60+
ctx,
61+
component.ExporterCreateParams{Logger: logger},
62+
cfg.Exporters["datadog"],
63+
)
64+
assert.Nil(t, err)
65+
assert.NotNil(t, exp)
66+
}
67+
68+
func TestCreateAPIMetricsExporter(t *testing.T) {
69+
logger := zap.NewNop()
70+
71+
factories, err := componenttest.ExampleComponents()
72+
assert.NoError(t, err)
73+
74+
factory := NewFactory()
75+
factories.Exporters[configmodels.Type(typeStr)] = factory
76+
cfg, err := configtest.LoadConfigFile(t, path.Join(".", "testdata", "config.yaml"), factories)
77+
78+
require.NoError(t, err)
79+
require.NotNil(t, cfg)
80+
81+
ctx := context.Background()
82+
exp, err := factory.CreateMetricsExporter(
83+
ctx,
84+
component.ExporterCreateParams{Logger: logger},
85+
cfg.Exporters["datadog/2"],
86+
)
87+
88+
// Not implemented
89+
assert.NotNil(t, err)
90+
assert.Nil(t, exp)
91+
}
92+
93+
func TestCreateAPITraceExporter(t *testing.T) {
94+
logger := zap.NewNop()
95+
96+
factories, err := componenttest.ExampleComponents()
97+
assert.NoError(t, err)
98+
99+
factory := NewFactory()
100+
factories.Exporters[configmodels.Type(typeStr)] = factory
101+
cfg, err := configtest.LoadConfigFile(t, path.Join(".", "testdata", "config.yaml"), factories)
102+
103+
require.NoError(t, err)
104+
require.NotNil(t, cfg)
105+
106+
ctx := context.Background()
107+
exp, err := factory.CreateTraceExporter(
108+
ctx,
109+
component.ExporterCreateParams{Logger: logger},
110+
cfg.Exporters["datadog/2"],
111+
)
112+
113+
// Not implemented
114+
assert.NotNil(t, err)
115+
assert.Nil(t, exp)
116+
}

exporter/datadogexporter/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ module github.com/DataDog/opentelemetry-collector-contrib/exporter/datadogexport
33
go 1.15
44

55
require (
6+
github.com/DataDog/datadog-go v3.7.2+incompatible
7+
github.com/census-instrumentation/opencensus-proto v0.3.0
68
github.com/stretchr/testify v1.6.1
79
go.opentelemetry.io/collector v0.8.1-0.20200820203435-961c48b75778
10+
go.uber.org/zap v1.15.0
811
)

0 commit comments

Comments
 (0)