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

[exporter/datadog] Add api.fail_on_invalid_key to fail fast if API Key is invalid #9426

Merged
merged 11 commits into from
May 5, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- `attributesprocessor`: Support filter by severity (#9132)
- `processor/transform`: Add transformation of logs (#9368)
- `datadogexporter`: Add `metrics::summaries::mode` to specify export mode for summaries (#8846)
- `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 @@ -58,6 +58,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()
mx-psi marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

Actually this cancels the exporter prematurely after the function exits. The cancel is only meant to be called on shutdown (it is part of shutdown, so technically it does get called on all paths).

Copy link
Member

Choose a reason for hiding this comment

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

(it is part of shutdown, so technically it does get called on all paths).

the lint was failing because it was not called when the traces exporter fails to get created. I opened #9797 to fix this, thanks for spotting that!

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 @@ -70,8 +70,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 @@ -151,8 +152,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 @@ -202,8 +204,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 @@ -303,8 +306,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 @@ -357,8 +361,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 @@ -417,6 +422,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 @@ -447,6 +495,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 @@ -39,22 +39,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 @@ -67,6 +85,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 @@ -65,10 +65,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 @@ -89,7 +91,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