1use std::sync::Arc;
5
6use agent_data_plane_config::{Live, SalukiConfiguration};
7use arc_swap::ArcSwap;
8use datadog_agent_config::{DatadogConfiguration, TranslateErrors};
9use saluki_config::dynamic::ConfigUpdate;
10use saluki_config::{ConfigurationError, GenericConfiguration};
11use serde::Deserialize;
12use serde_json::Value;
13use snafu::Snafu;
14use tokio::sync::{mpsc, watch};
15use tracing::{debug, warn};
16
17use crate::saluki_only::SalukiOnly;
18use crate::source::SourceTree;
19use crate::translators::DatadogTranslator;
20
21#[derive(Debug, Snafu)]
23pub enum Error {
24 #[snafu(context(false), display("{source}"))]
26 Source {
27 source: ConfigurationError,
29 },
30
31 #[snafu(context(false), display("{source}"))]
33 Deserialize {
34 source: serde_json::Error,
36 },
37
38 #[snafu(display("configuration stream closed before the initial snapshot"))]
40 StreamClosed,
41
42 #[snafu(display("failed to build the configuration base: {message}"))]
44 Base {
45 message: String,
47 },
48
49 #[snafu(display("{source}"))]
51 Translate {
52 source: TranslateErrors,
54 },
55
56 #[snafu(display(
58 "no Datadog API key is configured: set `api_key` in the Datadog Agent's configuration, or \
59 `DD_API_KEY` in the environment. Every payload is authenticated with this key, so nothing can \
60 be submitted without one"
61 ))]
62 MissingApiKey,
63}
64
65type Result<T> = std::result::Result<T, Error>;
66
67pub struct ConfigurationSystem {
76 raw_map: GenericConfiguration,
77 current: Arc<ArcSwap<SalukiConfiguration>>,
78 tick: Arc<watch::Sender<()>>,
82}
83
84impl ConfigurationSystem {
85 pub(crate) async fn connected(
99 mut agent_rx: mpsc::Receiver<ConfigUpdate>, compat_tx: mpsc::Sender<ConfigUpdate>,
100 compat_map: GenericConfiguration, base: SourceTree,
101 ) -> Result<Self> {
102 let first = agent_rx.recv().await.ok_or(Error::StreamClosed)?;
104
105 let mut agent = SourceTree::empty();
108 fold(&mut agent, &first);
109 forward(&compat_tx, first).await;
110 compat_map.ready().await;
111
112 let config = translate_authoritative(&base.overlay(&agent))?;
117
118 let current = Arc::new(ArcSwap::from_pointee(config));
119 let (tick, _) = watch::channel(());
122 let tick = Arc::new(tick);
123
124 tokio::spawn(agent_loop(
125 agent_rx,
126 compat_tx,
127 base,
128 agent,
129 Arc::clone(¤t),
130 Arc::clone(&tick),
131 ));
132
133 Ok(Self {
134 raw_map: compat_map,
135 current,
136 tick,
137 })
138 }
139
140 pub(crate) fn standalone(compat_map: GenericConfiguration, config: SalukiConfiguration) -> Self {
144 let current = Arc::new(ArcSwap::from_pointee(config));
145 let (tick, _) = watch::channel(());
146 Self {
147 raw_map: compat_map,
148 current,
149 tick: Arc::new(tick),
150 }
151 }
152
153 pub fn live<T>(&self, project: impl for<'a> Fn(&'a SalukiConfiguration) -> &'a T + Send + Sync + 'static) -> Live<T>
156 where
157 T: Clone + PartialEq + 'static,
158 {
159 Live::new_dynamic(Arc::clone(&self.current), self.tick.subscribe(), project)
160 }
161
162 pub fn config(&self) -> arc_swap::Guard<Arc<SalukiConfiguration>> {
166 self.current.load()
167 }
168
169 pub fn current_handle(&self) -> Arc<ArcSwap<SalukiConfiguration>> {
172 Arc::clone(&self.current)
173 }
174
175 pub fn raw_map(&self) -> GenericConfiguration {
177 self.raw_map.clone()
178 }
179}
180
181async fn agent_loop(
188 mut agent_rx: mpsc::Receiver<ConfigUpdate>, compat_tx: mpsc::Sender<ConfigUpdate>, base: SourceTree,
189 mut agent: SourceTree, current: Arc<ArcSwap<SalukiConfiguration>>, tick: Arc<watch::Sender<()>>,
190) {
191 while let Some(update) = agent_rx.recv().await {
192 let mut tentative = agent.clone();
196 fold(&mut tentative, &update);
197 match translate_authoritative(&base.overlay(&tentative)) {
198 Ok(config) => {
199 agent = tentative;
200 current.store(Arc::new(config));
201 tick.send_replace(());
202 debug!("Applied configuration update.");
203 }
204 Err(e) => warn!(
205 error = %e,
206 "Rejected configuration update; keeping the last-known-good typed configuration. The \
207 compatibility map still receives this update, so an un-migrated component may act on \
208 a value the typed model rejected."
209 ),
210 }
211 forward(&compat_tx, update).await;
216 }
217}
218
219fn fold(agent: &mut SourceTree, update: &ConfigUpdate) {
228 match update {
229 ConfigUpdate::Snapshot(settings) => *agent = SourceTree::from_settings(settings),
230 ConfigUpdate::Partial(setting) => agent.set(setting),
231 }
232}
233
234async fn forward(compat_tx: &mpsc::Sender<ConfigUpdate>, update: ConfigUpdate) {
236 let _ = compat_tx.send(update).await;
237}
238
239pub(crate) fn translate_strict(merged: &SourceTree) -> Result<SalukiConfiguration> {
245 let Sources { datadog, saluki } = deserialize_sources(&merged.to_value())?;
246 let (config, errors) = translate(&datadog, &saluki, merged);
247 if let Some(errors) = errors {
248 return Err(Error::Translate { source: errors });
249 }
250 Ok(config)
251}
252
253pub(crate) fn translate_authoritative(merged: &SourceTree) -> Result<SalukiConfiguration> {
263 let config = translate_strict(merged)?;
264 validate(&config)?;
265 Ok(config)
266}
267
268pub(crate) fn validate(config: &SalukiConfiguration) -> Result<()> {
287 if config.shared.endpoints.api_key.trim().is_empty() {
290 return Err(Error::MissingApiKey);
291 }
292
293 Ok(())
294}
295
296struct Sources {
303 datadog: DatadogConfiguration,
304 saluki: SalukiOnly,
305}
306
307fn deserialize_sources(merged: &Value) -> Result<Sources> {
314 let saluki = SalukiOnly::deserialize(merged)?;
315 let datadog = DatadogConfiguration::deserialize(merged)?;
316 Ok(Sources { datadog, saluki })
317}
318
319fn translate(
331 datadog: &DatadogConfiguration, saluki: &SalukiOnly, sources: &SourceTree,
332) -> (SalukiConfiguration, Option<TranslateErrors>) {
333 let (mut config, errors) = DatadogTranslator::new(datadog, sources).translate();
334 saluki.seed(&mut config);
335 (config, errors)
336}
337
338#[cfg(test)]
339mod tests {
340 use std::time::Duration;
341
342 use agent_data_plane_config::domains::dogstatsd::OriginTagCardinality;
343 use agent_data_plane_config::shared::V3SeriesMode;
344 use agent_data_plane_config::Provenance;
345 use agent_data_plane_config::{Live, SalukiConfiguration};
346 use datadog_agent_config::DatadogConfiguration;
347 use saluki_config::dynamic::{ConfigSetting, ConfigUpdate, Provenance as StreamProvenance};
348 use saluki_config::ConfigurationLoader;
349 use serde_json::{json, Value};
350 use tokio::sync::mpsc;
351
352 use super::{
353 translate, translate_authoritative, translate_strict, ConfigurationSystem, Error, SalukiOnly, SourceTree,
354 };
355
356 const TEST_API_KEY: &str = "test-api-key";
362
363 async fn standalone_system(
368 file: Option<Value>, env: Option<&[(String, String)]>,
369 ) -> Result<ConfigurationSystem, Error> {
370 let (compat_map, _) = ConfigurationLoader::for_tests(file, env, false).await;
371 let base = SourceTree::all_explicit(compat_map.as_typed::<Value>().expect("base extracts"));
372 let config = translate_strict(&base)?;
373 Ok(ConfigurationSystem::standalone(compat_map, config))
374 }
375
376 async fn connected_system(mut base: Value) -> (ConfigurationSystem, mpsc::Sender<ConfigUpdate>) {
383 let (agent_tx, agent_rx) = mpsc::channel(100);
384 let (compat_map, compat_tx) = ConfigurationLoader::for_tests(None, None, true).await;
385 let compat_tx = compat_tx.expect("dynamic sender exists");
386 agent_tx.send(ConfigUpdate::snapshot([])).await.unwrap();
387 if let Some(base) = base.as_object_mut() {
388 base.entry("api_key").or_insert(json!(TEST_API_KEY));
389 }
390 let base = SourceTree::all_explicit(base);
391 let system = ConfigurationSystem::connected(agent_rx, compat_tx, compat_map, base)
392 .await
393 .expect("system builds");
394 (system, agent_tx)
395 }
396
397 async fn await_config(system: &ConfigurationSystem, what: &str, predicate: impl Fn(&SalukiConfiguration) -> bool) {
399 tokio::time::timeout(Duration::from_secs(2), async {
400 while !predicate(&system.config()) {
401 tokio::time::sleep(Duration::from_millis(5)).await;
402 }
403 })
404 .await
405 .unwrap_or_else(|_| panic!("timed out waiting for {what}"));
406 }
407
408 #[tokio::test]
409 async fn startup_current_reflects_translation() {
410 let system = standalone_system(Some(json!({ "log_level": "warn", "dogstatsd_port": 9125 })), None)
411 .await
412 .expect("system builds");
413 let config = system.config();
414
415 assert_eq!(config.control.logging.level, "warn");
416 assert_eq!(config.domains.dogstatsd.listeners.port, 9125);
417 }
418
419 #[test]
420 fn boolean_use_v3_api_series_enabled_is_normalized() {
421 let sources = SourceTree::all_explicit(json!({
422 "use_v3_api": {
423 "series": {
424 "enabled": true
425 }
426 }
427 }));
428
429 let config = translate_strict(&sources).expect("a boolean V3 series mode should translate");
430
431 assert_eq!(config.shared.metrics_encoding.v3_series_mode, V3SeriesMode::Enabled);
432 }
433
434 #[tokio::test]
435 async fn connected_stream_translates_metrics_v3_routing_configuration() {
436 let (system, agent_tx) = connected_system(json!({
437 "data_plane": {
438 "metrics": {
439 "v3": {
440 "series": {
441 "enabled": true
442 }
443 }
444 }
445 }
446 }))
447 .await;
448
449 assert_eq!(
450 system.config().shared.metrics_encoding.v3_series_mode,
451 V3SeriesMode::DatadogOnly
452 );
453
454 agent_tx
455 .send(ConfigUpdate::snapshot([
456 ConfigSetting::explicit("serializer_compressor_kind", json!("zstd")),
457 ConfigSetting::explicit("serializer_experimental_use_v3_api.compression_level", json!(7)),
458 ConfigSetting::explicit(
459 "serializer_experimental_use_v3_api.series.endpoints",
460 json!(["https://app.us3.datadoghq.com"]),
461 ),
462 ConfigSetting::explicit("use_v2_api.series", json!(false)),
463 ConfigSetting::explicit("use_v3_api.series.enabled", json!("false")),
464 ConfigSetting::explicit(
466 "use_v3_api.series.endpoints",
467 json!({ "https://app.datadoghq.com": "true" }),
468 ),
469 ConfigSetting::explicit("observability_pipelines_worker.metrics.enabled", json!(true)),
470 ConfigSetting::explicit(
471 "observability_pipelines_worker.metrics.url",
472 json!("https://opw.example.com"),
473 ),
474 ConfigSetting::explicit("observability_pipelines_worker.metrics.use_v3_api.series", json!(true)),
475 ]))
476 .await
477 .unwrap();
478
479 await_config(&system, "the streamed metrics V3 routing configuration", |config| {
480 config.shared.metrics_encoding.v3_series_mode == V3SeriesMode::Disabled
481 && config
482 .shared
483 .metrics_encoding
484 .v3_series_endpoint_modes
485 .get("https://app.datadoghq.com")
486 == Some(&V3SeriesMode::Enabled)
487 })
488 .await;
489
490 let config = system.config();
491 let metrics = &config.shared.metrics_encoding;
492 assert!(!metrics.use_v2_series_api);
493 assert_eq!(metrics.v3_api.compression_level, 7);
494 assert_eq!(metrics.v3_api.series.endpoints, vec!["https://app.us3.datadoghq.com"]);
495 let opw = &config.shared.endpoints.opw_intake;
496 assert!(opw.enabled);
497 assert_eq!(opw.url, "https://opw.example.com");
498 assert!(opw.use_v3_series);
499 }
500
501 #[tokio::test]
502 async fn nested_datadog_key_reaches_the_model() {
503 let system = standalone_system(
507 Some(json!({
508 "autoscaling": {
509 "failover": {
510 "enabled": true,
511 "metrics": "container.memory.usage container.cpu.usage",
512 }
513 }
514 })),
515 None,
516 )
517 .await
518 .expect("system builds");
519 let config = system.config();
520
521 assert!(config.shared.autoscaling_failover.enabled);
522 assert_eq!(
523 config.shared.autoscaling_failover.metrics,
524 vec!["container.memory.usage".to_string(), "container.cpu.usage".to_string()]
525 );
526 }
527
528 #[tokio::test]
529 async fn nested_saluki_only_key_seeds_the_model() {
530 let system = standalone_system(Some(json!({ "data_plane": { "standalone_mode": true } })), None)
531 .await
532 .expect("system builds");
533
534 assert!(system.config().control.standalone_mode);
535 }
536
537 #[tokio::test]
538 async fn a_flattened_spelling_of_a_nested_key_is_not_read() {
539 let system = standalone_system(Some(json!({ "autoscaling_failover_enabled": true })), None)
544 .await
545 .expect("system builds");
546
547 assert!(!system.config().shared.autoscaling_failover.enabled);
548 }
549
550 #[tokio::test]
551 async fn load_fails_on_translation_invalid_startup_config() {
552 let result = standalone_system(Some(json!({ "dogstatsd_tag_cardinality": "bogus" })), None).await;
555
556 assert!(matches!(result, Err(Error::Translate { .. })));
557 }
558
559 #[tokio::test]
560 async fn negative_dogstatsd_workers_count_is_rejected_at_startup() {
561 let result = standalone_system(Some(json!({ "dogstatsd_workers_count": -1 })), None).await;
562
563 let Err(error) = result else {
564 panic!("negative worker count should fail the startup translation gate");
565 };
566 assert!(matches!(error, Error::Translate { .. }));
567 assert!(error.to_string().contains("dogstatsd_workers_count"));
568 assert!(error.to_string().contains("greater than or equal to 0"));
569 }
570
571 #[test]
572 fn zero_otlp_trace_interner_size_is_rejected() {
573 let sources = SourceTree::all_explicit(json!({ "otlp_config": { "traces": { "string_interner_size": 0 } } }));
576 let error = translate_strict(&sources).expect_err("zero trace interner size should fail translation");
577
578 assert!(matches!(error, Error::Deserialize { .. }));
579 assert!(error.to_string().contains("value of bytes must be greater than zero"));
580 }
581
582 #[test]
583 fn oversized_otlp_trace_interner_size_is_rejected() {
584 let sources =
585 SourceTree::all_explicit(json!({ "otlp_config": { "traces": { "string_interner_size": "2GiB" } } }));
586 let error = translate_strict(&sources).expect_err("oversized trace interner should fail translation");
587
588 assert!(matches!(error, Error::Deserialize { .. }));
589 assert!(error.to_string().contains("must not exceed 1073741824 bytes"));
590 }
591
592 #[test]
593 fn positive_otlp_trace_interner_size_is_accepted() {
594 let sources =
595 SourceTree::all_explicit(json!({ "otlp_config": { "traces": { "string_interner_size": "512KiB" } } }));
596 let config = translate_strict(&sources).expect("positive trace interner size should translate");
597
598 assert_eq!(config.domains.otlp.traces.string_interner_size.get(), 512 * 1024);
599 }
600
601 #[test]
602 fn invalid_metric_tag_value_allowlist_is_rejected_before_publication() {
603 let sources = SourceTree::all_explicit(json!({
604 "metric_tag_value_allowlist": [
605 { "metric_prefix": "requests.", "tag_name": "customer_id" },
606 { "metric_prefix": "requests.api.", "tag_name": "customer_id" }
607 ]
608 }));
609 let error = translate_strict(&sources).expect_err("overlapping allow-list prefixes should fail translation");
610
611 assert!(matches!(error, Error::Deserialize { .. }));
612 assert!(error.to_string().contains("overlapping metric prefixes"));
613 }
614
615 #[tokio::test]
616 async fn standalone_loads_numeric_byte_size() {
617 let system = standalone_system(Some(json!({ "dogstatsd_log_file_max_size": 10485760 })), None)
621 .await
622 .expect("numeric byte size boots");
623
624 assert_eq!(system.config().domains.dogstatsd.debug_log.log_file_max_size, 10485760);
625 }
626
627 #[test]
628 fn a_configuration_without_an_api_key_is_rejected() {
629 let sources = SourceTree::all_explicit(json!({}));
633 translate_strict(&sources).expect("an absent API key still translates");
634
635 let error = translate_authoritative(&sources).expect_err("an absent API key should fail the gate");
636
637 assert!(matches!(error, Error::MissingApiKey));
638 assert!(error.to_string().contains("api_key"));
639 }
640
641 #[test]
642 fn a_blank_api_key_is_rejected() {
643 for key in ["", " ", "\t\n"] {
646 let sources = SourceTree::all_explicit(json!({ "api_key": key }));
647
648 assert!(
649 matches!(translate_authoritative(&sources), Err(Error::MissingApiKey)),
650 "{key:?} should not count as an API key"
651 );
652 }
653
654 let sources = SourceTree::all_explicit(json!({ "api_key": TEST_API_KEY }));
655 assert_eq!(
656 TEST_API_KEY,
657 translate_authoritative(&sources)
658 .expect("a real key passes the gate")
659 .shared
660 .endpoints
661 .api_key
662 );
663 }
664
665 #[tokio::test]
666 async fn an_update_that_blanks_the_api_key_is_rejected_keeping_last_known_good() {
667 let (system, agent_tx) = connected_system(json!({ "log_level": "warn" })).await;
670 assert_eq!(TEST_API_KEY, system.config().shared.endpoints.api_key);
671
672 agent_tx
673 .send(ConfigUpdate::Partial(ConfigSetting::explicit("api_key", json!(""))))
674 .await
675 .unwrap();
676 agent_tx
677 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
678 "log_level",
679 json!("error"),
680 )))
681 .await
682 .unwrap();
683
684 await_config(&system, "the later valid update to take effect", |c| {
685 c.control.logging.level == "error"
686 })
687 .await;
688 assert_eq!(TEST_API_KEY, system.config().shared.endpoints.api_key);
689 }
690
691 #[tokio::test]
692 async fn standalone_loads_scalars_written_in_any_form_the_agent_casts() {
693 let system = standalone_system(
698 Some(json!({
699 "use_v3_api": { "series": { "enabled": true } },
700 "dogstatsd_port": "8126",
701 })),
702 None,
703 )
704 .await
705 .expect("scalars in Agent-castable forms boot");
706
707 assert_eq!(
708 system.config().shared.metrics_encoding.v3_series_mode,
709 V3SeriesMode::Enabled
710 );
711 assert_eq!(system.config().domains.dogstatsd.listeners.port, 8126);
712 }
713
714 #[tokio::test]
715 async fn translation_invalid_update_is_rejected_keeping_last_known_good() {
716 let (system, agent_tx) =
717 connected_system(json!({ "log_level": "warn", "dogstatsd_tag_cardinality": "high" })).await;
718 assert_eq!(
719 system.config().domains.dogstatsd.origin.tag_cardinality,
720 OriginTagCardinality::High
721 );
722
723 agent_tx
726 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
727 "dogstatsd_tag_cardinality",
728 json!("bogus"),
729 )))
730 .await
731 .unwrap();
732 agent_tx
733 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
734 "log_level",
735 json!("error"),
736 )))
737 .await
738 .unwrap();
739
740 await_config(&system, "the later valid update to take effect", |c| {
741 c.control.logging.level == "error"
742 })
743 .await;
744 assert_eq!(
747 system.config().domains.dogstatsd.origin.tag_cardinality,
748 OriginTagCardinality::High
749 );
750 }
751
752 #[tokio::test]
753 async fn invalid_metric_tag_value_allowlist_update_keeps_last_known_good() {
754 let initial_allowlist = json!([{
755 "metric_prefix": "requests.",
756 "tag_name": "customer_id",
757 "values": ["customer-1"]
758 }]);
759 let (system, agent_tx) = connected_system(json!({
760 "log_level": "warn",
761 "metric_tag_value_allowlist": initial_allowlist
762 }))
763 .await;
764
765 agent_tx
766 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
767 "metric_tag_value_allowlist",
768 json!([
769 { "metric_prefix": "requests.", "tag_name": "customer_id" },
770 { "metric_prefix": "requests.api.", "tag_name": "customer_id" }
771 ]),
772 )))
773 .await
774 .unwrap();
775 agent_tx
776 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
777 "log_level",
778 json!("error"),
779 )))
780 .await
781 .unwrap();
782
783 await_config(&system, "the later valid update to take effect", |config| {
784 config.control.logging.level == "error"
785 })
786 .await;
787 let config = system.config();
788 assert_eq!(config.domains.dogstatsd.tag_value_allowlist.len(), 1);
789 assert_eq!(
790 config.domains.dogstatsd.tag_value_allowlist[0].metric_prefix,
791 "requests."
792 );
793 assert_eq!(config.domains.dogstatsd.tag_value_allowlist[0].values, ["customer-1"]);
794 }
795
796 #[tokio::test]
797 async fn converges_to_latest_value_under_burst() {
798 let (system, agent_tx) = connected_system(json!({ "log_level": "info" })).await;
799
800 let burst = [
801 "warn", "error", "debug", "trace", "info", "warn", "error", "debug", "trace", "info", "warn", "error",
802 "debug", "trace", "info", "warn", "error", "debug", "trace",
803 ];
804 for (i, level) in burst.iter().enumerate() {
805 agent_tx
806 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
807 "log_level",
808 json!(level),
809 )))
810 .await
811 .unwrap();
812 if i == burst.len() / 2 {
816 agent_tx
817 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
818 "dogstatsd_tag_cardinality",
819 json!("bogus"),
820 )))
821 .await
822 .unwrap();
823 agent_tx
824 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
825 "dogstatsd_tag_cardinality",
826 json!("high"),
827 )))
828 .await
829 .unwrap();
830 }
831 }
832 let final_level = "error";
833 agent_tx
834 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
835 "log_level",
836 json!(final_level),
837 )))
838 .await
839 .unwrap();
840
841 await_config(
842 &system,
843 "the current configuration to converge to the final value",
844 |c| c.control.logging.level == final_level,
845 )
846 .await;
847 assert_eq!(system.config().control.logging.level, final_level);
848 }
849
850 #[tokio::test]
853 async fn a_defaulted_agent_value_does_not_erase_a_local_one() {
854 let (system, agent_tx) = connected_system(json!({ "dd_url": "https://vector.example.com" })).await;
855
856 agent_tx
857 .send(ConfigUpdate::snapshot([ConfigSetting::new(
858 "dd_url",
859 json!("https://app.datadoghq.com"),
860 StreamProvenance::Default,
861 )]))
862 .await
863 .unwrap();
864 agent_tx
866 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
867 "log_level",
868 json!("error"),
869 )))
870 .await
871 .unwrap();
872 await_config(&system, "the trailing update to take effect", |c| {
873 c.control.logging.level == "error"
874 })
875 .await;
876
877 let dd_url = &system.config().shared.endpoints.dd_url;
878 assert!(dd_url.is_explicit());
879 assert_eq!(dd_url.value, "https://vector.example.com");
880 }
881
882 #[tokio::test]
883 async fn demoting_an_agent_value_to_a_default_reveals_the_local_value() {
884 let (system, agent_tx) = connected_system(json!({ "dd_url": "https://vector.example.com" })).await;
885
886 agent_tx
888 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
889 "dd_url",
890 json!("https://app.datadoghq.eu"),
891 )))
892 .await
893 .unwrap();
894 await_config(&system, "the Agent override to take effect", |c| {
895 c.shared.endpoints.dd_url.value == "https://app.datadoghq.eu"
896 })
897 .await;
898
899 agent_tx
903 .send(ConfigUpdate::Partial(ConfigSetting::new(
904 "dd_url",
905 json!("https://app.datadoghq.com"),
906 StreamProvenance::Default,
907 )))
908 .await
909 .unwrap();
910
911 await_config(&system, "the local value to be revealed again", |c| {
912 c.shared.endpoints.dd_url.value == "https://vector.example.com"
913 })
914 .await;
915 assert!(system.config().shared.endpoints.dd_url.is_explicit());
916 }
917
918 #[tokio::test]
919 async fn a_snapshot_replaces_the_agent_layer() {
920 let (system, agent_tx) = connected_system(json!({})).await;
921
922 agent_tx
923 .send(ConfigUpdate::snapshot([ConfigSetting::explicit(
924 "dogstatsd_port",
925 json!(9125),
926 )]))
927 .await
928 .unwrap();
929 await_config(&system, "the first snapshot to take effect", |c| {
930 c.domains.dogstatsd.listeners.port == 9125
931 })
932 .await;
933
934 agent_tx
937 .send(ConfigUpdate::snapshot([ConfigSetting::explicit(
938 "log_level",
939 json!("error"),
940 )]))
941 .await
942 .unwrap();
943
944 await_config(&system, "the replacing snapshot to take effect", |c| {
945 c.control.logging.level == "error"
946 })
947 .await;
948 assert_eq!(system.config().domains.dogstatsd.listeners.port, 8125);
949 }
950
951 #[tokio::test]
952 async fn a_live_view_wakes_when_only_provenance_changes() {
953 let (system, agent_tx) = connected_system(json!({})).await;
954 let mut dd_url = system.live(|c| &c.shared.endpoints.dd_url);
955 assert_eq!(dd_url.provenance, Provenance::Default);
957 assert_eq!(dd_url.value, "https://app.datadoghq.com");
958
959 agent_tx
962 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
963 "dd_url",
964 json!("https://app.datadoghq.com"),
965 )))
966 .await
967 .unwrap();
968
969 let updated = tokio::time::timeout(Duration::from_secs(2), dd_url.changed())
970 .await
971 .expect("the view observes a provenance-only change");
972 assert_eq!(updated.provenance, Provenance::Explicit);
973 assert_eq!(updated.value, "https://app.datadoghq.com");
974 }
975
976 #[tokio::test]
977 async fn live_view_observes_debug_log_update() {
978 let (system, agent_tx) = connected_system(json!({ "dogstatsd_metrics_stats_enable": false })).await;
979 let mut view = system.live(|c| &c.domains.dogstatsd.debug_log);
980 assert!(!view.metrics_stats_enable);
981
982 agent_tx
983 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
984 "dogstatsd_metrics_stats_enable",
985 json!(true),
986 )))
987 .await
988 .unwrap();
989
990 let updated = tokio::time::timeout(Duration::from_secs(2), view.changed())
991 .await
992 .expect("view observes the debug-log update");
993 assert!(updated.metrics_stats_enable);
994 assert!(view.metrics_stats_enable);
996 }
997
998 #[tokio::test]
999 async fn field_view_wakes_on_its_field() {
1000 let (system, agent_tx) = connected_system(json!({ "dogstatsd_metrics_stats_enable": false })).await;
1003 let mut stats = system.live(|c| &c.domains.dogstatsd.debug_log.metrics_stats_enable);
1004 assert!(!*stats);
1005
1006 agent_tx
1007 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1008 "dogstatsd_metrics_stats_enable",
1009 json!(true),
1010 )))
1011 .await
1012 .unwrap();
1013
1014 let updated = tokio::time::timeout(Duration::from_secs(2), stats.changed())
1015 .await
1016 .expect("field view observes its field's update");
1017 assert!(updated);
1018 assert!(*stats);
1019 }
1020
1021 #[tokio::test]
1022 async fn fixed_view_never_changes() {
1023 let mut view: Live<bool> = Live::new_fixed(true);
1024 assert!(*view);
1025 assert!(tokio::time::timeout(Duration::from_millis(100), view.changed())
1027 .await
1028 .is_err());
1029 }
1030
1031 #[tokio::test]
1032 async fn live_views_reflect_startup_configuration() {
1033 let system = standalone_system(Some(json!({ "dogstatsd_metrics_stats_enable": true })), None)
1034 .await
1035 .expect("system builds");
1036 let config = system.config();
1037
1038 let debug_log = system.live(|c| &c.domains.dogstatsd.debug_log);
1039 assert_eq!(&*debug_log, &config.domains.dogstatsd.debug_log);
1040
1041 let prefix_filter = system.live(|c| &c.domains.dogstatsd.prefix_filter);
1042 assert_eq!(&*prefix_filter, &config.domains.dogstatsd.prefix_filter);
1043
1044 let multi_region_failover = system.live(|c| &c.domains.multi_region_failover);
1045 assert_eq!(&*multi_region_failover, &config.domains.multi_region_failover);
1046 }
1047
1048 #[test]
1049 fn translate_small_map_through_witness_and_seed() {
1050 let sources = SourceTree::all_explicit(json!({
1053 "api_key": "abc",
1054 "dd_url": "https://custom.example.com",
1055 "dogstatsd_port": 9125,
1056 "dogstatsd_tag_cardinality": "high",
1057 "expected_tags_duration": "15s",
1058 "provider_kind": "gke-autopilot",
1059 "eks_fargate": true,
1060 "kubernetes_kubelet_nodename": "fargate-node",
1061 "cluster_name": "fargate-cluster",
1062 "telemetry": { "dogstatsd_origin": true },
1063 "dogstatsd_tcp_port": 8126,
1064 }));
1065 let value = sources.to_value();
1066 let datadog: DatadogConfiguration = serde_json::from_value(value.clone()).expect("datadog source deserializes");
1067 let saluki: SalukiOnly = serde_json::from_value(value).expect("saluki-only source deserializes");
1068
1069 let (config, errors) = translate(&datadog, &saluki, &sources);
1070 assert!(errors.is_none(), "translation of a valid map records no error");
1071
1072 assert_eq!(config.domains.dogstatsd.listeners.port, 9125);
1074 assert_eq!(
1076 config.domains.dogstatsd.origin.tag_cardinality,
1077 OriginTagCardinality::High
1078 );
1079 assert_eq!(config.shared.tags.expected_tags_duration, Duration::from_secs(15));
1081 assert_eq!(config.shared.static_tags.provider_kind, "gke-autopilot");
1083 assert!(config.shared.static_tags.eks_fargate);
1084 assert_eq!(config.shared.static_tags.kubernetes_kubelet_nodename, "fargate-node");
1085 assert_eq!(config.shared.static_tags.cluster_name, "fargate-cluster");
1086 assert!(config.domains.dogstatsd.telemetry.origin_breakdown);
1088 assert_eq!(config.shared.endpoints.api_key, "abc");
1090 assert!(config.shared.endpoints.dd_url.is_explicit());
1091 assert_eq!(config.shared.endpoints.dd_url.value, "https://custom.example.com");
1092 assert_eq!(config.domains.dogstatsd.listeners.tcp_port, 8126);
1094 }
1095
1096 #[test]
1098 fn datadog_aggregation_keys_reach_the_model() {
1099 let sources = SourceTree::all_explicit(json!({
1100 "dogstatsd_expiry_seconds": 60,
1101 "dogstatsd_flush_incomplete_buckets": true,
1102 }));
1103 let value = sources.to_value();
1104 let datadog: DatadogConfiguration = serde_json::from_value(value.clone()).expect("datadog source deserializes");
1105 let saluki: SalukiOnly = serde_json::from_value(value).expect("saluki-only source deserializes");
1106
1107 let (config, errors) = translate(&datadog, &saluki, &sources);
1108 assert!(errors.is_none(), "translation of a valid map records no error");
1109
1110 let aggregation = &config.domains.dogstatsd.aggregation;
1111 assert_eq!(aggregation.counter_expiry_seconds, Some(60));
1112 assert!(aggregation.flush_open_windows);
1113 }
1114}