1use std::future::Future;
5use std::sync::{Arc, PoisonError, RwLock};
6use std::time::Duration;
7
8use agent_data_plane_config::{Live, SalukiConfiguration};
9use arc_swap::ArcSwap;
10use datadog_agent_config::{DatadogConfiguration, TranslateErrors};
11use saluki_config::dynamic::ConfigUpdate;
12use serde::Deserialize;
13use serde_json::Value;
14use snafu::Snafu;
15use tokio::sync::{mpsc, watch, Mutex};
16use tokio::time::timeout;
17use tracing::{debug, warn};
18
19use crate::saluki_only::SalukiOnly;
20use crate::source::SourceTree;
21use crate::translators::DatadogTranslator;
22
23const INITIAL_CONFIG_SNAPSHOT_TIMEOUT_SECS: u64 = 15;
28const INITIAL_CONFIG_SNAPSHOT_TIMEOUT: Duration = Duration::from_secs(INITIAL_CONFIG_SNAPSHOT_TIMEOUT_SECS);
29
30#[derive(Debug, Snafu)]
32pub enum Error {
33 #[snafu(context(false), display("{source}"))]
35 Deserialize {
36 source: serde_json::Error,
38 },
39
40 #[snafu(display("configuration stream closed before the initial snapshot"))]
42 StreamClosed,
43
44 #[snafu(display("configuration stream closed; no further configuration updates can be applied"))]
47 UpdateStreamClosed,
48
49 #[snafu(display("timed out waiting for the initial snapshot ({INITIAL_CONFIG_SNAPSHOT_TIMEOUT_SECS} seconds)"))]
51 SnapshotTimeOut,
52
53 #[snafu(display("failed to build the configuration base: {message}"))]
55 Base {
56 message: String,
58 },
59
60 #[snafu(display("{source}"))]
62 Translate {
63 source: TranslateErrors,
65 },
66
67 #[snafu(display(
69 "no Datadog API key is configured: set `api_key` in the Datadog Agent's configuration, or \
70 `DD_API_KEY` in the environment. Every payload is authenticated with this key, so nothing can \
71 be submitted without one"
72 ))]
73 MissingApiKey,
74}
75
76type Result<T> = std::result::Result<T, Error>;
77
78pub struct ConfigurationSystem {
85 current: Arc<ArcSwap<SalukiConfiguration>>,
86 sources: Arc<RwLock<Arc<SourceTree>>>,
90 tick: Arc<watch::Sender<()>>,
94}
95
96impl ConfigurationSystem {
97 pub(crate) async fn connected(
111 mut agent_rx: mpsc::Receiver<ConfigUpdate>, base: SourceTree,
112 ) -> Result<(Self, ConfigurationUpdates)> {
113 saluki_antithesis::reachable!("config readiness wait entered");
116 let first = match timeout(INITIAL_CONFIG_SNAPSHOT_TIMEOUT, agent_rx.recv()).await {
117 Ok(None) => {
118 saluki_antithesis::unreachable!("config stream closed before the initial snapshot");
119 return Err(Error::StreamClosed);
120 }
121 Ok(Some(first)) => first,
122 Err(_) => return Err(Error::SnapshotTimeOut),
123 };
124 saluki_antithesis::sometimes!(true, "config readiness signal received");
125
126 let mut agent = SourceTree::empty();
127 fold(&mut agent, &first);
128
129 let merged = base.overlay(&agent);
134 let config = translate_authoritative(&merged)?;
135
136 let current = Arc::new(ArcSwap::from_pointee(config));
137 let sources = Arc::new(RwLock::new(Arc::new(merged)));
138 let (tick, _) = watch::channel(());
141 let tick = Arc::new(tick);
142
143 let updates = ConfigurationUpdates {
144 state: Arc::new(Mutex::new(UpdateState {
145 agent_rx,
146 base,
147 agent,
148 current: Arc::clone(¤t),
149 sources: Arc::clone(&sources),
150 tick: Arc::clone(&tick),
151 })),
152 };
153
154 Ok((Self { current, sources, tick }, updates))
155 }
156
157 pub(crate) fn standalone(config: SalukiConfiguration, sources: SourceTree) -> Self {
161 let current = Arc::new(ArcSwap::from_pointee(config));
162 let (tick, _) = watch::channel(());
163 Self {
164 current,
165 sources: Arc::new(RwLock::new(Arc::new(sources))),
166 tick: Arc::new(tick),
167 }
168 }
169
170 pub fn live<T>(&self, project: impl for<'a> Fn(&'a SalukiConfiguration) -> &'a T + Send + Sync + 'static) -> Live<T>
173 where
174 T: Clone + PartialEq + 'static,
175 {
176 Live::new_dynamic(Arc::clone(&self.current), self.tick.subscribe(), project)
177 }
178
179 pub fn config(&self) -> arc_swap::Guard<Arc<SalukiConfiguration>> {
183 self.current.load()
184 }
185
186 pub fn current_handle(&self) -> Arc<ArcSwap<SalukiConfiguration>> {
189 Arc::clone(&self.current)
190 }
191
192 pub fn raw_snapshot(&self) -> Arc<dyn Fn() -> Value + Send + Sync> {
198 let sources = Arc::clone(&self.sources);
199 Arc::new(move || load_sources(&sources).to_value())
200 }
201
202 pub(crate) fn sources(&self) -> Arc<SourceTree> {
204 load_sources(&self.sources)
205 }
206}
207
208fn load_sources(sources: &RwLock<Arc<SourceTree>>) -> Arc<SourceTree> {
209 Arc::clone(&sources.read().unwrap_or_else(PoisonError::into_inner))
210}
211
212#[must_use = "configuration updates from the Datadog Agent are never applied unless the updates are run"]
218pub struct ConfigurationUpdates {
219 state: Arc<Mutex<UpdateState>>,
220}
221
222impl ConfigurationUpdates {
223 pub fn run(&self) -> impl Future<Output = Result<()>> + Send + 'static {
234 let state = Arc::clone(&self.state);
235 async move {
236 let mut state = state.lock_owned().await;
238 state.apply_updates().await
239 }
240 }
241}
242
243struct UpdateState {
245 agent_rx: mpsc::Receiver<ConfigUpdate>,
246 base: SourceTree,
247 agent: SourceTree,
249 current: Arc<ArcSwap<SalukiConfiguration>>,
250 sources: Arc<RwLock<Arc<SourceTree>>>,
251 tick: Arc<watch::Sender<()>>,
252}
253
254impl UpdateState {
255 async fn apply_updates(&mut self) -> Result<()> {
261 while let Some(update) = self.agent_rx.recv().await {
262 let mut tentative = self.agent.clone();
266 fold(&mut tentative, &update);
267 let merged = self.base.overlay(&tentative);
268 match translate_authoritative(&merged) {
269 Ok(config) => {
270 self.agent = tentative;
271 {
272 let mut sources = self.sources.write().unwrap_or_else(PoisonError::into_inner);
273 *sources = Arc::new(merged);
274 self.current.store(Arc::new(config));
275 }
276 self.tick.send_replace(());
277 debug!("Applied configuration update.");
278 }
279 Err(e) => warn!(
280 error = %e,
281 "Rejected configuration update; keeping the last-known-good typed configuration."
282 ),
283 }
284 }
285
286 Err(Error::UpdateStreamClosed)
287 }
288}
289
290fn fold(agent: &mut SourceTree, update: &ConfigUpdate) {
297 match update {
298 ConfigUpdate::Snapshot(settings) => *agent = SourceTree::from_settings(settings),
299 ConfigUpdate::Partial(setting) => agent.set(setting),
300 }
301}
302
303pub(crate) fn translate_strict(merged: &SourceTree) -> Result<SalukiConfiguration> {
309 let Sources { datadog, saluki } = deserialize_sources(&merged.to_value())?;
310 let (config, errors) = translate(&datadog, &saluki, merged);
311 if let Some(errors) = errors {
312 return Err(Error::Translate { source: errors });
313 }
314 Ok(config)
315}
316
317pub(crate) fn translate_authoritative(merged: &SourceTree) -> Result<SalukiConfiguration> {
327 let config = translate_strict(merged)?;
328 validate(&config)?;
329 Ok(config)
330}
331
332pub(crate) fn validate(config: &SalukiConfiguration) -> Result<()> {
351 if config.shared.endpoints.api_key.trim().is_empty() {
354 return Err(Error::MissingApiKey);
355 }
356
357 Ok(())
358}
359
360struct Sources {
367 datadog: DatadogConfiguration,
368 saluki: SalukiOnly,
369}
370
371fn deserialize_sources(merged: &Value) -> Result<Sources> {
378 let saluki = SalukiOnly::deserialize(merged)?;
379 let datadog = DatadogConfiguration::deserialize(merged)?;
380 Ok(Sources { datadog, saluki })
381}
382
383fn translate(
395 datadog: &DatadogConfiguration, saluki: &SalukiOnly, sources: &SourceTree,
396) -> (SalukiConfiguration, Option<TranslateErrors>) {
397 let (mut config, errors) = DatadogTranslator::new(datadog, sources).translate();
398 saluki.seed(&mut config);
399 (config, errors)
400}
401
402#[cfg(test)]
403mod tests {
404 use std::collections::HashSet;
405 use std::sync::Arc;
406 use std::time::Duration;
407
408 use agent_data_plane_config::domains::dogstatsd::{MetricPrefixRule, OriginTagCardinality};
409 use agent_data_plane_config::shared::V3SeriesMode;
410 use agent_data_plane_config::Provenance;
411 use agent_data_plane_config::{Live, SalukiConfiguration};
412 use datadog_agent_config::{apply_datadog_env_vars, DatadogConfiguration};
413 use futures::StreamExt as _;
414 use saluki_config::dynamic::{ConfigSetting, ConfigUpdate, Provenance as StreamProvenance};
415 use serde_json::{json, Value};
416 use tokio::sync::mpsc;
417
418 use super::{
419 translate, translate_authoritative, translate_strict, ConfigurationSystem, Error, SalukiOnly, SourceTree,
420 };
421
422 const TEST_API_KEY: &str = "test-api-key";
428
429 fn standalone_system(file: Value) -> Result<ConfigurationSystem, Error> {
434 let base = SourceTree::all_explicit(file);
435 let config = translate_strict(&base)?;
436 Ok(ConfigurationSystem::standalone(config, base))
437 }
438
439 async fn connected_system(mut base: Value) -> (ConfigurationSystem, mpsc::Sender<ConfigUpdate>) {
446 let (agent_tx, agent_rx) = mpsc::channel(100);
447 agent_tx.send(ConfigUpdate::snapshot([])).await.unwrap();
448 if let Some(base) = base.as_object_mut() {
449 base.entry("api_key").or_insert(json!(TEST_API_KEY));
450 }
451 let base = SourceTree::all_explicit(base);
452 let (system, updates) = ConfigurationSystem::connected(agent_rx, base)
453 .await
454 .expect("system builds");
455 tokio::spawn(updates.run());
456 (system, agent_tx)
457 }
458
459 async fn await_config(system: &ConfigurationSystem, what: &str, predicate: impl Fn(&SalukiConfiguration) -> bool) {
461 tokio::time::timeout(Duration::from_secs(2), async {
462 while !predicate(&system.config()) {
463 tokio::time::sleep(Duration::from_millis(5)).await;
464 }
465 })
466 .await
467 .unwrap_or_else(|_| panic!("timed out waiting for {what}"));
468 }
469
470 #[tokio::test]
471 async fn startup_current_reflects_translation() {
472 let system = standalone_system(json!({ "log_level": "warn", "dogstatsd_port": 9125 })).expect("system builds");
473 let config = system.config();
474
475 assert_eq!(config.control.logging.level, "warn");
476 assert_eq!(config.domains.dogstatsd.listeners.port, 9125);
477 }
478
479 #[test]
480 fn boolean_use_v3_api_series_enabled_is_normalized() {
481 let sources = SourceTree::all_explicit(json!({
482 "use_v3_api": {
483 "series": {
484 "enabled": true
485 }
486 }
487 }));
488
489 let config = translate_strict(&sources).expect("a boolean V3 series mode should translate");
490
491 assert_eq!(config.shared.metrics_encoding.v3_series_mode, V3SeriesMode::Enabled);
492 }
493
494 #[test]
495 fn compound_v3_series_endpoint_modes_are_rejected_at_startup() {
496 for mode in [json!(["true"]), json!({ "enabled": true })] {
497 let sources = SourceTree::all_explicit(json!({
498 "use_v3_api": { "series": { "endpoints": { "https://app.datadoghq.com": mode } } }
499 }));
500
501 assert!(translate_strict(&sources).is_err());
502 }
503 }
504
505 #[tokio::test]
506 async fn malformed_v3_endpoint_update_keeps_last_known_good_and_recovers() {
507 let (system, agent_tx) = connected_system(json!({
508 "dogstatsd_port": 9125,
509 "use_v3_api": { "series": { "endpoints": { "https://app.datadoghq.com": true } } }
510 }))
511 .await;
512
513 agent_tx
514 .send(ConfigUpdate::snapshot([
515 ConfigSetting::explicit("dogstatsd_port", json!(9999)),
516 ConfigSetting::explicit(
517 "use_v3_api.series.endpoints",
518 json!({ "https://app.datadoghq.com": ["false"] }),
519 ),
520 ]))
521 .await
522 .unwrap();
523 agent_tx
524 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
525 "log_level",
526 json!("error"),
527 )))
528 .await
529 .unwrap();
530 await_config(&system, "the update following the rejected snapshot", |config| {
531 config.control.logging.level == "error"
532 })
533 .await;
534
535 assert_eq!(system.config().domains.dogstatsd.listeners.port, 9125);
536 assert_eq!(
537 system.config().shared.metrics_encoding.v3_series_endpoint_modes["https://app.datadoghq.com"],
538 V3SeriesMode::Enabled
539 );
540
541 agent_tx
542 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
543 "use_v3_api.series.endpoints",
544 json!({ "https://app.datadoghq.com": false }),
545 )))
546 .await
547 .unwrap();
548 await_config(&system, "the corrected endpoint mode", |config| {
549 config.shared.metrics_encoding.v3_series_endpoint_modes["https://app.datadoghq.com"]
550 == V3SeriesMode::Disabled
551 })
552 .await;
553 }
554
555 #[tokio::test]
558 async fn connected_startup_accepts_json_encoded_map_settings() {
559 let (agent_tx, agent_rx) = mpsc::channel(1);
560 agent_tx
561 .send(ConfigUpdate::snapshot([
562 ConfigSetting::explicit(
563 "additional_endpoints",
564 json!(r#"{"https://app.datadoghq.com": ["second-org-key"]}"#),
565 ),
566 ConfigSetting::explicit(
567 "use_v3_api.series.endpoints",
568 json!(r#"{"https://app.datadoghq.com": "true"}"#),
569 ),
570 ]))
571 .await
572 .unwrap();
573 let base = SourceTree::all_explicit(json!({ "api_key": TEST_API_KEY }));
574
575 let (system, _updates) = ConfigurationSystem::connected(agent_rx, base)
577 .await
578 .expect("startup accepts the streamed JSON strings");
579
580 let config = system.config();
581 assert_eq!(
582 config.shared.endpoints.additional_endpoints["https://app.datadoghq.com"],
583 ["second-org-key"]
584 );
585 assert_eq!(
586 config.shared.metrics_encoding.v3_series_endpoint_modes["https://app.datadoghq.com"],
587 V3SeriesMode::Enabled
588 );
589 }
590
591 #[tokio::test]
594 async fn connected_startup_reads_null_collections_as_empty() {
595 let (agent_tx, agent_rx) = mpsc::channel(1);
596 agent_tx
597 .send(ConfigUpdate::snapshot([
598 ConfigSetting::explicit("histogram_aggregates", Value::Null),
599 ConfigSetting::explicit("proxy.no_proxy", Value::Null),
600 ConfigSetting::explicit("additional_endpoints", Value::Null),
601 ]))
602 .await
603 .unwrap();
604 let base = SourceTree::all_explicit(json!({ "api_key": TEST_API_KEY }));
605
606 let (system, _updates) = ConfigurationSystem::connected(agent_rx, base)
607 .await
608 .expect("startup accepts streamed null collections");
609
610 let config = system.config();
611 assert!(config.shared.metrics_encoding.histogram.aggregates.is_empty());
612 assert!(config.shared.endpoints.proxy.no_proxy.is_empty());
613 assert!(config.shared.endpoints.additional_endpoints.is_empty());
614 }
615
616 #[tokio::test]
619 async fn connected_startup_accepts_json_encoded_event_extraction_settings() {
620 let (agent_tx, agent_rx) = mpsc::channel(1);
621 agent_tx
622 .send(ConfigUpdate::snapshot([
623 ConfigSetting::explicit(
624 "apm_config.analyzed_spans",
625 json!(r#"{"test-service|operation": 0.75}"#),
626 ),
627 ConfigSetting::explicit(
628 "apm_config.analyzed_rate_by_service",
629 json!(r#"{"legacy-service": 0.5}"#),
630 ),
631 ]))
632 .await
633 .unwrap();
634 let base = SourceTree::all_explicit(json!({ "api_key": TEST_API_KEY }));
635
636 let (system, _updates) = ConfigurationSystem::connected(agent_rx, base)
638 .await
639 .expect("startup accepts the streamed JSON strings");
640
641 let config = system.config();
642 assert_eq!(
643 config.domains.traces.analyzed_spans_by_service["test-service"]["operation"],
644 0.75
645 );
646 assert_eq!(config.domains.traces.analyzed_rate_by_service["legacy-service"], 0.5);
647 }
648
649 #[tokio::test]
650 async fn null_update_clears_a_collection_rather_than_restoring_its_default() {
651 let (system, agent_tx) = connected_system(json!({
652 "additional_endpoints": { "https://app.datadoghq.com": ["second-org-key"] },
653 "histogram_aggregates": ["max"]
654 }))
655 .await;
656 assert!(!system.config().shared.endpoints.additional_endpoints.is_empty());
657
658 for key in ["additional_endpoints", "histogram_aggregates"] {
659 agent_tx
660 .send(ConfigUpdate::Partial(ConfigSetting::explicit(key, Value::Null)))
661 .await
662 .unwrap();
663 }
664 await_config(&system, "the cleared collections", |config| {
665 config.shared.endpoints.additional_endpoints.is_empty()
666 && config.shared.metrics_encoding.histogram.aggregates.is_empty()
667 })
668 .await;
669 }
670
671 #[tokio::test]
672 async fn connected_stream_translates_metrics_v3_routing_configuration() {
673 let (system, agent_tx) = connected_system(json!({
674 "data_plane": {
675 "metrics": {
676 "v3": {
677 "series": {
678 "enabled": true
679 }
680 }
681 }
682 }
683 }))
684 .await;
685
686 assert_eq!(
687 system.config().shared.metrics_encoding.v3_series_mode,
688 V3SeriesMode::DatadogOnly
689 );
690
691 agent_tx
692 .send(ConfigUpdate::snapshot([
693 ConfigSetting::explicit("serializer_compressor_kind", json!("zstd")),
694 ConfigSetting::explicit("serializer_experimental_use_v3_api.compression_level", json!(7)),
695 ConfigSetting::explicit("use_v2_api.series", json!(false)),
696 ConfigSetting::explicit("use_v3_api.series.enabled", json!("false")),
697 ConfigSetting::explicit(
699 "use_v3_api.series.endpoints",
700 json!({ "https://app.datadoghq.com": "true" }),
701 ),
702 ConfigSetting::explicit("observability_pipelines_worker.metrics.enabled", json!(true)),
703 ConfigSetting::explicit(
704 "observability_pipelines_worker.metrics.url",
705 json!("https://opw.example.com"),
706 ),
707 ConfigSetting::explicit("observability_pipelines_worker.metrics.use_v3_api.series", json!(true)),
708 ]))
709 .await
710 .unwrap();
711
712 await_config(&system, "the streamed metrics V3 routing configuration", |config| {
713 config.shared.metrics_encoding.v3_series_mode == V3SeriesMode::Disabled
714 && config
715 .shared
716 .metrics_encoding
717 .v3_series_endpoint_modes
718 .get("https://app.datadoghq.com")
719 == Some(&V3SeriesMode::Enabled)
720 })
721 .await;
722
723 let config = system.config();
724 let metrics = &config.shared.metrics_encoding;
725 assert!(!metrics.use_v2_series_api);
726 assert_eq!(metrics.v3_api.compression_level, 7);
727 let opw = &config.shared.endpoints.opw_intake;
728 assert!(opw.enabled);
729 assert_eq!(opw.url, "https://opw.example.com");
730 assert!(opw.use_v3_series);
731 }
732
733 #[tokio::test]
734 async fn raw_snapshot_reflects_streamed_updates() {
735 let (system, agent_tx) = connected_system(json!({})).await;
736 let snapshot = system.raw_snapshot();
737 let cloned = Arc::clone(&snapshot);
738
739 assert_eq!(snapshot().pointer("/dogstatsd_port"), None);
740
741 agent_tx
742 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
743 "dogstatsd_port",
744 json!(9125),
745 )))
746 .await
747 .unwrap();
748
749 tokio::time::timeout(Duration::from_secs(2), async {
750 while cloned().pointer("/dogstatsd_port") != Some(&json!(9125)) {
751 tokio::time::sleep(Duration::from_millis(5)).await;
752 }
753 })
754 .await
755 .expect("timed out waiting for the raw snapshot to reflect the update");
756 }
757
758 #[tokio::test]
759 async fn raw_snapshot_keeps_keys_the_typed_model_does_not_carry() {
760 let (system, agent_tx) = connected_system(json!({ "dogstatsd_stats_buffer": 42 })).await;
764 let snapshot = system.raw_snapshot();
765
766 assert_eq!(snapshot().pointer("/dogstatsd_stats_buffer"), Some(&json!(42)));
767
768 agent_tx
769 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
770 "config_id",
771 json!("abc"),
772 )))
773 .await
774 .unwrap();
775
776 tokio::time::timeout(Duration::from_secs(2), async {
777 while snapshot().pointer("/config_id") != Some(&json!("abc")) {
778 tokio::time::sleep(Duration::from_millis(5)).await;
779 }
780 })
781 .await
782 .expect("timed out waiting for the streamed unsupported key");
783 }
784
785 #[tokio::test]
786 async fn nested_datadog_key_reaches_the_model() {
787 let system = standalone_system(json!({
791 "autoscaling": {
792 "failover": {
793 "enabled": true,
794 "metrics": "container.memory.usage container.cpu.usage",
795 }
796 }
797 }))
798 .expect("system builds");
799 let config = system.config();
800
801 assert!(config.shared.autoscaling_failover.enabled);
802 assert_eq!(
803 config.shared.autoscaling_failover.metrics,
804 vec!["container.memory.usage".to_string(), "container.cpu.usage".to_string()]
805 );
806 }
807
808 #[tokio::test]
809 async fn unset_autoscaling_failover_keeps_its_schema_defaults() {
810 let system = standalone_system(json!({})).expect("system builds");
813 let config = system.config();
814
815 assert!(!config.shared.autoscaling_failover.enabled);
816 assert_eq!(
817 config.shared.autoscaling_failover.metrics,
818 vec!["container.memory.usage".to_string(), "container.cpu.usage".to_string()]
819 );
820 }
821
822 #[tokio::test]
823 async fn nested_saluki_only_key_seeds_the_model() {
824 let system = standalone_system(json!({ "data_plane": { "standalone_mode": true } })).expect("system builds");
825
826 assert!(system.config().control.standalone_mode);
827 }
828
829 #[tokio::test]
830 async fn experimental_metric_allowlists_seed_the_model() {
831 let policies = json!({
832 "https://primary.example.com": ["allowed.metric"],
833 "https://secondary.example.com": []
834 });
835 let system = standalone_system(json!({
836 "experimental": {
837 "metrics_endpoint_routing": {
838 "metric_allowlist": policies.clone(),
839 "metric_prefix_allowlist": { "https://primary.example.com": ["billing."] }
840 }
841 }
842 }))
843 .expect("system builds");
844
845 assert_eq!(
846 serde_json::to_value(&system.config().domains.metrics_endpoint_routing.metric_allowlists).unwrap(),
847 policies
848 );
849 assert_eq!(
850 system
851 .config()
852 .domains
853 .metrics_endpoint_routing
854 .metric_prefix_allowlists,
855 std::collections::HashMap::from([(
856 "https://primary.example.com".to_string(),
857 vec!["billing.".to_string()]
858 )])
859 );
860 }
861
862 #[tokio::test]
863 async fn empty_experimental_sections_preserve_default_routing() {
864 for source in [
865 json!({}),
866 json!({ "experimental": {} }),
867 json!({ "experimental": { "metrics_endpoint_routing": {} } }),
868 json!({ "experimental": { "metrics_endpoint_routing": { "metric_allowlist": {} } } }),
869 json!({ "experimental": { "metrics_endpoint_routing": { "metric_prefix_allowlist": {} } } }),
870 ] {
871 let system = standalone_system(source).expect("system builds");
872 assert!(system
873 .config()
874 .domains
875 .metrics_endpoint_routing
876 .metric_allowlists
877 .is_empty());
878 assert!(system
879 .config()
880 .domains
881 .metrics_endpoint_routing
882 .metric_prefix_allowlists
883 .is_empty());
884 }
885 }
886
887 #[tokio::test]
888 async fn a_flattened_spelling_of_a_nested_key_is_not_read() {
889 let system = standalone_system(json!({ "autoscaling_failover_enabled": true })).expect("system builds");
894
895 assert!(!system.config().shared.autoscaling_failover.enabled);
896 }
897
898 #[tokio::test]
899 async fn load_fails_on_translation_invalid_startup_config() {
900 let result = standalone_system(json!({ "dogstatsd_tag_cardinality": "bogus" }));
903
904 assert!(matches!(result, Err(Error::Translate { .. })));
905 }
906
907 #[tokio::test]
908 async fn negative_dogstatsd_workers_count_is_rejected_at_startup() {
909 let result = standalone_system(json!({ "dogstatsd_workers_count": -1 }));
910
911 let Err(error) = result else {
912 panic!("negative worker count should fail the startup translation gate");
913 };
914 assert!(matches!(error, Error::Translate { .. }));
915 assert!(error.to_string().contains("dogstatsd_workers_count"));
916 assert!(error.to_string().contains("greater than or equal to 0"));
917 }
918
919 #[test]
920 fn zero_otlp_trace_interner_size_is_rejected() {
921 let sources = SourceTree::all_explicit(json!({ "otlp_config": { "traces": { "string_interner_size": 0 } } }));
924 let error = translate_strict(&sources).expect_err("zero trace interner size should fail translation");
925
926 assert!(matches!(error, Error::Deserialize { .. }));
927 assert!(error.to_string().contains("value of bytes must be greater than zero"));
928 }
929
930 #[test]
931 fn oversized_otlp_trace_interner_size_is_rejected() {
932 let sources =
933 SourceTree::all_explicit(json!({ "otlp_config": { "traces": { "string_interner_size": "2GiB" } } }));
934 let error = translate_strict(&sources).expect_err("oversized trace interner should fail translation");
935
936 assert!(matches!(error, Error::Deserialize { .. }));
937 assert!(error.to_string().contains("must not exceed 1073741824 bytes"));
938 }
939
940 #[test]
941 fn positive_otlp_trace_interner_size_is_accepted() {
942 let sources =
943 SourceTree::all_explicit(json!({ "otlp_config": { "traces": { "string_interner_size": "512KiB" } } }));
944 let config = translate_strict(&sources).expect("positive trace interner size should translate");
945
946 assert_eq!(config.domains.otlp.traces.string_interner_size.get(), 512 * 1024);
947 }
948
949 #[test]
950 fn invalid_metric_tag_value_allowlist_is_rejected_before_publication() {
951 let sources = SourceTree::all_explicit(json!({
952 "metric_tag_value_allowlist": [
953 { "metric_prefix": "requests.", "tag_name": "customer_id" },
954 { "metric_prefix": "requests.api.", "tag_name": "customer_id" }
955 ]
956 }));
957 let error = translate_strict(&sources).expect_err("overlapping allow-list prefixes should fail translation");
958
959 assert!(matches!(error, Error::Deserialize { .. }));
960 assert!(error.to_string().contains("overlapping metric prefixes"));
961 }
962
963 #[tokio::test]
964 async fn standalone_loads_numeric_byte_size() {
965 let system =
969 standalone_system(json!({ "dogstatsd_log_file_max_size": 10485760 })).expect("numeric byte size boots");
970
971 assert_eq!(system.config().domains.dogstatsd.debug_log.log_file_max_size, 10485760);
972 }
973
974 fn standalone_system_with_env(mut file: Value, env: &[(&str, &str)]) -> Result<ConfigurationSystem, Error> {
979 let vars = env.iter().map(|(name, value)| (name.to_string(), value.to_string()));
980 apply_datadog_env_vars(&mut file, vars, true).expect("environment decodes");
981 standalone_system(file)
982 }
983
984 #[tokio::test]
985 async fn standalone_reads_log_file_max_size_from_numbers_strings_and_environment() {
986 let cases = [
990 (
991 "number",
992 standalone_system(json!({ "log_file_max_size": 10_485_760 })),
993 10_485_760,
994 ),
995 (
996 "string",
997 standalone_system(json!({ "log_file_max_size": "10MB" })),
998 10_485_760,
999 ),
1000 (
1001 "environment",
1002 standalone_system_with_env(json!({}), &[("DD_LOG_FILE_MAX_SIZE", "0x10KB")]),
1003 16_384,
1004 ),
1005 ];
1006 for (source, system, expected) in cases {
1007 let system = system.unwrap_or_else(|error| panic!("{source} boots: {error}"));
1008 let file_max_size = &system.config().control.logging.file_max_size;
1009 assert!(file_max_size.is_explicit(), "{source}");
1010 assert_eq!(file_max_size.value, expected, "{source}");
1011 }
1012
1013 let system = standalone_system_with_env(
1015 json!({ "dogstatsd_log_file_max_size": "1KB" }),
1016 &[("DD_DOGSTATSD_LOG_FILE_MAX_SIZE", "5KB")],
1017 )
1018 .expect("environment size boots");
1019 assert_eq!(system.config().domains.dogstatsd.debug_log.log_file_max_size, 5_120);
1020 }
1021
1022 #[test]
1023 fn a_configuration_without_an_api_key_is_rejected() {
1024 let sources = SourceTree::all_explicit(json!({}));
1028 translate_strict(&sources).expect("an absent API key still translates");
1029
1030 let error = translate_authoritative(&sources).expect_err("an absent API key should fail the gate");
1031
1032 assert!(matches!(error, Error::MissingApiKey));
1033 assert!(error.to_string().contains("api_key"));
1034 }
1035
1036 #[test]
1037 fn a_blank_api_key_is_rejected() {
1038 for key in ["", " ", "\t\n"] {
1041 let sources = SourceTree::all_explicit(json!({ "api_key": key }));
1042
1043 assert!(
1044 matches!(translate_authoritative(&sources), Err(Error::MissingApiKey)),
1045 "{key:?} should not count as an API key"
1046 );
1047 }
1048
1049 let sources = SourceTree::all_explicit(json!({ "api_key": TEST_API_KEY }));
1050 assert_eq!(
1051 TEST_API_KEY,
1052 translate_authoritative(&sources)
1053 .expect("a real key passes the gate")
1054 .shared
1055 .endpoints
1056 .api_key
1057 );
1058 }
1059
1060 #[tokio::test]
1061 async fn an_update_that_blanks_the_api_key_is_rejected_keeping_last_known_good() {
1062 let (system, agent_tx) = connected_system(json!({ "log_level": "warn" })).await;
1065 assert_eq!(TEST_API_KEY, system.config().shared.endpoints.api_key);
1066
1067 agent_tx
1068 .send(ConfigUpdate::Partial(ConfigSetting::explicit("api_key", json!(""))))
1069 .await
1070 .unwrap();
1071 agent_tx
1072 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1073 "log_level",
1074 json!("error"),
1075 )))
1076 .await
1077 .unwrap();
1078
1079 await_config(&system, "the later valid update to take effect", |c| {
1080 c.control.logging.level == "error"
1081 })
1082 .await;
1083 assert_eq!(TEST_API_KEY, system.config().shared.endpoints.api_key);
1084 }
1085
1086 #[tokio::test]
1087 async fn standalone_loads_scalars_written_in_any_form_the_agent_casts() {
1088 let system = standalone_system(json!({
1093 "use_v3_api": { "series": { "enabled": true } },
1094 "dogstatsd_port": "8126",
1095 }))
1096 .expect("scalars in Agent-castable forms boot");
1097
1098 assert_eq!(
1099 system.config().shared.metrics_encoding.v3_series_mode,
1100 V3SeriesMode::Enabled
1101 );
1102 assert_eq!(system.config().domains.dogstatsd.listeners.port, 8126);
1103 }
1104
1105 #[tokio::test]
1106 async fn translation_invalid_update_is_rejected_keeping_last_known_good() {
1107 let (system, agent_tx) =
1108 connected_system(json!({ "log_level": "warn", "dogstatsd_tag_cardinality": "high" })).await;
1109 assert_eq!(
1110 system.config().domains.dogstatsd.origin.tag_cardinality,
1111 OriginTagCardinality::High
1112 );
1113
1114 agent_tx
1117 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1118 "dogstatsd_tag_cardinality",
1119 json!("bogus"),
1120 )))
1121 .await
1122 .unwrap();
1123 agent_tx
1124 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1125 "log_level",
1126 json!("error"),
1127 )))
1128 .await
1129 .unwrap();
1130
1131 await_config(&system, "the later valid update to take effect", |c| {
1132 c.control.logging.level == "error"
1133 })
1134 .await;
1135 assert_eq!(
1138 system.config().domains.dogstatsd.origin.tag_cardinality,
1139 OriginTagCardinality::High
1140 );
1141 }
1142
1143 #[tokio::test]
1144 async fn invalid_metric_tag_value_allowlist_update_keeps_last_known_good() {
1145 let initial_allowlist = json!([{
1146 "metric_prefix": "requests.",
1147 "tag_name": "customer_id",
1148 "values": ["customer-1"]
1149 }]);
1150 let (system, agent_tx) = connected_system(json!({
1151 "log_level": "warn",
1152 "metric_tag_value_allowlist": initial_allowlist
1153 }))
1154 .await;
1155
1156 agent_tx
1157 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1158 "metric_tag_value_allowlist",
1159 json!([
1160 { "metric_prefix": "requests.", "tag_name": "customer_id" },
1161 { "metric_prefix": "requests.api.", "tag_name": "customer_id" }
1162 ]),
1163 )))
1164 .await
1165 .unwrap();
1166 agent_tx
1167 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1168 "log_level",
1169 json!("error"),
1170 )))
1171 .await
1172 .unwrap();
1173
1174 await_config(&system, "the later valid update to take effect", |config| {
1175 config.control.logging.level == "error"
1176 })
1177 .await;
1178 let config = system.config();
1179 assert_eq!(config.domains.dogstatsd.tag_value_allowlist.len(), 1);
1180 assert_eq!(
1181 config.domains.dogstatsd.tag_value_allowlist[0].metric_prefix,
1182 "requests."
1183 );
1184 assert_eq!(config.domains.dogstatsd.tag_value_allowlist[0].values, ["customer-1"]);
1185 }
1186
1187 #[tokio::test]
1188 async fn converges_to_latest_value_under_burst() {
1189 let (system, agent_tx) = connected_system(json!({ "log_level": "info" })).await;
1190
1191 let burst = [
1192 "warn", "error", "debug", "trace", "info", "warn", "error", "debug", "trace", "info", "warn", "error",
1193 "debug", "trace", "info", "warn", "error", "debug", "trace",
1194 ];
1195 for (i, level) in burst.iter().enumerate() {
1196 agent_tx
1197 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1198 "log_level",
1199 json!(level),
1200 )))
1201 .await
1202 .unwrap();
1203 if i == burst.len() / 2 {
1207 agent_tx
1208 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1209 "dogstatsd_tag_cardinality",
1210 json!("bogus"),
1211 )))
1212 .await
1213 .unwrap();
1214 agent_tx
1215 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1216 "dogstatsd_tag_cardinality",
1217 json!("high"),
1218 )))
1219 .await
1220 .unwrap();
1221 }
1222 }
1223 let final_level = "error";
1224 agent_tx
1225 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1226 "log_level",
1227 json!(final_level),
1228 )))
1229 .await
1230 .unwrap();
1231
1232 await_config(
1233 &system,
1234 "the current configuration to converge to the final value",
1235 |c| c.control.logging.level == final_level,
1236 )
1237 .await;
1238 assert_eq!(system.config().control.logging.level, final_level);
1239 }
1240
1241 #[tokio::test]
1244 async fn a_defaulted_agent_value_does_not_erase_a_local_one() {
1245 let (system, agent_tx) = connected_system(json!({ "dd_url": "https://vector.example.com" })).await;
1246
1247 agent_tx
1248 .send(ConfigUpdate::snapshot([ConfigSetting::new(
1249 "dd_url",
1250 json!("https://app.datadoghq.com"),
1251 StreamProvenance::Default,
1252 )]))
1253 .await
1254 .unwrap();
1255 agent_tx
1257 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1258 "log_level",
1259 json!("error"),
1260 )))
1261 .await
1262 .unwrap();
1263 await_config(&system, "the trailing update to take effect", |c| {
1264 c.control.logging.level == "error"
1265 })
1266 .await;
1267
1268 let dd_url = &system.config().shared.endpoints.dd_url;
1269 assert!(dd_url.is_explicit());
1270 assert_eq!(dd_url.value, "https://vector.example.com");
1271 }
1272
1273 #[tokio::test]
1274 async fn demoting_an_agent_value_to_a_default_reveals_the_local_value() {
1275 let (system, agent_tx) = connected_system(json!({ "dd_url": "https://vector.example.com" })).await;
1276
1277 agent_tx
1279 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1280 "dd_url",
1281 json!("https://app.datadoghq.eu"),
1282 )))
1283 .await
1284 .unwrap();
1285 await_config(&system, "the Agent override to take effect", |c| {
1286 c.shared.endpoints.dd_url.value == "https://app.datadoghq.eu"
1287 })
1288 .await;
1289
1290 agent_tx
1294 .send(ConfigUpdate::Partial(ConfigSetting::new(
1295 "dd_url",
1296 json!("https://app.datadoghq.com"),
1297 StreamProvenance::Default,
1298 )))
1299 .await
1300 .unwrap();
1301
1302 await_config(&system, "the local value to be revealed again", |c| {
1303 c.shared.endpoints.dd_url.value == "https://vector.example.com"
1304 })
1305 .await;
1306 assert!(system.config().shared.endpoints.dd_url.is_explicit());
1307 }
1308
1309 #[tokio::test]
1310 async fn the_retained_sources_follow_accepted_updates_only() {
1311 let (system, agent_tx) = connected_system(json!({})).await;
1315 let all_pipelines = HashSet::new();
1316 system
1317 .check_compatibility(&all_pipelines)
1318 .expect("nothing unsupported is set yet");
1319
1320 agent_tx
1323 .send(ConfigUpdate::snapshot([
1324 ConfigSetting::explicit("heroku_dyno", json!(true)),
1325 ConfigSetting::explicit("dogstatsd_tag_cardinality", json!("bogus")),
1326 ]))
1327 .await
1328 .unwrap();
1329 agent_tx
1330 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1331 "log_level",
1332 json!("error"),
1333 )))
1334 .await
1335 .unwrap();
1336 await_config(&system, "the update following the rejected snapshot", |config| {
1337 config.control.logging.level == "error"
1338 })
1339 .await;
1340 system
1341 .check_compatibility(&all_pipelines)
1342 .expect("a rejected update does not reach the sources");
1343
1344 agent_tx
1345 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1346 "heroku_dyno",
1347 json!(true),
1348 )))
1349 .await
1350 .unwrap();
1351 agent_tx
1352 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1353 "log_level",
1354 json!("warn"),
1355 )))
1356 .await
1357 .unwrap();
1358 await_config(&system, "the accepted unsupported key", |config| {
1359 config.control.logging.level == "warn"
1360 })
1361 .await;
1362 system
1363 .check_compatibility(&all_pipelines)
1364 .expect_err("an accepted update reaches the sources");
1365 }
1366
1367 #[tokio::test]
1368 async fn a_snapshot_replaces_the_agent_layer() {
1369 let (system, agent_tx) = connected_system(json!({})).await;
1370
1371 agent_tx
1372 .send(ConfigUpdate::snapshot([ConfigSetting::explicit(
1373 "dogstatsd_port",
1374 json!(9125),
1375 )]))
1376 .await
1377 .unwrap();
1378 await_config(&system, "the first snapshot to take effect", |c| {
1379 c.domains.dogstatsd.listeners.port == 9125
1380 })
1381 .await;
1382
1383 agent_tx
1386 .send(ConfigUpdate::snapshot([ConfigSetting::explicit(
1387 "log_level",
1388 json!("error"),
1389 )]))
1390 .await
1391 .unwrap();
1392
1393 await_config(&system, "the replacing snapshot to take effect", |c| {
1394 c.control.logging.level == "error"
1395 })
1396 .await;
1397 assert_eq!(system.config().domains.dogstatsd.listeners.port, 8125);
1398 }
1399
1400 #[tokio::test]
1401 async fn a_live_view_wakes_when_only_provenance_changes() {
1402 let (system, agent_tx) = connected_system(json!({})).await;
1403 let mut dd_url = system.live(|c| &c.shared.endpoints.dd_url);
1404 assert_eq!(dd_url.provenance, Provenance::Default);
1406 assert_eq!(dd_url.value, "https://app.datadoghq.com");
1407
1408 agent_tx
1411 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1412 "dd_url",
1413 json!("https://app.datadoghq.com"),
1414 )))
1415 .await
1416 .unwrap();
1417
1418 let updated = tokio::time::timeout(Duration::from_secs(2), dd_url.changed())
1419 .await
1420 .expect("the view observes a provenance-only change");
1421 assert_eq!(updated.provenance, Provenance::Explicit);
1422 assert_eq!(updated.value, "https://app.datadoghq.com");
1423 }
1424
1425 #[tokio::test]
1426 async fn live_view_observes_debug_log_update() {
1427 let (system, agent_tx) = connected_system(json!({ "dogstatsd_metrics_stats_enable": false })).await;
1428 let mut view = system.live(|c| &c.domains.dogstatsd.debug_log);
1429 assert!(!view.metrics_stats_enable);
1430
1431 agent_tx
1432 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1433 "dogstatsd_metrics_stats_enable",
1434 json!(true),
1435 )))
1436 .await
1437 .unwrap();
1438
1439 let updated = tokio::time::timeout(Duration::from_secs(2), view.changed())
1440 .await
1441 .expect("view observes the debug-log update");
1442 assert!(updated.metrics_stats_enable);
1443 assert!(view.metrics_stats_enable);
1445 }
1446
1447 #[tokio::test]
1448 async fn field_view_wakes_on_its_field() {
1449 let (system, agent_tx) = connected_system(json!({ "dogstatsd_metrics_stats_enable": false })).await;
1452 let mut stats = system.live(|c| &c.domains.dogstatsd.debug_log.metrics_stats_enable);
1453 assert!(!*stats);
1454
1455 agent_tx
1456 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1457 "dogstatsd_metrics_stats_enable",
1458 json!(true),
1459 )))
1460 .await
1461 .unwrap();
1462
1463 let updated = tokio::time::timeout(Duration::from_secs(2), stats.changed())
1464 .await
1465 .expect("field view observes its field's update");
1466 assert!(updated);
1467 assert!(*stats);
1468 }
1469
1470 #[tokio::test]
1471 async fn live_metric_filter_follows_current_and_legacy_precedence() {
1472 let (system, agent_tx) = connected_system(json!({})).await;
1474 let mut metric_filter = system.live(|c| &c.domains.dogstatsd.metric_filter);
1475 assert!(metric_filter.values.is_empty());
1476
1477 agent_tx
1478 .send(ConfigUpdate::snapshot([
1479 ConfigSetting::explicit("metric_filterlist", json!(["current.duration.max"])),
1480 ConfigSetting::explicit("metric_filterlist_match_prefix", json!(false)),
1481 ConfigSetting::explicit("statsd_metric_blocklist", json!(["legacy.duration"])),
1482 ConfigSetting::explicit("statsd_metric_blocklist_match_prefix", json!(true)),
1483 ]))
1484 .await
1485 .unwrap();
1486
1487 let current = tokio::time::timeout(Duration::from_secs(2), metric_filter.changed())
1488 .await
1489 .expect("view observes the current filterlist taking precedence");
1490 assert_eq!(current.values, vec!["current.duration.max".to_string()]);
1491 assert!(!current.match_prefix);
1492
1493 agent_tx
1494 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1495 "metric_filterlist",
1496 json!([]),
1497 )))
1498 .await
1499 .unwrap();
1500
1501 let legacy = tokio::time::timeout(Duration::from_secs(2), metric_filter.changed())
1502 .await
1503 .expect("view observes the fallback to the legacy blocklist");
1504 assert_eq!(legacy.values, vec!["legacy.duration".to_string()]);
1505 assert!(legacy.match_prefix);
1506
1507 agent_tx
1508 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1509 "metric_filterlist",
1510 json!(["current.duration.avg"]),
1511 )))
1512 .await
1513 .unwrap();
1514
1515 let restored = tokio::time::timeout(Duration::from_secs(2), metric_filter.changed())
1516 .await
1517 .expect("view observes the current filterlist shadowing the legacy blocklist again");
1518 assert_eq!(restored.values, vec!["current.duration.avg".to_string()]);
1519 assert!(!restored.match_prefix);
1520 }
1521
1522 #[tokio::test]
1523 async fn live_metric_filter_tracks_prefix_rules() {
1524 let (system, agent_tx) = connected_system(json!({})).await;
1525 let mut metric_filter = system.live(|c| &c.domains.dogstatsd.metric_filter);
1526 assert!(metric_filter.prefix_rules.is_empty());
1527
1528 agent_tx
1529 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1530 "metric_filterlist_prefix",
1531 json!([
1532 {
1533 "prefix": "service.",
1534 "except_prefix": ["service.keep."],
1535 "except_exact": ["service.keep.exact"],
1536 }
1537 ]),
1538 )))
1539 .await
1540 .unwrap();
1541
1542 let updated = tokio::time::timeout(Duration::from_secs(2), metric_filter.changed())
1543 .await
1544 .expect("view observes metric_filterlist_prefix updates");
1545 assert_eq!(
1546 updated.prefix_rules,
1547 vec![MetricPrefixRule {
1548 prefix: "service.".to_string(),
1549 except_prefix: vec!["service.keep.".to_string()],
1550 except_exact: vec!["service.keep.exact".to_string()],
1551 }]
1552 );
1553 }
1554
1555 #[tokio::test]
1556 async fn fixed_view_never_changes() {
1557 let mut view: Live<bool> = Live::new_fixed(true);
1558 assert!(*view);
1559 assert!(tokio::time::timeout(Duration::from_millis(100), view.changed())
1561 .await
1562 .is_err());
1563 }
1564
1565 #[tokio::test]
1566 async fn live_stream_yields_each_change() {
1567 let (system, agent_tx) = connected_system(json!({})).await;
1568 let mut levels = Box::pin(system.live(|c| &c.control.logging.level).into_stream());
1569
1570 for level in ["debug", "warn"] {
1571 agent_tx
1572 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1573 "log_level",
1574 json!(level),
1575 )))
1576 .await
1577 .unwrap();
1578
1579 let updated = tokio::time::timeout(Duration::from_secs(2), levels.next())
1580 .await
1581 .expect("the stream observes the log level update");
1582 assert_eq!(updated.as_deref(), Some(level));
1583 }
1584 }
1585
1586 #[tokio::test]
1587 async fn fixed_live_stream_never_yields_or_ends() {
1588 let mut values = Box::pin(Live::new_fixed(true).into_stream());
1589 assert!(tokio::time::timeout(Duration::from_millis(100), values.next())
1591 .await
1592 .is_err());
1593 }
1594
1595 #[tokio::test]
1596 async fn live_views_reflect_startup_configuration() {
1597 let system = standalone_system(json!({ "dogstatsd_metrics_stats_enable": true })).expect("system builds");
1598 let config = system.config();
1599
1600 let debug_log = system.live(|c| &c.domains.dogstatsd.debug_log);
1601 assert_eq!(&*debug_log, &config.domains.dogstatsd.debug_log);
1602
1603 let prefix_filter = system.live(|c| &c.domains.dogstatsd.prefix_filter);
1604 assert_eq!(&*prefix_filter, &config.domains.dogstatsd.prefix_filter);
1605
1606 let multi_region_failover = system.live(|c| &c.domains.multi_region_failover);
1607 assert_eq!(&*multi_region_failover, &config.domains.multi_region_failover);
1608 }
1609
1610 #[test]
1611 fn translate_small_map_through_witness_and_seed() {
1612 let sources = SourceTree::all_explicit(json!({
1615 "api_key": "abc",
1616 "dd_url": "https://custom.example.com",
1617 "dogstatsd_port": 9125,
1618 "dogstatsd_tag_cardinality": "high",
1619 "expected_tags_duration": "15s",
1620 "provider_kind": "gke-autopilot",
1621 "eks_fargate": true,
1622 "kubernetes_kubelet_nodename": "fargate-node",
1623 "cluster_name": "fargate-cluster",
1624 "telemetry": { "dogstatsd_origin": true },
1625 "dogstatsd_tcp_port": 8126,
1626 }));
1627 let value = sources.to_value();
1628 let datadog: DatadogConfiguration = serde_json::from_value(value.clone()).expect("datadog source deserializes");
1629 let saluki: SalukiOnly = serde_json::from_value(value).expect("saluki-only source deserializes");
1630
1631 let (config, errors) = translate(&datadog, &saluki, &sources);
1632 assert!(errors.is_none(), "translation of a valid map records no error");
1633
1634 assert_eq!(config.domains.dogstatsd.listeners.port, 9125);
1636 assert_eq!(
1638 config.domains.dogstatsd.origin.tag_cardinality,
1639 OriginTagCardinality::High
1640 );
1641 assert_eq!(config.shared.tags.expected_tags_duration, Duration::from_secs(15));
1643 assert_eq!(config.shared.static_tags.provider_kind, "gke-autopilot");
1645 assert!(config.shared.static_tags.eks_fargate);
1646 assert_eq!(config.shared.static_tags.kubernetes_kubelet_nodename, "fargate-node");
1647 assert_eq!(config.shared.static_tags.cluster_name, "fargate-cluster");
1648 assert!(config.domains.dogstatsd.telemetry.origin_breakdown);
1650 assert_eq!(config.shared.endpoints.api_key, "abc");
1652 assert!(config.shared.endpoints.dd_url.is_explicit());
1653 assert_eq!(config.shared.endpoints.dd_url.value, "https://custom.example.com");
1654 assert_eq!(config.domains.dogstatsd.listeners.tcp_port, 8126);
1656 }
1657
1658 #[test]
1660 fn datadog_aggregation_keys_reach_the_model() {
1661 let sources = SourceTree::all_explicit(json!({
1662 "dogstatsd_expiry_seconds": 60,
1663 "dogstatsd_flush_incomplete_buckets": true,
1664 }));
1665 let value = sources.to_value();
1666 let datadog: DatadogConfiguration = serde_json::from_value(value.clone()).expect("datadog source deserializes");
1667 let saluki: SalukiOnly = serde_json::from_value(value).expect("saluki-only source deserializes");
1668
1669 let (config, errors) = translate(&datadog, &saluki, &sources);
1670 assert!(errors.is_none(), "translation of a valid map records no error");
1671
1672 let aggregation = &config.domains.dogstatsd.aggregation;
1673 assert_eq!(aggregation.counter_expiry_seconds, Some(60));
1674 assert!(aggregation.flush_open_windows);
1675 }
1676}