Skip to content

Commit

Permalink
[exporter/datadog] Add api.fail_on_invalid_key to fail fast if API …
Browse files Browse the repository at this point in the history
…Key is invalid (open-telemetry#9426)

* add new `fail_on_invalid_key` option

* improve the description of fail_on_invalid_key

* avoid failing when a transient network error

* remove unnecessary comment

* utils.ValidateAPI() returns error

* add tests

* not returning error when api failure

* fix test names

* fix lint

* fix go vet: prevent possible context leak

Co-authored-by: Alex Boten <[email protected]>
  • Loading branch information
2 people authored and djaglowski committed May 10, 2022
1 parent 24f7140 commit 7bb7e37
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- `internal/stanza`: Add support for `remove` operator (#9524)
- `k8sattributesprocessor`: Support regex capture groups in tag_name (#9525)
- `transformprocessor`: Add new `truncation` function to allow truncating string values in maps such as `attributes` or `resource.attributes` (#9546)
- `datadogexporter`: Add `api.fail_on_invalid_key` to fail fast if api key is invalid (#9426)

### 🧰 Bug fixes 🧰

Expand Down
4 changes: 4 additions & 0 deletions exporter/datadogexporter/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ type APIConfig struct {
// It can also be set through the `DD_SITE` environment variable (Deprecated: [v0.47.0] set environment variable explicitly on configuration instead).
// The default value is "datadoghq.com".
Site string `mapstructure:"site"`

// FailOnInvalidKey states whether to exit at startup on invalid API key.
// The default value is false.
FailOnInvalidKey bool `mapstructure:"fail_on_invalid_key"`
}

// MetricsConfig defines the metrics exporter specific configuration options
Expand Down
5 changes: 5 additions & 0 deletions exporter/datadogexporter/example/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ exporters:
#
# site: datadoghq.com

## @param fail_on_invalid_key - boolean - optional - default: false
## Whether to exit at startup on invalid API key.
#
# fail_on_invalid_key: false

## @param tls - custom object - optional
# TLS settings for HTTPS communications.
# tls:
Expand Down
7 changes: 6 additions & 1 deletion exporter/datadogexporter/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ func (f *factory) createTracesExporter(
}

ctx, cancel := context.WithCancel(ctx)
defer cancel()
var pushTracesFn consumer.ConsumeTracesFunc

if cfg.OnlyMetadata {
Expand All @@ -214,7 +215,11 @@ func (f *factory) createTracesExporter(
return nil
}
} else {
pushTracesFn = newTracesExporter(ctx, set, cfg, &f.onceMetadata).pushTraceDataScrubbed
exporter, err := newTracesExporter(ctx, set, cfg, &f.onceMetadata)
if err != nil {
return nil, err
}
pushTracesFn = exporter.pushTraceDataScrubbed
}

return exporterhelper.NewTracesExporter(
Expand Down
111 changes: 101 additions & 10 deletions exporter/datadogexporter/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ func TestCreateDefaultConfig(t *testing.T) {
QueueSettings: exporterhelper.NewDefaultQueueSettings(),

API: ddconfig.APIConfig{
Key: "API_KEY",
Site: "SITE",
Key: "API_KEY",
Site: "SITE",
FailOnInvalidKey: false,
},

Metrics: ddconfig.MetricsConfig{
Expand Down Expand Up @@ -152,8 +153,9 @@ func TestLoadConfig(t *testing.T) {
Tags: []string{"example:tag"},
}, apiConfig.TagsConfig)
assert.Equal(t, ddconfig.APIConfig{
Key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Site: "datadoghq.eu",
Key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Site: "datadoghq.eu",
FailOnInvalidKey: true,
}, apiConfig.API)
assert.Equal(t, ddconfig.MetricsConfig{
TCPAddr: confignet.TCPAddr{
Expand Down Expand Up @@ -203,8 +205,9 @@ func TestLoadConfig(t *testing.T) {
},

API: ddconfig.APIConfig{
Key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Site: "datadoghq.com",
Key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Site: "datadoghq.com",
FailOnInvalidKey: false,
},

Metrics: ddconfig.MetricsConfig{
Expand Down Expand Up @@ -304,8 +307,9 @@ func TestLoadConfigEnvVariables(t *testing.T) {
}, apiConfig.TagsConfig)
assert.Equal(t,
ddconfig.APIConfig{
Key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Site: "datadoghq.eu",
Key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Site: "datadoghq.eu",
FailOnInvalidKey: false,
}, apiConfig.API)
assert.Equal(t,
ddconfig.MetricsConfig{
Expand Down Expand Up @@ -358,8 +362,9 @@ func TestLoadConfigEnvVariables(t *testing.T) {
EnvVarTags: "envexample:tag envexample2:tag",
}, defaultConfig.TagsConfig)
assert.Equal(t, ddconfig.APIConfig{
Key: "replacedapikey",
Site: "datadoghq.test",
Key: "replacedapikey",
Site: "datadoghq.test",
FailOnInvalidKey: false,
}, defaultConfig.API)
assert.Equal(t, ddconfig.MetricsConfig{
TCPAddr: confignet.TCPAddr{
Expand Down Expand Up @@ -418,6 +423,49 @@ func TestCreateAPIMetricsExporter(t *testing.T) {
assert.NotNil(t, exp)
}

func TestCreateAPIMetricsExporterFailOnInvalidkey(t *testing.T) {
server := testutils.DatadogServerMock(testutils.ValidateAPIKeyEndpointInvalid)
defer server.Close()

factories, err := componenttest.NopFactories()
require.NoError(t, err)

factory := NewFactory()
factories.Exporters[typeStr] = factory
cfg, err := servicetest.LoadConfigAndValidate(filepath.Join("testdata", "config.yaml"), factories)

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

// Use the mock server for API key validation
c := (cfg.Exporters[config.NewComponentIDWithName(typeStr, "api")]).(*ddconfig.Config)
c.Metrics.TCPAddr.Endpoint = server.URL
c.HostMetadata.Enabled = false

t.Run("fail_on_invalid_key is true", func(t *testing.T) {
c.API.FailOnInvalidKey = true
ctx := context.Background()
exp, err := factory.CreateMetricsExporter(
ctx,
componenttest.NewNopExporterCreateSettings(),
cfg.Exporters[config.NewComponentIDWithName(typeStr, "api")],
)
assert.EqualError(t, err, "API Key validation failed")
assert.Nil(t, exp)
})
t.Run("fail_on_invalid_key is false", func(t *testing.T) {
c.API.FailOnInvalidKey = false
ctx := context.Background()
exp, err := factory.CreateMetricsExporter(
ctx,
componenttest.NewNopExporterCreateSettings(),
cfg.Exporters[config.NewComponentIDWithName(typeStr, "api")],
)
assert.Nil(t, err)
assert.NotNil(t, exp)
})
}

func TestCreateAPITracesExporter(t *testing.T) {
server := testutils.DatadogServerMock()
defer server.Close()
Expand Down Expand Up @@ -448,6 +496,49 @@ func TestCreateAPITracesExporter(t *testing.T) {
assert.NotNil(t, exp)
}

func TestCreateAPITracesExporterFailOnInvalidkey(t *testing.T) {
server := testutils.DatadogServerMock(testutils.ValidateAPIKeyEndpointInvalid)
defer server.Close()

factories, err := componenttest.NopFactories()
require.NoError(t, err)

factory := NewFactory()
factories.Exporters[typeStr] = factory
cfg, err := servicetest.LoadConfigAndValidate(filepath.Join("testdata", "config.yaml"), factories)

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

// Use the mock server for API key validation
c := (cfg.Exporters[config.NewComponentIDWithName(typeStr, "api")]).(*ddconfig.Config)
c.Metrics.TCPAddr.Endpoint = server.URL
c.HostMetadata.Enabled = false

t.Run("fail_on_invalid_key is true", func(t *testing.T) {
c.API.FailOnInvalidKey = true
ctx := context.Background()
exp, err := factory.CreateTracesExporter(
ctx,
componenttest.NewNopExporterCreateSettings(),
cfg.Exporters[config.NewComponentIDWithName(typeStr, "api")],
)
assert.EqualError(t, err, "API Key validation failed")
assert.Nil(t, exp)
})
t.Run("fail_on_invalid_key is false", func(t *testing.T) {
c.API.FailOnInvalidKey = false
ctx := context.Background()
exp, err := factory.CreateTracesExporter(
ctx,
componenttest.NewNopExporterCreateSettings(),
cfg.Exporters[config.NewComponentIDWithName(typeStr, "api")],
)
assert.Nil(t, err)
assert.NotNil(t, exp)
})
}

func TestOnlyMetadata(t *testing.T) {
server := testutils.DatadogServerMock()
defer server.Close()
Expand Down
40 changes: 33 additions & 7 deletions exporter/datadogexporter/internal/testutils/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,40 @@ type DatadogServer struct {
}

// DatadogServerMock mocks a Datadog backend server
func DatadogServerMock() *DatadogServer {
func DatadogServerMock(overwriteHandlerFuncs ...OverwriteHandleFunc) *DatadogServer {
metadataChan := make(chan []byte)
handler := http.NewServeMux()
handler.HandleFunc("/api/v1/validate", validateAPIKeyEndpoint)
handler.HandleFunc("/api/v1/series", metricsEndpoint)
handler.HandleFunc("/intake", newMetadataEndpoint(metadataChan))
handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {})
mux := http.NewServeMux()

srv := httptest.NewServer(handler)
handlers := map[string]http.HandlerFunc{
"/api/v1/validate": validateAPIKeyEndpoint,
"/api/v1/series": metricsEndpoint,
"/intake": newMetadataEndpoint(metadataChan),
"/": func(w http.ResponseWriter, r *http.Request) {},
}
for _, f := range overwriteHandlerFuncs {
p, hf := f()
handlers[p] = hf
}
for pattern, handler := range handlers {
mux.HandleFunc(pattern, handler)
}

srv := httptest.NewServer(mux)

return &DatadogServer{
srv,
metadataChan,
}
}

// OverwriteHandleFuncs allows to overwrite the default handler functions
type OverwriteHandleFunc func() (string, http.HandlerFunc)

// ValidateAPIKeyEndpointInvalid returns a handler function that returns an invalid API key response
func ValidateAPIKeyEndpointInvalid() (string, http.HandlerFunc) {
return "/api/v1/validate", validateAPIKeyEndpointInvalid
}

type validateAPIKeyResponse struct {
Valid bool `json:"valid"`
}
Expand All @@ -68,6 +86,14 @@ func validateAPIKeyEndpoint(w http.ResponseWriter, r *http.Request) {
w.Write(resJSON)
}

func validateAPIKeyEndpointInvalid(w http.ResponseWriter, r *http.Request) {
res := validateAPIKeyResponse{Valid: false}
resJSON, _ := json.Marshal(res)

w.Header().Set("Content-Type", "application/json")
w.Write(resJSON)
}

type metricsResponse struct {
Status string `json:"status"`
}
Expand Down
23 changes: 14 additions & 9 deletions exporter/datadogexporter/internal/utils/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package utils // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/internal/utils"

import (
"errors"

"go.uber.org/zap"
"gopkg.in/zorkian/go-datadog-api.v2"
)
Expand All @@ -27,17 +29,20 @@ func CreateClient(APIKey string, endpoint string) *datadog.Client {
return client
}

var ErrInvalidAPI = errors.New("API Key validation failed")

// ValidateAPIKey checks that the provided client was given a correct API key.
func ValidateAPIKey(logger *zap.Logger, client *datadog.Client) {
func ValidateAPIKey(logger *zap.Logger, client *datadog.Client) error {
logger.Info("Validating API key.")
res, err := client.Validate()
if err != nil {
logger.Warn("Error while validating API key.", zap.Error(err))
}

if res {
valid, err := client.Validate()
if err == nil && valid {
logger.Info("API key validation successful.")
} else {
logger.Warn("API key validation failed.")
return nil
}
if err != nil {
logger.Warn("Error while validating API key", zap.Error(err))
return nil
}
logger.Warn(ErrInvalidAPI.Error())
return ErrInvalidAPI
}
4 changes: 3 additions & 1 deletion exporter/datadogexporter/metrics_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ func newMetricsExporter(ctx context.Context, params component.ExporterCreateSett
client.ExtraHeader["User-Agent"] = utils.UserAgent(params.BuildInfo)
client.HttpClient = utils.NewHTTPClient(cfg.TimeoutSettings, cfg.LimitedHTTPClientSettings.TLSSetting.InsecureSkipVerify)

utils.ValidateAPIKey(params.Logger, client)
if err := utils.ValidateAPIKey(params.Logger, client); err != nil && cfg.API.FailOnInvalidKey {
return nil, err
}

tr, err := translatorFromConfig(params.Logger, cfg)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion exporter/datadogexporter/testdata/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ exporters:
api:
key: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
site: datadoghq.eu
fail_on_invalid_key: true

traces:
sample_rate: 1
Expand Down Expand Up @@ -63,7 +64,7 @@ exporters:
datadog/invalid:
metrics:
endpoint: "invalid:"

traces:
endpoint: "invalid:"

Expand Down
8 changes: 5 additions & 3 deletions exporter/datadogexporter/traces_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,12 @@ var (
}
)

func newTracesExporter(ctx context.Context, params component.ExporterCreateSettings, cfg *config.Config, onceMetadata *sync.Once) *traceExporter {
func newTracesExporter(ctx context.Context, params component.ExporterCreateSettings, cfg *config.Config, onceMetadata *sync.Once) (*traceExporter, error) {
// client to send running metric to the backend & perform API key validation
client := utils.CreateClient(cfg.API.Key, cfg.Metrics.TCPAddr.Endpoint)
utils.ValidateAPIKey(params.Logger, client)
if err := utils.ValidateAPIKey(params.Logger, client); err != nil && cfg.API.FailOnInvalidKey {
return nil, err
}

// removes potentially sensitive info and PII, approach taken from serverless approach
// https://github.com/DataDog/datadog-serverless-functions/blob/11f170eac105d66be30f18eda09eca791bc0d31b/aws/logs_monitoring/trace_forwarder/cmd/trace/main.go#L43
Expand All @@ -90,7 +92,7 @@ func newTracesExporter(ctx context.Context, params component.ExporterCreateSetti
onceMetadata: onceMetadata,
}

return exporter
return exporter, nil
}

// TODO: when component.Host exposes a way to retrieve processors, check for batch processors
Expand Down

0 comments on commit 7bb7e37

Please sign in to comment.