diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e1b95d64a56..465112552cb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,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 🧰 diff --git a/exporter/datadogexporter/config/config.go b/exporter/datadogexporter/config/config.go index 344f85669d44..63dcf5028332 100644 --- a/exporter/datadogexporter/config/config.go +++ b/exporter/datadogexporter/config/config.go @@ -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 diff --git a/exporter/datadogexporter/example/config.yaml b/exporter/datadogexporter/example/config.yaml index c3a830e8c5a8..b78355ef6202 100644 --- a/exporter/datadogexporter/example/config.yaml +++ b/exporter/datadogexporter/example/config.yaml @@ -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: diff --git a/exporter/datadogexporter/factory.go b/exporter/datadogexporter/factory.go index 00a98e81626b..9f60635cf300 100644 --- a/exporter/datadogexporter/factory.go +++ b/exporter/datadogexporter/factory.go @@ -198,6 +198,7 @@ func (f *factory) createTracesExporter( } ctx, cancel := context.WithCancel(ctx) + defer cancel() var pushTracesFn consumer.ConsumeTracesFunc if cfg.OnlyMetadata { @@ -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( diff --git a/exporter/datadogexporter/factory_test.go b/exporter/datadogexporter/factory_test.go index 0baeee002521..5070e5ec7147 100644 --- a/exporter/datadogexporter/factory_test.go +++ b/exporter/datadogexporter/factory_test.go @@ -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{ @@ -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{ @@ -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{ @@ -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{ @@ -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{ @@ -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() @@ -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() diff --git a/exporter/datadogexporter/internal/testutils/test_utils.go b/exporter/datadogexporter/internal/testutils/test_utils.go index 908f84680e58..60663f3e8709 100644 --- a/exporter/datadogexporter/internal/testutils/test_utils.go +++ b/exporter/datadogexporter/internal/testutils/test_utils.go @@ -39,15 +39,25 @@ 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, @@ -55,6 +65,14 @@ func DatadogServerMock() *DatadogServer { } } +// 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"` } @@ -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"` } diff --git a/exporter/datadogexporter/internal/utils/api.go b/exporter/datadogexporter/internal/utils/api.go index 02a8296650ba..05791d7b61fb 100644 --- a/exporter/datadogexporter/internal/utils/api.go +++ b/exporter/datadogexporter/internal/utils/api.go @@ -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" ) @@ -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 } diff --git a/exporter/datadogexporter/metrics_exporter.go b/exporter/datadogexporter/metrics_exporter.go index 3dc1976f913d..90a3a0d6d510 100644 --- a/exporter/datadogexporter/metrics_exporter.go +++ b/exporter/datadogexporter/metrics_exporter.go @@ -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 { diff --git a/exporter/datadogexporter/testdata/config.yaml b/exporter/datadogexporter/testdata/config.yaml index f2961b96c539..a737ef80fe19 100644 --- a/exporter/datadogexporter/testdata/config.yaml +++ b/exporter/datadogexporter/testdata/config.yaml @@ -21,6 +21,7 @@ exporters: api: key: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa site: datadoghq.eu + fail_on_invalid_key: true traces: sample_rate: 1 @@ -63,7 +64,7 @@ exporters: datadog/invalid: metrics: endpoint: "invalid:" - + traces: endpoint: "invalid:" diff --git a/exporter/datadogexporter/traces_exporter.go b/exporter/datadogexporter/traces_exporter.go index 55c8f6df80e6..07aee5d9f807 100644 --- a/exporter/datadogexporter/traces_exporter.go +++ b/exporter/datadogexporter/traces_exporter.go @@ -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 @@ -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