Skip to main content

vector/sinks/datadog/traces/
config.rs

1use std::sync::{Arc, Mutex};
2
3use indoc::indoc;
4use tokio::sync::oneshot::{Sender, channel};
5use tower::ServiceBuilder;
6use vector_lib::{
7    config::{AcknowledgementsConfig, proxy::ProxyConfig},
8    configurable::configurable_component,
9    stream::BatcherSettings,
10};
11
12use super::{
13    apm_stats::{Aggregator, flush_apm_stats_thread},
14    service::TraceApiRetry,
15};
16use crate::{
17    common::datadog,
18    config::{DynValidatedSink, GenerateConfig, Input, SinkConfig, SinkContext, ValidatedSink},
19    http::HttpClient,
20    sinks::{
21        Healthcheck, VectorSink,
22        datadog::{
23            DatadogCommonConfig, LocalDatadogCommonConfig,
24            traces::{
25                request_builder::DatadogTracesRequestBuilder, service::TraceApiService,
26                sink::TracesSink,
27            },
28        },
29        util::{
30            BatchConfig, Compression, HttpEndpoint, SinkBatchSettings, TowerRequestConfig,
31            service::ServiceBuilderExt,
32        },
33    },
34    tls::{MaybeTlsSettings, TlsEnableableConfig},
35};
36
37// The Datadog API has a hard limit of 3.2MB for uncompressed payloads.
38// Beyond this limit the payload will be ignored, enforcing a slight lower
39// limit as a safety margin.
40pub const BATCH_GOAL_BYTES: usize = 3_000_000;
41pub const BATCH_MAX_EVENTS: usize = 1_000;
42pub const BATCH_DEFAULT_TIMEOUT_SECS: f64 = 10.0;
43
44pub const PAYLOAD_LIMIT: usize = 3_200_000;
45
46#[derive(Clone, Copy, Debug, Default)]
47pub struct DatadogTracesDefaultBatchSettings;
48
49impl SinkBatchSettings for DatadogTracesDefaultBatchSettings {
50    const MAX_EVENTS: Option<usize> = Some(BATCH_MAX_EVENTS);
51    const MAX_BYTES: Option<usize> = Some(BATCH_GOAL_BYTES);
52    const TIMEOUT_SECS: f64 = BATCH_DEFAULT_TIMEOUT_SECS;
53}
54
55/// Configuration for the `datadog_traces` sink.
56#[configurable_component(sink("datadog_traces", "Publish trace events to Datadog."))]
57#[derive(Clone, Debug, Default)]
58#[serde(deny_unknown_fields)]
59pub struct DatadogTracesConfig {
60    #[serde(flatten)]
61    pub local_dd_common: LocalDatadogCommonConfig,
62
63    #[configurable(derived)]
64    #[serde(default)]
65    pub compression: Option<Compression>,
66
67    #[configurable(derived)]
68    #[serde(default)]
69    pub batch: BatchConfig<DatadogTracesDefaultBatchSettings>,
70
71    #[configurable(derived)]
72    #[serde(default)]
73    pub request: TowerRequestConfig,
74}
75
76impl GenerateConfig for DatadogTracesConfig {
77    fn generate_config() -> serde_json::Value {
78        serde_yaml::from_str(indoc! {r#"
79            default_api_key: ${DATADOG_API_KEY_ENV_VAR}
80        "#})
81        .unwrap()
82    }
83}
84
85/// Datadog traces API has two routes: one for traces and another one for stats.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87pub enum DatadogTracesEndpoint {
88    Traces,
89    #[allow(dead_code)] // This will be used when APM stats will be generated
90    APMStats,
91}
92
93/// Store traces & APM stats endpoints actual URIs.
94#[derive(Clone)]
95pub struct DatadogTracesEndpointConfiguration {
96    traces_endpoint: HttpEndpoint,
97    stats_endpoint: HttpEndpoint,
98}
99
100impl DatadogTracesEndpointConfiguration {
101    pub fn get_uri_for_endpoint(&self, endpoint: DatadogTracesEndpoint) -> HttpEndpoint {
102        match endpoint {
103            DatadogTracesEndpoint::Traces => self.traces_endpoint.clone(),
104            DatadogTracesEndpoint::APMStats => self.stats_endpoint.clone(),
105        }
106    }
107}
108
109impl DatadogTracesConfig {
110    fn traces_base_endpoint(endpoint: Option<&str>, site: &str) -> String {
111        endpoint.map_or_else(
112            || format!("https://trace.agent.{site}"),
113            |endpoint| endpoint.to_string(),
114        )
115    }
116
117    fn generate_traces_endpoint_configuration(
118        &self,
119        dd_common: &DatadogCommonConfig,
120    ) -> crate::Result<DatadogTracesEndpointConfiguration> {
121        let base_uri = Self::traces_base_endpoint(dd_common.endpoint.as_deref(), &dd_common.site);
122        let traces_endpoint = build_uri(&base_uri, "/api/v0.2/traces")?;
123        let stats_endpoint = build_uri(&base_uri, "/api/v0.2/stats")?;
124
125        Ok(DatadogTracesEndpointConfiguration {
126            traces_endpoint,
127            stats_endpoint,
128        })
129    }
130
131    pub fn build_sink(
132        &self,
133        dd_common: &DatadogCommonConfig,
134        client: HttpClient,
135        batcher_settings: BatcherSettings,
136    ) -> crate::Result<VectorSink> {
137        let default_api_key: Arc<str> = Arc::from(dd_common.default_api_key.inner());
138        let request_limits = self.request.into_settings();
139        let endpoints = self.generate_traces_endpoint_configuration(dd_common)?;
140
141        let service = ServiceBuilder::new()
142            .settings(request_limits, TraceApiRetry)
143            .service(TraceApiService::new(client.clone()));
144
145        // Object responsible for caching/processing APM stats from incoming trace events.
146        let apm_stats_aggregator =
147            Arc::new(Mutex::new(Aggregator::new(Arc::clone(&default_api_key))));
148
149        let compression = self.compression.unwrap_or_else(Compression::gzip_default);
150
151        let request_builder = DatadogTracesRequestBuilder::new(
152            Arc::clone(&default_api_key),
153            endpoints.clone(),
154            compression,
155            PAYLOAD_LIMIT,
156            Arc::clone(&apm_stats_aggregator),
157        )?;
158
159        // shutdown= Sender that the sink signals when input stream is exhausted.
160        // tripwire= Receiver that APM stats flush thread listens for exit signal on.
161        let (shutdown, tripwire) = channel::<Sender<()>>();
162
163        let sink = TracesSink::new(
164            service,
165            request_builder,
166            batcher_settings,
167            shutdown,
168            endpoints.traces_endpoint.protocol().to_string(),
169        );
170
171        // Send the APM stats payloads independently of the sink framework.
172        // This is necessary to comply with what the APM stats backend of Datadog expects with
173        // respect to receiving stats payloads.
174        crate::spawn_in_current_span(flush_apm_stats_thread(
175            tripwire,
176            client,
177            compression,
178            endpoints,
179            Arc::clone(&apm_stats_aggregator),
180        ));
181
182        Ok(VectorSink::from_event_streamsink(sink))
183    }
184
185    pub fn build_client(&self, proxy: &ProxyConfig) -> crate::Result<HttpClient> {
186        let default_tls_config;
187
188        let tls_settings = MaybeTlsSettings::from_config(
189            Some(match self.local_dd_common.tls.as_ref() {
190                Some(config) => config,
191                None => {
192                    default_tls_config = TlsEnableableConfig::enabled();
193                    &default_tls_config
194                }
195            }),
196            false,
197        )?;
198        Ok(HttpClient::new(tls_settings, proxy)?)
199    }
200}
201
202#[async_trait::async_trait]
203#[typetag::serde(name = "datadog_traces")]
204impl SinkConfig for DatadogTracesConfig {
205    fn input(&self) -> Input {
206        Input::trace()
207    }
208
209    fn acknowledgements(&self) -> &AcknowledgementsConfig {
210        &self.local_dd_common.acknowledgements
211    }
212
213    fn as_dyn_validated(&self) -> Option<&dyn DynValidatedSink> {
214        Some(self)
215    }
216}
217
218#[derive(Clone, Debug)]
219pub struct ValidatedTraces {
220    batcher_settings: BatcherSettings,
221}
222
223#[async_trait::async_trait]
224impl ValidatedSink for DatadogTracesConfig {
225    type Validated = ValidatedTraces;
226
227    fn validate(&self) -> crate::Result<ValidatedTraces> {
228        let batcher_settings = self
229            .batch
230            .validate()?
231            .limit_max_bytes(BATCH_GOAL_BYTES)?
232            .limit_max_events(BATCH_MAX_EVENTS)?
233            .into_batcher_settings()?;
234
235        let site = self
236            .local_dd_common
237            .site
238            .clone()
239            .unwrap_or_else(|| datadog::DD_US_SITE.to_owned());
240        let base = Self::traces_base_endpoint(self.local_dd_common.endpoint.as_deref(), &site);
241        HttpEndpoint::parse(&base)?;
242
243        Ok(ValidatedTraces { batcher_settings })
244    }
245
246    async fn build(
247        &self,
248        validated: &ValidatedTraces,
249        cx: SinkContext,
250    ) -> crate::Result<(VectorSink, Healthcheck)> {
251        let client = self.build_client(&cx.proxy)?;
252        let global = cx.extra_context.get_or_default::<datadog::Options>();
253        let dd_common = self.local_dd_common.with_globals(global)?;
254        let healthcheck = dd_common.build_healthcheck(client.clone())?;
255        let sink = self.build_sink(&dd_common, client, validated.batcher_settings)?;
256
257        Ok((sink, healthcheck))
258    }
259}
260
261fn build_uri(host: &str, endpoint: &str) -> crate::Result<HttpEndpoint> {
262    Ok(HttpEndpoint::parse(host)?.append_path(endpoint)?)
263}
264
265#[cfg(test)]
266mod test {
267    use super::{BATCH_GOAL_BYTES, BATCH_MAX_EVENTS, DatadogTracesConfig};
268    use crate::{config::ValidatedSink, sinks::datadog::LocalDatadogCommonConfig};
269
270    #[test]
271    fn generate_config() {
272        crate::test_util::test_generate_config::<DatadogTracesConfig>();
273    }
274
275    #[test]
276    fn validate_produces_usable_batch_settings() {
277        let config = DatadogTracesConfig::default();
278        let validated = config.validate().expect("validation should succeed");
279        assert_eq!(validated.batcher_settings.size_limit, BATCH_GOAL_BYTES);
280        assert_eq!(validated.batcher_settings.item_limit, BATCH_MAX_EVENTS);
281    }
282
283    #[test]
284    fn validate_rejects_malformed_endpoint() {
285        let config = DatadogTracesConfig {
286            local_dd_common: LocalDatadogCommonConfig::new(
287                Some("not a uri".to_string()),
288                None,
289                None,
290            ),
291            ..Default::default()
292        };
293        assert!(config.validate().is_err());
294    }
295
296    #[test]
297    fn validate_rejects_non_http_scheme() {
298        let config = DatadogTracesConfig {
299            local_dd_common: LocalDatadogCommonConfig::new(
300                Some("ftp://localhost:8080".to_string()),
301                None,
302                None,
303            ),
304            ..Default::default()
305        };
306        assert!(config.validate().is_err());
307    }
308}