saluki_components/forwarders/datadog/
mod.rs1use std::{collections::HashMap, sync::Arc};
2
3use agent_data_plane_config::shared::{Secrets, SharedConfiguration};
4use agent_data_plane_config::Live;
5use async_trait::async_trait;
6use http::Uri;
7use saluki_common::buf::FrozenChunkedBytesBuffer;
8use saluki_core::accounting::{MemoryBounds, MemoryBoundsBuilder, UsageExpr};
9use saluki_core::{
10 components::{forwarders::*, BuildContext},
11 data_model::payload::{PayloadMetadata, PayloadType},
12 observability::ComponentMetricsExt as _,
13};
14use saluki_error::GenericError;
15use saluki_metrics::MetricsBuilder;
16use stringtheory::MetaString;
17use tokio::select;
18use tracing::debug;
19
20use crate::common::datadog::{
21 api_key::{ApiKeyView, LiveApiKeys},
22 config::ForwarderConfiguration,
23 endpoints::SingleDestination,
24 io::{LiveForwarderConfiguration, TransactionForwarder},
25 protocol::{MetricsEndpointRouting, MetricsPayloadInfo},
26 telemetry::ComponentTelemetry,
27 transaction::{Metadata, Transaction},
28 validation::ValidationReadiness,
29 DEFAULT_INTAKE_COMPRESSED_SIZE_LIMIT, METRICS_SERIES_V3_BETA_PATH, METRICS_SERIES_V3_PATH,
30 METRICS_SKETCHES_V3_PATH,
31};
32
33pub struct DatadogForwarderConfiguration {
39 forwarder_config: ForwarderConfiguration,
43
44 api_keys: LiveApiKeys,
46
47 secrets: Live<Secrets>,
49}
50
51impl DatadogForwarderConfiguration {
52 pub fn from_configuration(
58 shared: &SharedConfiguration, api_key: Live<String>, additional_endpoints: Live<HashMap<String, Vec<String>>>,
59 secrets: Live<Secrets>,
60 ) -> Self {
61 Self {
62 forwarder_config: ForwarderConfiguration::from_configuration(shared),
63 api_keys: LiveApiKeys {
64 primary: Some(ApiKeyView::Required(api_key)),
65 additional: Some(additional_endpoints),
66 },
67 secrets,
68 }
69 }
70
71 pub fn for_endpoint_override(
78 shared: &SharedConfiguration, dd_url: String, api_key: String, api_key_view: Live<Option<String>>,
79 secrets: Live<Secrets>,
80 ) -> Self {
81 let destination = SingleDestination {
82 url: dd_url,
83 api_key,
84 accepts_v3_series: true,
85 };
86
87 Self {
88 forwarder_config: ForwarderConfiguration::for_single_destination(shared, &destination),
89 api_keys: LiveApiKeys {
90 primary: Some(ApiKeyView::Optional(api_key_view)),
91 additional: None,
92 },
93 secrets,
94 }
95 }
96}
97
98#[async_trait]
99impl ForwarderBuilder for DatadogForwarderConfiguration {
100 fn input_payload_type(&self) -> PayloadType {
101 PayloadType::Http
102 }
103
104 async fn build(&self, context: BuildContext) -> Result<Box<dyn Forwarder + Send>, GenericError> {
105 let metrics_builder = MetricsBuilder::from_component_context(context.component_context());
106 let telemetry = ComponentTelemetry::from_builder(&metrics_builder);
107 let forwarder = TransactionForwarder::from_config(
108 context.component_context().clone(),
109 self.forwarder_config.clone(),
110 LiveForwarderConfiguration {
111 api_keys: self.api_keys.clone(),
112 secrets: self.secrets.clone(),
113 },
114 get_dd_endpoint_name,
115 telemetry.clone(),
116 metrics_builder,
117 )?;
118
119 Ok(Box::new(Datadog { forwarder }))
120 }
121}
122
123impl MemoryBounds for DatadogForwarderConfiguration {
124 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
125 builder
126 .minimum()
127 .with_single_value::<Datadog>("component struct")
128 .with_array::<Transaction<FrozenChunkedBytesBuffer>>("requests channel", 8);
129
130 builder
131 .firm()
132 .with_expr(UsageExpr::sum(
139 "in-flight requests",
140 UsageExpr::config(
141 "forwarder_retry_queue_payloads_max_size",
142 self.forwarder_config.retry().queue_max_size_bytes() as usize,
143 ),
144 UsageExpr::product(
145 "high priority queue",
146 UsageExpr::config(
147 "forwarder_high_prio_buffer_size",
148 self.forwarder_config.endpoint_buffer_size(),
149 ),
150 UsageExpr::constant("maximum compressed payload size", DEFAULT_INTAKE_COMPRESSED_SIZE_LIMIT),
153 ),
154 ));
155 }
156}
157
158pub struct Datadog {
159 forwarder: TransactionForwarder<FrozenChunkedBytesBuffer>,
160}
161
162#[async_trait]
163impl Forwarder for Datadog {
164 async fn run(mut self: Box<Self>, mut context: ForwarderContext) -> Result<(), GenericError> {
165 let Self { forwarder } = *self;
166
167 let mut health = context.take_health_handle();
168
169 let mut validation = forwarder.api_key_validator().spawn();
172 let forwarder = forwarder.spawn();
173
174 debug!("Datadog forwarder started.");
175
176 loop {
177 select! {
178 _ = health.live() => continue,
179 readiness = validation.wait_for_change() => match readiness {
180 ValidationReadiness::Ready => health.mark_ready(),
181 ValidationReadiness::NotReady => health.mark_not_ready(),
182 },
183 maybe_payload = context.payloads().next() => match maybe_payload {
184 Some(payload) => if let Some(http_payload) = payload.try_into_http_payload() {
185 let (payload_meta, request) = http_payload.into_parts();
186 let transaction_meta = transaction_metadata_from_payload_metadata(&payload_meta);
187 let transaction = Transaction::from_original(transaction_meta, request);
188
189 forwarder.send_transaction(transaction).await?;
190 }
191 None => break,
192 },
193 }
194 }
195
196 forwarder.shutdown();
199 drop(validation);
200
201 debug!("Datadog forwarder stopped.");
202
203 Ok(())
204 }
205}
206
207fn transaction_metadata_from_payload_metadata(payload_meta: &PayloadMetadata) -> Metadata {
208 let mut transaction_meta =
209 Metadata::from_event_and_data_point_count(payload_meta.event_count(), payload_meta.data_point_count());
210 transaction_meta.payload_info = payload_meta.get::<MetricsPayloadInfo>().copied();
211 transaction_meta.metrics_endpoint_routing = payload_meta.get::<Arc<MetricsEndpointRouting>>().cloned();
212 transaction_meta
213}
214
215fn get_dd_endpoint_name(uri: &Uri) -> Option<MetaString> {
216 match uri.path() {
217 "/api/v2/logs" => Some(MetaString::from_static("logs_v2")),
218 "/api/v1/series" => Some(MetaString::from_static("series_v1")),
219 "/api/v2/series" => Some(MetaString::from_static("series_v2")),
220 METRICS_SERIES_V3_PATH => Some(MetaString::from_static("series_v3")),
221 METRICS_SERIES_V3_BETA_PATH => Some(MetaString::from_static("series_v3beta")),
222 "/api/beta/sketches" => Some(MetaString::from_static("sketches_v2")),
223 METRICS_SKETCHES_V3_PATH => Some(MetaString::from_static("sketches_v3")),
224 "/api/v1/check_run" => Some(MetaString::from_static("check_run_v1")),
225 "/api/v1/events_batch" => Some(MetaString::from_static("events_batch_v1")),
226 "/api/v0.2/traces" => Some(MetaString::from_static("traces_v0.2")),
227 _ => None,
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use agent_data_plane_config::SalukiConfiguration;
234 use saluki_core::components::test_util::TestComponentSupervisor;
235
236 use super::*;
237 use crate::common::datadog::{
238 api_key::ApiKeyRefresher,
239 test_util::{shared_configuration, LiveConfiguration},
240 };
241
242 #[test]
243 fn dd_endpoint_names_map_from_request_path() {
244 let cases: [(Uri, Option<&str>); 12] = [
247 (Uri::from_static("/api/v2/logs"), Some("logs_v2")),
248 (Uri::from_static("/api/v1/series"), Some("series_v1")),
249 (Uri::from_static("/api/v2/series"), Some("series_v2")),
250 (Uri::from_static(METRICS_SERIES_V3_PATH), Some("series_v3")),
251 (Uri::from_static(METRICS_SERIES_V3_BETA_PATH), Some("series_v3beta")),
252 (Uri::from_static("/api/beta/sketches"), Some("sketches_v2")),
253 (Uri::from_static(METRICS_SKETCHES_V3_PATH), Some("sketches_v3")),
254 (Uri::from_static("/api/v1/check_run"), Some("check_run_v1")),
255 (Uri::from_static("/api/v1/events_batch"), Some("events_batch_v1")),
256 (Uri::from_static("/api/v0.2/traces"), Some("traces_v0.2")),
257 (
258 Uri::from_static("https://app.datadoghq.com/api/v2/series"),
259 Some("series_v2"),
260 ),
261 (Uri::from_static("/api/v1/unknown"), None),
262 ];
263
264 for (uri, expected) in cases {
265 assert_eq!(
266 expected,
267 get_dd_endpoint_name(&uri).as_deref(),
268 "get_dd_endpoint_name({})",
269 uri.path()
270 );
271 }
272 }
273
274 #[test]
275 fn transaction_metadata_carries_counts_and_metrics_payload_info() {
276 let payload_meta = PayloadMetadata::from_event_and_data_point_count(3, 11);
278 let transaction_meta = transaction_metadata_from_payload_metadata(&payload_meta);
279 assert_eq!(3, transaction_meta.event_count);
280 assert_eq!(11, transaction_meta.data_point_count);
281 assert_eq!(None, transaction_meta.payload_info);
282 assert_eq!(None, transaction_meta.metrics_endpoint_routing);
283
284 let endpoint_routing = Arc::new(MetricsEndpointRouting::Only(
286 ["https://secondary.example.com".to_string()].into(),
287 ));
288 let payload_meta = PayloadMetadata::from_event_and_data_point_count(2, 7)
289 .with(MetricsPayloadInfo::v3_series())
290 .with(Arc::clone(&endpoint_routing));
291 let transaction_meta = transaction_metadata_from_payload_metadata(&payload_meta);
292 assert_eq!(2, transaction_meta.event_count);
293 assert_eq!(7, transaction_meta.data_point_count);
294 assert_eq!(Some(MetricsPayloadInfo::v3_series()), transaction_meta.payload_info);
295 assert!(Arc::ptr_eq(
296 transaction_meta.metrics_endpoint_routing.as_ref().unwrap(),
297 &endpoint_routing
298 ));
299 let cloned_meta = transaction_meta.clone();
300 assert!(Arc::ptr_eq(
301 cloned_meta.metrics_endpoint_routing.as_ref().unwrap(),
302 &endpoint_routing
303 ));
304 }
305
306 #[tokio::test]
307 async fn an_endpoint_override_follows_the_failover_key_and_not_the_primary_one() {
308 let mut live_config = SalukiConfiguration::default();
309 live_config.shared.endpoints.api_key = "primary-api-key".to_string();
310 live_config.domains.multi_region_failover.api_key = Some("mrf-api-key".to_string());
311 let live = LiveConfiguration::new(live_config.clone());
312
313 let config = DatadogForwarderConfiguration::for_endpoint_override(
314 &shared_configuration(),
315 "http://mrf.example.test".to_string(),
316 "mrf-api-key".to_string(),
317 live.live(|config| &config.domains.multi_region_failover.api_key),
318 live.live(|config| &config.shared.secrets),
319 );
320
321 let endpoints = config
322 .forwarder_config
323 .build_routable_endpoints()
324 .expect("endpoint should resolve");
325 assert_eq!(endpoints.len(), 1);
326 assert_eq!("mrf-api-key", &*endpoints[0].endpoint().api_key());
327
328 let refresher = ApiKeyRefresher::new(&endpoints, &config.api_keys)
329 .expect("the destination should follow the failover view");
330
331 let supervisor = TestComponentSupervisor::start("test_forwarder").await;
334 supervisor.scope(async move { refresher.spawn() }).await;
335
336 live_config.shared.endpoints.api_key = "rotated-primary-api-key".to_string();
338 live.store(live_config.clone());
339
340 live_config.domains.multi_region_failover.api_key = Some("rotated-mrf-api-key".to_string());
341 live.store(live_config);
342
343 let endpoint = endpoints[0].endpoint();
344 tokio::time::timeout(std::time::Duration::from_secs(2), async {
345 while &*endpoint.api_key() != "rotated-mrf-api-key" {
346 tokio::task::yield_now().await;
347 }
348 })
349 .await
350 .expect("the endpoint override should follow the failover key");
351 }
352}