diff --git a/docs/sources/reference/components/otelcol/otelcol.receiver.datadog.md b/docs/sources/reference/components/otelcol/otelcol.receiver.datadog.md index cc8f0e945ec..6477f51bbb6 100644 --- a/docs/sources/reference/components/otelcol/otelcol.receiver.datadog.md +++ b/docs/sources/reference/components/otelcol/otelcol.receiver.datadog.md @@ -49,6 +49,7 @@ You can use the following arguments with `otelcol.receiver.datadog`: | `keep_alives_enabled` | `boolean` | Whether or not HTTP keep-alives are enabled | `true` | no | | `max_request_body_size` | `string` | Maximum request body size the server will allow. | `"20MiB"` | no | | `read_timeout` | `duration` | Read timeout for requests of the HTTP server. | `"60s"` | no | +| `trace_id_cache_size` | `int` | Cache size for mapping 64-bit to 128-bit trace IDs. | `0` | no | @@ -59,13 +60,16 @@ To expose the HTTP server to other machines on your network, configure `endpoint You can use the following blocks with `otelcol.receiver.datadog`: -| Block | Description | Required | -| -------------------------------- | -------------------------------------------------------------------------- | -------- | -| [`output`][output] | Configures where to send received telemetry data. | yes | -| [`cors`][cors] | Configures CORS for the HTTP server. | no | -| [`debug_metrics`][debug_metrics] | Configures the metrics that this component generates to monitor its state. | no | -| [`tls`][tls] | Configures TLS for the HTTP server. | no | -| `tls` > [`tpm`][tpm] | Configures TPM settings for the TLS `key_file`. | no | +| Block | Description | Required | +| ------------------------------------------- | -------------------------------------------------------------------------- | -------- | +| [`output`][output] | Configures where to send received telemetry data. | yes | +| [`cors`][cors] | Configures CORS for the HTTP server. | no | +| [`debug_metrics`][debug_metrics] | Configures the metrics that this component generates to monitor its state. | no | +| [`intake`][intake] | Configures the `/intake` endpoint behavior. | no | +| `intake` > [`proxy`][proxy] | Configures the proxy for the `/intake` endpoint. | no | +| `intake` > `proxy` > [`api`][api] | Configures the Datadog API connection for the intake proxy. | conditional | +| [`tls`][tls] | Configures TLS for the HTTP server. | no | +| `tls` > [`tpm`][tpm] | Configures TPM settings for the TLS `key_file`. | no | The > symbol indicates deeper levels of nesting. For example, `tls` > `tpm` refers to a `tpm` block defined inside a `tls` block. @@ -74,6 +78,9 @@ For example, `tls` > `tpm` refers to a `tpm` block defined inside a `tls` block. [tpm]: #tpm [cors]: #cors [debug_metrics]: #debug_metrics +[intake]: #intake +[proxy]: #proxy +[api]: #api [output]: #output ### `output` @@ -104,6 +111,41 @@ The following headers are always implicitly allowed: If `allowed_headers` includes `"*"`, all headers are permitted. +### `intake` + +The `intake` block configures how the `/intake` endpoint behaves. +The Datadog Agent uses this endpoint to submit host tags and other metadata. + +The following arguments are supported: + +| Name | Type | Description | Default | Required | +|------------|----------|---------------------------------------------------------|---------|----------| +| `behavior` | `string` | How the `/intake` endpoint behaves: `"disable"` or `"proxy"`. | | yes | + +Set `behavior` to `"proxy"` to forward `/intake` requests to Datadog's API. +Proxying requires a nested `proxy { api { ... } }` block with a Datadog API key. +When set to `"disable"`, the endpoint returns an error for any incoming request. + +### `proxy` + +The `proxy` block configures how the `/intake` proxy operates. +It's only used when `behavior` is set to `"proxy"`. +If `behavior` isn't `"proxy"`, this block is ignored. + +This block has no arguments and is configured with the nested [`api`][api] block. + +### `api` + +The `api` block configures the Datadog API connection used by the intake proxy. + +The following arguments are supported: + +| Name | Type | Description | Default | Required | +|-----------------------|------------|--------------------------------------------------|--------------------|----------| +| `key` | `secret` | Datadog API key. | | yes | +| `site` | `string` | Datadog site to send data to. | `"datadoghq.com"` | no | +| `fail_on_invalid_key` | `bool` | Exit on startup if the API key is invalid. | `false` | no | + ### `debug_metrics` {{< docs/shared lookup="reference/components/otelcol-debug-metrics-block.md" source="alloy" version="" >}} @@ -159,6 +201,29 @@ otelcol.exporter.otlphttp "default" { } ``` +## Proxy `/intake` requests to Datadog + +You can configure the receiver to forward `/intake` requests (host tags and metadata) to Datadog's API while processing metrics and traces locally: + +```alloy +otelcol.receiver.datadog "default" { + intake { + behavior = "proxy" + proxy { + api { + key = sys.env("DD_API_KEY") + site = "datadoghq.eu" + } + } + } + + output { + metrics = [otelcol.processor.batch.default.input] + traces = [otelcol.processor.batch.default.input] + } +} +``` + ## Enable authentication You can create a `otelcol.receiver.datadog` component that requires authentication for requests. diff --git a/internal/component/otelcol/receiver/datadog/datadog.go b/internal/component/otelcol/receiver/datadog/datadog.go index d64a91259f6..7a9e71ff95b 100644 --- a/internal/component/otelcol/receiver/datadog/datadog.go +++ b/internal/component/otelcol/receiver/datadog/datadog.go @@ -2,6 +2,7 @@ package datadog import ( + "fmt" "time" "github.com/grafana/alloy/internal/component" @@ -9,8 +10,11 @@ import ( otelcolCfg "github.com/grafana/alloy/internal/component/otelcol/config" "github.com/grafana/alloy/internal/component/otelcol/receiver" "github.com/grafana/alloy/internal/featuregate" + "github.com/grafana/alloy/syntax/alloytypes" + datadogconfig "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/datadog/config" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver" otelcomponent "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/configopaque" "go.opentelemetry.io/collector/pipeline" ) @@ -31,7 +35,10 @@ func init() { type Arguments struct { HTTPServer otelcol.HTTPServerArguments `alloy:",squash"` - ReadTimeout time.Duration `alloy:"read_timeout,attr,optional"` + ReadTimeout time.Duration `alloy:"read_timeout,attr,optional"` + TraceIDCacheSize int `alloy:"trace_id_cache_size,attr,optional"` + + Intake *IntakeArguments `alloy:"intake,block,optional"` // DebugMetrics configures component internal metrics. Optional. DebugMetrics otelcolCfg.DebugMetricsArguments `alloy:"debug_metrics,block,optional"` @@ -40,8 +47,54 @@ type Arguments struct { Output *otelcol.ConsumerArguments `alloy:"output,block"` } +// IntakeArguments controls the /intake endpoint behavior. +type IntakeArguments struct { + // Behavior is required; allowed values are "disable" or "proxy". + Behavior string `alloy:"behavior,attr"` + Proxy *ProxyArguments `alloy:"proxy,block,optional"` +} + +// ProxyArguments controls how the /intake proxy operates. +type ProxyArguments struct { + API APIArguments `alloy:"api,block"` +} + +// APIArguments configures the Datadog API connection for the intake proxy. +type APIArguments struct { + Key alloytypes.Secret `alloy:"key,attr"` + Site string `alloy:"site,attr,optional"` + FailOnInvalidKey bool `alloy:"fail_on_invalid_key,attr,optional"` +} + var _ receiver.Arguments = Arguments{} +// Validate implements syntax.Validator. +func (args *Arguments) Validate() error { + if args.Intake != nil { + if err := args.Intake.Validate(); err != nil { + return err + } + } + + // Validate the converted upstream config. The upstream Validate() is not + // called automatically by the Alloy receiver framework, so we call it + // explicitly here. This also avoids duplicating upstream validation logic + // (e.g. allowed intake behavior values) which may evolve over time. + cfg, err := args.Convert() + if err != nil { + return err + } + return cfg.(*datadogreceiver.Config).Validate() +} + +// Validate checks IntakeArguments constraints documented in the component reference. +func (args *IntakeArguments) Validate() error { + if args.Behavior == "proxy" && args.Proxy == nil { + return fmt.Errorf("a proxy block with an api block is required when intake behavior is %q", args.Behavior) + } + return nil +} + // SetToDefault implements syntax.Defaulter. func (args *Arguments) SetToDefault() { *args = Arguments{ @@ -61,10 +114,38 @@ func (args Arguments) Convert() (otelcomponent.Config, error) { return nil, err } - return &datadogreceiver.Config{ - ServerConfig: *convertedHttpServer, - ReadTimeout: args.ReadTimeout, - }, nil + cfg := &datadogreceiver.Config{ + ServerConfig: *convertedHttpServer, + ReadTimeout: args.ReadTimeout, + TraceIDCacheSize: args.TraceIDCacheSize, + } + + if args.Intake != nil { + cfg.Intake = args.Intake.Convert() + } + + return cfg, nil +} + +func (args *IntakeArguments) Convert() datadogreceiver.IntakeConfig { + ic := datadogreceiver.IntakeConfig{ + Behavior: args.Behavior, + } + if args.Behavior == "proxy" && args.Proxy != nil { + apiSite := args.Proxy.API.Site + if apiSite == "" { + apiSite = datadogconfig.DefaultSite + } + + ic.Proxy = datadogreceiver.ProxyConfig{ + API: datadogconfig.APIConfig{ + Key: configopaque.String(args.Proxy.API.Key), + Site: apiSite, + FailOnInvalidKey: args.Proxy.API.FailOnInvalidKey, + }, + } + } + return ic } // Extensions implements receiver.Arguments. diff --git a/internal/component/otelcol/receiver/datadog/datadog_test.go b/internal/component/otelcol/receiver/datadog/datadog_test.go index cb004de7292..0632c96cae2 100644 --- a/internal/component/otelcol/receiver/datadog/datadog_test.go +++ b/internal/component/otelcol/receiver/datadog/datadog_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/alloy/syntax" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver" "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/config/configopaque" ) func TestRun(t *testing.T) { @@ -41,7 +42,7 @@ func TestRun(t *testing.T) { } func TestArguments_UnmarshalAlloy(t *testing.T) { - t.Run("grpc", func(t *testing.T) { + t.Run("basic", func(t *testing.T) { httpAddr := componenttest.GetFreeAddr(t) in := fmt.Sprintf(` endpoint = "%s" @@ -67,13 +68,201 @@ func TestArguments_UnmarshalAlloy(t *testing.T) { require.True(t, ok) - // Check the arguments require.Equal(t, otelArgs.Endpoint, httpAddr) require.Equal(t, len(otelArgs.CORS.Get().AllowedOrigins), 2) require.Equal(t, otelArgs.CORS.Get().AllowedOrigins[0], "https://*.test.com") require.Equal(t, otelArgs.CORS.Get().AllowedOrigins[1], "https://test.com") require.Equal(t, otelArgs.ReadTimeout, time.Hour) }) + + t.Run("trace_id_cache_size", func(t *testing.T) { + in := ` + trace_id_cache_size = 500 + + output { /* no-op */ } + ` + var args datadog.Arguments + require.NoError(t, syntax.Unmarshal([]byte(in), &args)) + ext, err := args.Convert() + require.NoError(t, err) + otelArgs := ext.(*datadogreceiver.Config) + require.Equal(t, 500, otelArgs.TraceIDCacheSize) + }) + + t.Run("intake_proxy", func(t *testing.T) { + in := ` + intake { + behavior = "proxy" + proxy { + api { + key = "my-secret-key" + site = "datadoghq.eu" + fail_on_invalid_key = true + } + } + } + + output { /* no-op */ } + ` + var args datadog.Arguments + require.NoError(t, syntax.Unmarshal([]byte(in), &args)) + ext, err := args.Convert() + require.NoError(t, err) + otelArgs := ext.(*datadogreceiver.Config) + + require.Equal(t, "proxy", otelArgs.Intake.Behavior) + require.Equal(t, configopaque.String("my-secret-key"), otelArgs.Intake.Proxy.API.Key) + require.Equal(t, "datadoghq.eu", otelArgs.Intake.Proxy.API.Site) + require.True(t, otelArgs.Intake.Proxy.API.FailOnInvalidKey) + }) + + t.Run("intake_proxy_default_site", func(t *testing.T) { + in := ` + intake { + behavior = "proxy" + proxy { + api { + key = "my-secret-key" + } + } + } + + output { /* no-op */ } + ` + var args datadog.Arguments + require.NoError(t, syntax.Unmarshal([]byte(in), &args)) + ext, err := args.Convert() + require.NoError(t, err) + otelArgs := ext.(*datadogreceiver.Config) + + require.Equal(t, "proxy", otelArgs.Intake.Behavior) + require.Equal(t, configopaque.String("my-secret-key"), otelArgs.Intake.Proxy.API.Key) + require.Equal(t, "datadoghq.com", otelArgs.Intake.Proxy.API.Site) + }) + + t.Run("intake_disable", func(t *testing.T) { + in := ` + intake { + behavior = "disable" + } + + output { /* no-op */ } + ` + var args datadog.Arguments + require.NoError(t, syntax.Unmarshal([]byte(in), &args)) + ext, err := args.Convert() + require.NoError(t, err) + otelArgs := ext.(*datadogreceiver.Config) + + require.Equal(t, "disable", otelArgs.Intake.Behavior) + }) + + t.Run("intake_disable_ignores_proxy_block", func(t *testing.T) { + in := ` + intake { + behavior = "disable" + proxy { + api { + key = "my-secret-key" + site = "datadoghq.eu" + } + } + } + + output { /* no-op */ } + ` + var args datadog.Arguments + require.NoError(t, syntax.Unmarshal([]byte(in), &args)) + ext, err := args.Convert() + require.NoError(t, err) + otelArgs := ext.(*datadogreceiver.Config) + + require.Equal(t, "disable", otelArgs.Intake.Behavior) + require.Equal(t, configopaque.String(""), otelArgs.Intake.Proxy.API.Key) + require.Equal(t, "", otelArgs.Intake.Proxy.API.Site) + require.False(t, otelArgs.Intake.Proxy.API.FailOnInvalidKey) + }) + + t.Run("no_intake", func(t *testing.T) { + in := ` + output { /* no-op */ } + ` + var args datadog.Arguments + require.NoError(t, syntax.Unmarshal([]byte(in), &args)) + ext, err := args.Convert() + require.NoError(t, err) + otelArgs := ext.(*datadogreceiver.Config) + + require.Equal(t, "", otelArgs.Intake.Behavior) + }) +} + +func TestArguments_Validate(t *testing.T) { + // syntax.Unmarshal calls Validate() automatically, so validation errors + // surface at unmarshal time. + + t.Run("invalid_intake_behavior", func(t *testing.T) { + in := ` + intake { + behavior = "bogus" + } + + output { /* no-op */ } + ` + var args datadog.Arguments + err := syntax.Unmarshal([]byte(in), &args) + require.ErrorContains(t, err, `invalid value "bogus"`) + }) + + t.Run("proxy_behavior_without_proxy_block", func(t *testing.T) { + in := ` + intake { + behavior = "proxy" + } + + output { /* no-op */ } + ` + var args datadog.Arguments + err := syntax.Unmarshal([]byte(in), &args) + require.ErrorContains(t, err, `proxy block with an api block is required`) + }) + + t.Run("valid_proxy_config", func(t *testing.T) { + in := ` + intake { + behavior = "proxy" + proxy { + api { + key = "my-secret-key" + } + } + } + + output { /* no-op */ } + ` + var args datadog.Arguments + require.NoError(t, syntax.Unmarshal([]byte(in), &args)) + }) + + t.Run("valid_disable", func(t *testing.T) { + in := ` + intake { + behavior = "disable" + } + + output { /* no-op */ } + ` + var args datadog.Arguments + require.NoError(t, syntax.Unmarshal([]byte(in), &args)) + }) + + t.Run("valid_no_intake", func(t *testing.T) { + in := ` + output { /* no-op */ } + ` + var args datadog.Arguments + require.NoError(t, syntax.Unmarshal([]byte(in), &args)) + }) } func TestDebugMetricsConfig(t *testing.T) {