vector/sinks/datadog/traces/
config.rs1use 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
37pub 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87pub enum DatadogTracesEndpoint {
88 Traces,
89 #[allow(dead_code)] APMStats,
91}
92
93#[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 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 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 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}