saluki_components/forwarders/datadog/
mod.rs1use agent_data_plane_config::shared::SharedConfiguration;
2use async_trait::async_trait;
3use http::Uri;
4use saluki_common::buf::FrozenChunkedBytesBuffer;
5use saluki_config::GenericConfiguration;
6use saluki_core::accounting::{MemoryBounds, MemoryBoundsBuilder, UsageExpr};
7use saluki_core::{
8 components::{forwarders::*, ComponentContext},
9 data_model::payload::{PayloadMetadata, PayloadType},
10 observability::ComponentMetricsExt as _,
11};
12use saluki_error::GenericError;
13use saluki_metrics::MetricsBuilder;
14use stringtheory::MetaString;
15use tokio::select;
16use tracing::debug;
17
18use crate::common::datadog::{
19 config::ForwarderConfiguration,
20 endpoints::SingleDestination,
21 io::TransactionForwarder,
22 protocol::MetricsPayloadInfo,
23 telemetry::ComponentTelemetry,
24 transaction::{Metadata, Transaction},
25 validation::ValidationReadiness,
26 DEFAULT_INTAKE_COMPRESSED_SIZE_LIMIT, METRICS_SERIES_V3_BETA_PATH, METRICS_SERIES_V3_PATH,
27 METRICS_SKETCHES_V3_PATH,
28};
29
30pub struct DatadogForwarderConfiguration {
36 forwarder_config: ForwarderConfiguration,
40
41 configuration: GenericConfiguration,
43}
44
45impl DatadogForwarderConfiguration {
46 pub fn from_configuration(shared: &SharedConfiguration, config: &GenericConfiguration) -> Self {
50 Self {
51 forwarder_config: ForwarderConfiguration::from_configuration(shared, config),
52 configuration: config.clone(),
53 }
54 }
55
56 pub fn for_endpoint_override(
62 shared: &SharedConfiguration, config: &GenericConfiguration, dd_url: String, api_key: String,
63 api_key_refresh_config_path: &'static str,
64 ) -> Self {
65 let destination = SingleDestination {
66 url: dd_url,
67 api_key,
68 api_key_refresh_config_path: Some(api_key_refresh_config_path),
69 accepts_v3_series: true,
70 };
71
72 Self {
73 forwarder_config: ForwarderConfiguration::for_single_destination(shared, config, &destination),
74 configuration: config.clone(),
75 }
76 }
77}
78
79#[async_trait]
80impl ForwarderBuilder for DatadogForwarderConfiguration {
81 fn input_payload_type(&self) -> PayloadType {
82 PayloadType::Http
83 }
84
85 async fn build(&self, context: ComponentContext) -> Result<Box<dyn Forwarder + Send>, GenericError> {
86 let metrics_builder = MetricsBuilder::from_component_context(&context);
87 let telemetry = ComponentTelemetry::from_builder(&metrics_builder);
88 let forwarder = TransactionForwarder::from_config(
89 context,
90 self.forwarder_config.clone(),
91 Some(self.configuration.clone()),
92 get_dd_endpoint_name,
93 telemetry.clone(),
94 metrics_builder,
95 )?;
96
97 Ok(Box::new(Datadog { forwarder }))
98 }
99}
100
101impl MemoryBounds for DatadogForwarderConfiguration {
102 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
103 builder
104 .minimum()
105 .with_single_value::<Datadog>("component struct")
106 .with_array::<Transaction<FrozenChunkedBytesBuffer>>("requests channel", 8);
107
108 builder
109 .firm()
110 .with_expr(UsageExpr::sum(
117 "in-flight requests",
118 UsageExpr::config(
119 "forwarder_retry_queue_payloads_max_size",
120 self.forwarder_config.retry().queue_max_size_bytes() as usize,
121 ),
122 UsageExpr::product(
123 "high priority queue",
124 UsageExpr::config(
125 "forwarder_high_prio_buffer_size",
126 self.forwarder_config.endpoint_buffer_size(),
127 ),
128 UsageExpr::constant("maximum compressed payload size", DEFAULT_INTAKE_COMPRESSED_SIZE_LIMIT),
131 ),
132 ));
133 }
134}
135
136pub struct Datadog {
137 forwarder: TransactionForwarder<FrozenChunkedBytesBuffer>,
138}
139
140#[async_trait]
141impl Forwarder for Datadog {
142 async fn run(mut self: Box<Self>, mut context: ForwarderContext) -> Result<(), GenericError> {
143 let Self { forwarder } = *self;
144
145 let mut health = context.take_health_handle();
146
147 let mut validation = forwarder.api_key_validator().spawn();
148
149 let forwarder = forwarder.spawn().await;
151
152 debug!("Datadog forwarder started.");
153
154 loop {
155 select! {
156 _ = health.live() => continue,
157 readiness = validation.wait_for_change() => match readiness {
158 ValidationReadiness::Ready => health.mark_ready(),
159 ValidationReadiness::NotReady => health.mark_not_ready(),
160 },
161 maybe_payload = context.payloads().next() => match maybe_payload {
162 Some(payload) => if let Some(http_payload) = payload.try_into_http_payload() {
163 let (payload_meta, request) = http_payload.into_parts();
164 let transaction_meta = transaction_metadata_from_payload_metadata(&payload_meta);
165 let transaction = Transaction::from_original(transaction_meta, request);
166
167 forwarder.send_transaction(transaction).await?;
168 }
169 None => break,
170 },
171 }
172 }
173
174 validation.abort();
176 forwarder.shutdown().await;
177
178 debug!("Datadog forwarder stopped.");
179
180 Ok(())
181 }
182}
183
184fn transaction_metadata_from_payload_metadata(payload_meta: &PayloadMetadata) -> Metadata {
185 let mut transaction_meta =
186 Metadata::from_event_and_data_point_count(payload_meta.event_count(), payload_meta.data_point_count());
187 transaction_meta.payload_info = payload_meta.get::<MetricsPayloadInfo>().copied();
188 transaction_meta
189}
190
191fn get_dd_endpoint_name(uri: &Uri) -> Option<MetaString> {
192 match uri.path() {
193 "/api/v2/logs" => Some(MetaString::from_static("logs_v2")),
194 "/api/v1/series" => Some(MetaString::from_static("series_v1")),
195 "/api/v2/series" => Some(MetaString::from_static("series_v2")),
196 METRICS_SERIES_V3_PATH => Some(MetaString::from_static("series_v3")),
197 METRICS_SERIES_V3_BETA_PATH => Some(MetaString::from_static("series_v3beta")),
198 "/api/beta/sketches" => Some(MetaString::from_static("sketches_v2")),
199 METRICS_SKETCHES_V3_PATH => Some(MetaString::from_static("sketches_v3")),
200 "/api/v1/check_run" => Some(MetaString::from_static("check_run_v1")),
201 "/api/v1/events_batch" => Some(MetaString::from_static("events_batch_v1")),
202 "/api/v0.2/traces" => Some(MetaString::from_static("traces_v0.2")),
203 _ => None,
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use saluki_config::ConfigurationLoader;
210 use serde_json::json;
211
212 use super::*;
213 use crate::common::datadog::test_util::shared_configuration;
214
215 #[test]
216 fn dd_endpoint_names_map_from_request_path() {
217 let cases: [(Uri, Option<&str>); 12] = [
220 (Uri::from_static("/api/v2/logs"), Some("logs_v2")),
221 (Uri::from_static("/api/v1/series"), Some("series_v1")),
222 (Uri::from_static("/api/v2/series"), Some("series_v2")),
223 (Uri::from_static(METRICS_SERIES_V3_PATH), Some("series_v3")),
224 (Uri::from_static(METRICS_SERIES_V3_BETA_PATH), Some("series_v3beta")),
225 (Uri::from_static("/api/beta/sketches"), Some("sketches_v2")),
226 (Uri::from_static(METRICS_SKETCHES_V3_PATH), Some("sketches_v3")),
227 (Uri::from_static("/api/v1/check_run"), Some("check_run_v1")),
228 (Uri::from_static("/api/v1/events_batch"), Some("events_batch_v1")),
229 (Uri::from_static("/api/v0.2/traces"), Some("traces_v0.2")),
230 (
231 Uri::from_static("https://app.datadoghq.com/api/v2/series"),
232 Some("series_v2"),
233 ),
234 (Uri::from_static("/api/v1/unknown"), None),
235 ];
236
237 for (uri, expected) in cases {
238 assert_eq!(
239 expected,
240 get_dd_endpoint_name(&uri).as_deref(),
241 "get_dd_endpoint_name({})",
242 uri.path()
243 );
244 }
245 }
246
247 #[test]
248 fn transaction_metadata_carries_counts_and_metrics_payload_info() {
249 let payload_meta = PayloadMetadata::from_event_and_data_point_count(3, 11);
251 let transaction_meta = transaction_metadata_from_payload_metadata(&payload_meta);
252 assert_eq!(3, transaction_meta.event_count);
253 assert_eq!(11, transaction_meta.data_point_count);
254 assert_eq!(None, transaction_meta.payload_info);
255
256 let payload_meta = PayloadMetadata::from_event_and_data_point_count(2, 7).with(MetricsPayloadInfo::v3_series());
258 let transaction_meta = transaction_metadata_from_payload_metadata(&payload_meta);
259 assert_eq!(2, transaction_meta.event_count);
260 assert_eq!(7, transaction_meta.data_point_count);
261 assert_eq!(Some(MetricsPayloadInfo::v3_series()), transaction_meta.payload_info);
262 }
263
264 #[tokio::test]
265 async fn endpoint_override_refreshes_from_mrf_api_key() {
266 let (generic_config, sender) = ConfigurationLoader::for_tests(
267 Some(json!({
268 "api_key": "primary-api-key",
269 "multi_region_failover": {
270 "api_key": "mrf-api-key"
271 }
272 })),
273 None,
274 true,
275 )
276 .await;
277 let sender = sender.expect("dynamic sender should exist");
278 sender
279 .send(saluki_config::dynamic::ConfigUpdate::snapshot([]))
280 .await
281 .expect("initial dynamic snapshot should be sent");
282 generic_config.ready().await;
283
284 let config = DatadogForwarderConfiguration::for_endpoint_override(
285 &shared_configuration(),
286 &generic_config,
287 "http://mrf.example.test".to_string(),
288 "mrf-api-key".to_string(),
289 "multi_region_failover.api_key",
290 );
291
292 let mut endpoints = config
293 .forwarder_config
294 .build_routable_endpoints(Some(config.configuration.clone()))
295 .expect("endpoint should resolve");
296
297 assert_eq!(endpoints.len(), 1);
298 let (_, mut endpoint) = endpoints.pop().unwrap().into_parts();
299 assert_eq!(endpoint.cached_api_key(), "mrf-api-key");
300 assert!(endpoint.has_configuration());
301 assert_eq!(endpoint.api_key(), "mrf-api-key");
302
303 sender
304 .send(saluki_config::dynamic::ConfigUpdate::Partial(
305 saluki_config::dynamic::ConfigSetting::explicit("api_key", json!("rotated-primary-api-key")),
306 ))
307 .await
308 .expect("primary API key update should be sent");
309 sender
310 .send(saluki_config::dynamic::ConfigUpdate::Partial(
311 saluki_config::dynamic::ConfigSetting::explicit(
312 "multi_region_failover.api_key",
313 json!("rotated-mrf-api-key"),
314 ),
315 ))
316 .await
317 .expect("MRF API key update should be sent");
318
319 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
320 loop {
321 if endpoint.api_key() == "rotated-mrf-api-key" {
322 break;
323 }
324 assert!(
325 std::time::Instant::now() < deadline,
326 "timed out waiting for endpoint override to refresh from MRF API key"
327 );
328 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
329 }
330 }
331}