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 saluki_error::GenericError;
12use serde::Deserialize;
13use serde_json::Value;
14use snafu::Snafu;
15use tokio::sync::{mpsc, watch};
16use tracing::{debug, warn};
17
18use crate::saluki_only::SalukiOnly;
19use crate::source::SourceTree;
20use crate::translators::DatadogTranslator;
21
22#[derive(Debug, Snafu)]
24pub enum Error {
25 #[snafu(context(false), display("{source}"))]
27 Source {
28 source: ConfigurationError,
30 },
31
32 #[snafu(context(false), display("{source}"))]
34 Deserialize {
35 source: serde_json::Error,
37 },
38
39 #[snafu(display("configuration stream closed before the initial snapshot"))]
41 StreamClosed,
42
43 #[snafu(display("failed to build the configuration base: {message}"))]
45 Base {
46 message: String,
48 },
49
50 #[snafu(display("{source}"))]
52 Translate {
53 source: TranslateErrors,
55 },
56
57 #[snafu(display(
59 "no Datadog API key is configured: set `api_key` in the Datadog Agent's configuration, or \
60 `DD_API_KEY` in the environment. Every payload is authenticated with this key, so nothing can \
61 be submitted without one"
62 ))]
63 MissingApiKey,
64}
65
66type Result<T> = std::result::Result<T, Error>;
67
68pub struct ConfigurationSystem {
77 raw_map: GenericConfiguration,
78 current: Arc<ArcSwap<SalukiConfiguration>>,
79 tick: Arc<watch::Sender<()>>,
83}
84
85impl ConfigurationSystem {
86 pub(crate) async fn connected(
100 mut agent_rx: mpsc::Receiver<ConfigUpdate>, compat_tx: mpsc::Sender<ConfigUpdate>,
101 compat_map: GenericConfiguration, base: SourceTree,
102 ) -> Result<Self> {
103 let first = agent_rx.recv().await.ok_or(Error::StreamClosed)?;
105
106 let mut agent = SourceTree::empty();
109 fold(&mut agent, &first);
110 forward(&compat_tx, first).await;
111 compat_map.ready().await;
112
113 let config = translate_authoritative(&base.overlay(&agent))?;
118
119 let current = Arc::new(ArcSwap::from_pointee(config));
120 let (tick, _) = watch::channel(());
123 let tick = Arc::new(tick);
124
125 tokio::spawn(agent_loop(
126 agent_rx,
127 compat_tx,
128 base,
129 agent,
130 Arc::clone(¤t),
131 Arc::clone(&tick),
132 ));
133
134 Ok(Self {
135 raw_map: compat_map,
136 current,
137 tick,
138 })
139 }
140
141 pub(crate) fn standalone(compat_map: GenericConfiguration, config: SalukiConfiguration) -> Self {
145 let current = Arc::new(ArcSwap::from_pointee(config));
146 let (tick, _) = watch::channel(());
147 Self {
148 raw_map: compat_map,
149 current,
150 tick: Arc::new(tick),
151 }
152 }
153
154 pub fn live<T>(&self, project: impl for<'a> Fn(&'a SalukiConfiguration) -> &'a T + Send + Sync + 'static) -> Live<T>
157 where
158 T: Clone + PartialEq + 'static,
159 {
160 Live::new_dynamic(Arc::clone(&self.current), self.tick.subscribe(), project)
161 }
162
163 pub fn config(&self) -> arc_swap::Guard<Arc<SalukiConfiguration>> {
167 self.current.load()
168 }
169
170 pub fn current_handle(&self) -> Arc<ArcSwap<SalukiConfiguration>> {
173 Arc::clone(&self.current)
174 }
175
176 pub fn raw_map(&self) -> GenericConfiguration {
178 self.raw_map.clone()
179 }
180
181 pub fn raw_snapshot(&self) -> Arc<dyn Fn() -> std::result::Result<Value, GenericError> + Send + Sync> {
183 let raw_map = self.raw_map.clone();
184 Arc::new(move || raw_map.as_typed::<Value>().map_err(Into::into))
185 }
186}
187
188async fn agent_loop(
195 mut agent_rx: mpsc::Receiver<ConfigUpdate>, compat_tx: mpsc::Sender<ConfigUpdate>, base: SourceTree,
196 mut agent: SourceTree, current: Arc<ArcSwap<SalukiConfiguration>>, tick: Arc<watch::Sender<()>>,
197) {
198 while let Some(update) = agent_rx.recv().await {
199 let mut tentative = agent.clone();
203 fold(&mut tentative, &update);
204 match translate_authoritative(&base.overlay(&tentative)) {
205 Ok(config) => {
206 agent = tentative;
207 current.store(Arc::new(config));
208 tick.send_replace(());
209 debug!("Applied configuration update.");
210 }
211 Err(e) => warn!(
212 error = %e,
213 "Rejected configuration update; keeping the last-known-good typed configuration. The \
214 compatibility map still receives this update, so an un-migrated component may act on \
215 a value the typed model rejected."
216 ),
217 }
218 forward(&compat_tx, update).await;
223 }
224}
225
226fn fold(agent: &mut SourceTree, update: &ConfigUpdate) {
235 match update {
236 ConfigUpdate::Snapshot(settings) => *agent = SourceTree::from_settings(settings),
237 ConfigUpdate::Partial(setting) => agent.set(setting),
238 }
239}
240
241async fn forward(compat_tx: &mpsc::Sender<ConfigUpdate>, update: ConfigUpdate) {
243 let _ = compat_tx.send(update).await;
244}
245
246pub(crate) fn translate_strict(merged: &SourceTree) -> Result<SalukiConfiguration> {
252 let Sources { datadog, saluki } = deserialize_sources(&merged.to_value())?;
253 let (config, errors) = translate(&datadog, &saluki, merged);
254 if let Some(errors) = errors {
255 return Err(Error::Translate { source: errors });
256 }
257 Ok(config)
258}
259
260pub(crate) fn translate_authoritative(merged: &SourceTree) -> Result<SalukiConfiguration> {
270 let config = translate_strict(merged)?;
271 validate(&config)?;
272 Ok(config)
273}
274
275pub(crate) fn validate(config: &SalukiConfiguration) -> Result<()> {
294 if config.shared.endpoints.api_key.trim().is_empty() {
297 return Err(Error::MissingApiKey);
298 }
299
300 Ok(())
301}
302
303struct Sources {
310 datadog: DatadogConfiguration,
311 saluki: SalukiOnly,
312}
313
314fn deserialize_sources(merged: &Value) -> Result<Sources> {
321 let saluki = SalukiOnly::deserialize(merged)?;
322 let datadog = DatadogConfiguration::deserialize(merged)?;
323 Ok(Sources { datadog, saluki })
324}
325
326fn translate(
338 datadog: &DatadogConfiguration, saluki: &SalukiOnly, sources: &SourceTree,
339) -> (SalukiConfiguration, Option<TranslateErrors>) {
340 let (mut config, errors) = DatadogTranslator::new(datadog, sources).translate();
341 saluki.seed(&mut config);
342 (config, errors)
343}
344
345#[cfg(test)]
346mod tests {
347 use std::sync::Arc;
348 use std::time::Duration;
349
350 use agent_data_plane_config::domains::dogstatsd::OriginTagCardinality;
351 use agent_data_plane_config::shared::V3SeriesMode;
352 use agent_data_plane_config::Provenance;
353 use agent_data_plane_config::{Live, SalukiConfiguration};
354 use datadog_agent_config::DatadogConfiguration;
355 use saluki_config::dynamic::{ConfigSetting, ConfigUpdate, Provenance as StreamProvenance};
356 use saluki_config::ConfigurationLoader;
357 use serde_json::{json, Value};
358 use tokio::sync::mpsc;
359
360 use super::{
361 translate, translate_authoritative, translate_strict, ConfigurationSystem, Error, SalukiOnly, SourceTree,
362 };
363
364 const TEST_API_KEY: &str = "test-api-key";
370
371 async fn standalone_system(
376 file: Option<Value>, env: Option<&[(String, String)]>,
377 ) -> Result<ConfigurationSystem, Error> {
378 let (compat_map, _) = ConfigurationLoader::for_tests(file, env, false).await;
379 let base = SourceTree::all_explicit(compat_map.as_typed::<Value>().expect("base extracts"));
380 let config = translate_strict(&base)?;
381 Ok(ConfigurationSystem::standalone(compat_map, config))
382 }
383
384 async fn connected_system(mut base: Value) -> (ConfigurationSystem, mpsc::Sender<ConfigUpdate>) {
391 let (agent_tx, agent_rx) = mpsc::channel(100);
392 let (compat_map, compat_tx) = ConfigurationLoader::for_tests(None, None, true).await;
393 let compat_tx = compat_tx.expect("dynamic sender exists");
394 agent_tx.send(ConfigUpdate::snapshot([])).await.unwrap();
395 if let Some(base) = base.as_object_mut() {
396 base.entry("api_key").or_insert(json!(TEST_API_KEY));
397 }
398 let base = SourceTree::all_explicit(base);
399 let system = ConfigurationSystem::connected(agent_rx, compat_tx, compat_map, base)
400 .await
401 .expect("system builds");
402 (system, agent_tx)
403 }
404
405 async fn await_config(system: &ConfigurationSystem, what: &str, predicate: impl Fn(&SalukiConfiguration) -> bool) {
407 tokio::time::timeout(Duration::from_secs(2), async {
408 while !predicate(&system.config()) {
409 tokio::time::sleep(Duration::from_millis(5)).await;
410 }
411 })
412 .await
413 .unwrap_or_else(|_| panic!("timed out waiting for {what}"));
414 }
415
416 #[tokio::test]
417 async fn startup_current_reflects_translation() {
418 let system = standalone_system(Some(json!({ "log_level": "warn", "dogstatsd_port": 9125 })), None)
419 .await
420 .expect("system builds");
421 let config = system.config();
422
423 assert_eq!(config.control.logging.level, "warn");
424 assert_eq!(config.domains.dogstatsd.listeners.port, 9125);
425 }
426
427 #[test]
428 fn boolean_use_v3_api_series_enabled_is_normalized() {
429 let sources = SourceTree::all_explicit(json!({
430 "use_v3_api": {
431 "series": {
432 "enabled": true
433 }
434 }
435 }));
436
437 let config = translate_strict(&sources).expect("a boolean V3 series mode should translate");
438
439 assert_eq!(config.shared.metrics_encoding.v3_series_mode, V3SeriesMode::Enabled);
440 }
441
442 #[test]
443 fn compound_v3_series_endpoint_modes_are_rejected_at_startup() {
444 for mode in [json!(["true"]), json!({ "enabled": true })] {
445 let sources = SourceTree::all_explicit(json!({
446 "use_v3_api": { "series": { "endpoints": { "https://app.datadoghq.com": mode } } }
447 }));
448
449 assert!(translate_strict(&sources).is_err());
450 }
451 }
452
453 #[tokio::test]
454 async fn malformed_v3_endpoint_update_keeps_last_known_good_and_recovers() {
455 let (system, agent_tx) = connected_system(json!({
456 "dogstatsd_port": 9125,
457 "use_v3_api": { "series": { "endpoints": { "https://app.datadoghq.com": true } } }
458 }))
459 .await;
460
461 agent_tx
462 .send(ConfigUpdate::snapshot([
463 ConfigSetting::explicit("dogstatsd_port", json!(9999)),
464 ConfigSetting::explicit(
465 "use_v3_api.series.endpoints",
466 json!({ "https://app.datadoghq.com": ["false"] }),
467 ),
468 ]))
469 .await
470 .unwrap();
471 agent_tx
472 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
473 "log_level",
474 json!("error"),
475 )))
476 .await
477 .unwrap();
478 await_config(&system, "the update following the rejected snapshot", |config| {
479 config.control.logging.level == "error"
480 })
481 .await;
482
483 assert_eq!(system.config().domains.dogstatsd.listeners.port, 9125);
484 assert_eq!(
485 system.config().shared.metrics_encoding.v3_series_endpoint_modes["https://app.datadoghq.com"],
486 V3SeriesMode::Enabled
487 );
488
489 agent_tx
490 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
491 "use_v3_api.series.endpoints",
492 json!({ "https://app.datadoghq.com": false }),
493 )))
494 .await
495 .unwrap();
496 await_config(&system, "the corrected endpoint mode", |config| {
497 config.shared.metrics_encoding.v3_series_endpoint_modes["https://app.datadoghq.com"]
498 == V3SeriesMode::Disabled
499 })
500 .await;
501 }
502
503 #[tokio::test]
504 async fn connected_stream_translates_metrics_v3_routing_configuration() {
505 let (system, agent_tx) = connected_system(json!({
506 "data_plane": {
507 "metrics": {
508 "v3": {
509 "series": {
510 "enabled": true
511 }
512 }
513 }
514 }
515 }))
516 .await;
517
518 assert_eq!(
519 system.config().shared.metrics_encoding.v3_series_mode,
520 V3SeriesMode::DatadogOnly
521 );
522
523 agent_tx
524 .send(ConfigUpdate::snapshot([
525 ConfigSetting::explicit("serializer_compressor_kind", json!("zstd")),
526 ConfigSetting::explicit("serializer_experimental_use_v3_api.compression_level", json!(7)),
527 ConfigSetting::explicit("use_v2_api.series", json!(false)),
528 ConfigSetting::explicit("use_v3_api.series.enabled", json!("false")),
529 ConfigSetting::explicit(
531 "use_v3_api.series.endpoints",
532 json!({ "https://app.datadoghq.com": "true" }),
533 ),
534 ConfigSetting::explicit("observability_pipelines_worker.metrics.enabled", json!(true)),
535 ConfigSetting::explicit(
536 "observability_pipelines_worker.metrics.url",
537 json!("https://opw.example.com"),
538 ),
539 ConfigSetting::explicit("observability_pipelines_worker.metrics.use_v3_api.series", json!(true)),
540 ]))
541 .await
542 .unwrap();
543
544 await_config(&system, "the streamed metrics V3 routing configuration", |config| {
545 config.shared.metrics_encoding.v3_series_mode == V3SeriesMode::Disabled
546 && config
547 .shared
548 .metrics_encoding
549 .v3_series_endpoint_modes
550 .get("https://app.datadoghq.com")
551 == Some(&V3SeriesMode::Enabled)
552 })
553 .await;
554
555 let config = system.config();
556 let metrics = &config.shared.metrics_encoding;
557 assert!(!metrics.use_v2_series_api);
558 assert_eq!(metrics.v3_api.compression_level, 7);
559 let opw = &config.shared.endpoints.opw_intake;
560 assert!(opw.enabled);
561 assert_eq!(opw.url, "https://opw.example.com");
562 assert!(opw.use_v3_series);
563 }
564
565 #[tokio::test]
566 async fn raw_snapshot_reflects_streamed_updates() {
567 let (system, agent_tx) = connected_system(json!({})).await;
568 let snapshot = system.raw_snapshot();
569 let cloned = Arc::clone(&snapshot);
570
571 assert_eq!(snapshot().expect("serializes").pointer("/dogstatsd_port"), None);
572
573 agent_tx
574 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
575 "dogstatsd_port",
576 json!(9125),
577 )))
578 .await
579 .unwrap();
580
581 tokio::time::timeout(Duration::from_secs(2), async {
582 while cloned().expect("serializes").pointer("/dogstatsd_port") != Some(&json!(9125)) {
583 tokio::time::sleep(Duration::from_millis(5)).await;
584 }
585 })
586 .await
587 .expect("timed out waiting for the raw snapshot to reflect the update");
588 }
589
590 #[tokio::test]
591 async fn nested_datadog_key_reaches_the_model() {
592 let system = standalone_system(
596 Some(json!({
597 "autoscaling": {
598 "failover": {
599 "enabled": true,
600 "metrics": "container.memory.usage container.cpu.usage",
601 }
602 }
603 })),
604 None,
605 )
606 .await
607 .expect("system builds");
608 let config = system.config();
609
610 assert!(config.shared.autoscaling_failover.enabled);
611 assert_eq!(
612 config.shared.autoscaling_failover.metrics,
613 vec!["container.memory.usage".to_string(), "container.cpu.usage".to_string()]
614 );
615 }
616
617 #[tokio::test]
618 async fn unset_autoscaling_failover_keeps_its_schema_defaults() {
619 let system = standalone_system(Some(json!({})), None).await.expect("system builds");
622 let config = system.config();
623
624 assert!(!config.shared.autoscaling_failover.enabled);
625 assert_eq!(
626 config.shared.autoscaling_failover.metrics,
627 vec!["container.memory.usage".to_string(), "container.cpu.usage".to_string()]
628 );
629 }
630
631 #[tokio::test]
632 async fn nested_saluki_only_key_seeds_the_model() {
633 let system = standalone_system(Some(json!({ "data_plane": { "standalone_mode": true } })), None)
634 .await
635 .expect("system builds");
636
637 assert!(system.config().control.standalone_mode);
638 }
639
640 #[tokio::test]
641 async fn a_flattened_spelling_of_a_nested_key_is_not_read() {
642 let system = standalone_system(Some(json!({ "autoscaling_failover_enabled": true })), None)
647 .await
648 .expect("system builds");
649
650 assert!(!system.config().shared.autoscaling_failover.enabled);
651 }
652
653 #[tokio::test]
654 async fn load_fails_on_translation_invalid_startup_config() {
655 let result = standalone_system(Some(json!({ "dogstatsd_tag_cardinality": "bogus" })), None).await;
658
659 assert!(matches!(result, Err(Error::Translate { .. })));
660 }
661
662 #[tokio::test]
663 async fn negative_dogstatsd_workers_count_is_rejected_at_startup() {
664 let result = standalone_system(Some(json!({ "dogstatsd_workers_count": -1 })), None).await;
665
666 let Err(error) = result else {
667 panic!("negative worker count should fail the startup translation gate");
668 };
669 assert!(matches!(error, Error::Translate { .. }));
670 assert!(error.to_string().contains("dogstatsd_workers_count"));
671 assert!(error.to_string().contains("greater than or equal to 0"));
672 }
673
674 #[test]
675 fn zero_otlp_trace_interner_size_is_rejected() {
676 let sources = SourceTree::all_explicit(json!({ "otlp_config": { "traces": { "string_interner_size": 0 } } }));
679 let error = translate_strict(&sources).expect_err("zero trace interner size should fail translation");
680
681 assert!(matches!(error, Error::Deserialize { .. }));
682 assert!(error.to_string().contains("value of bytes must be greater than zero"));
683 }
684
685 #[test]
686 fn oversized_otlp_trace_interner_size_is_rejected() {
687 let sources =
688 SourceTree::all_explicit(json!({ "otlp_config": { "traces": { "string_interner_size": "2GiB" } } }));
689 let error = translate_strict(&sources).expect_err("oversized trace interner should fail translation");
690
691 assert!(matches!(error, Error::Deserialize { .. }));
692 assert!(error.to_string().contains("must not exceed 1073741824 bytes"));
693 }
694
695 #[test]
696 fn positive_otlp_trace_interner_size_is_accepted() {
697 let sources =
698 SourceTree::all_explicit(json!({ "otlp_config": { "traces": { "string_interner_size": "512KiB" } } }));
699 let config = translate_strict(&sources).expect("positive trace interner size should translate");
700
701 assert_eq!(config.domains.otlp.traces.string_interner_size.get(), 512 * 1024);
702 }
703
704 #[test]
705 fn invalid_metric_tag_value_allowlist_is_rejected_before_publication() {
706 let sources = SourceTree::all_explicit(json!({
707 "metric_tag_value_allowlist": [
708 { "metric_prefix": "requests.", "tag_name": "customer_id" },
709 { "metric_prefix": "requests.api.", "tag_name": "customer_id" }
710 ]
711 }));
712 let error = translate_strict(&sources).expect_err("overlapping allow-list prefixes should fail translation");
713
714 assert!(matches!(error, Error::Deserialize { .. }));
715 assert!(error.to_string().contains("overlapping metric prefixes"));
716 }
717
718 #[tokio::test]
719 async fn standalone_loads_numeric_byte_size() {
720 let system = standalone_system(Some(json!({ "dogstatsd_log_file_max_size": 10485760 })), None)
724 .await
725 .expect("numeric byte size boots");
726
727 assert_eq!(system.config().domains.dogstatsd.debug_log.log_file_max_size, 10485760);
728 }
729
730 #[test]
731 fn a_configuration_without_an_api_key_is_rejected() {
732 let sources = SourceTree::all_explicit(json!({}));
736 translate_strict(&sources).expect("an absent API key still translates");
737
738 let error = translate_authoritative(&sources).expect_err("an absent API key should fail the gate");
739
740 assert!(matches!(error, Error::MissingApiKey));
741 assert!(error.to_string().contains("api_key"));
742 }
743
744 #[test]
745 fn a_blank_api_key_is_rejected() {
746 for key in ["", " ", "\t\n"] {
749 let sources = SourceTree::all_explicit(json!({ "api_key": key }));
750
751 assert!(
752 matches!(translate_authoritative(&sources), Err(Error::MissingApiKey)),
753 "{key:?} should not count as an API key"
754 );
755 }
756
757 let sources = SourceTree::all_explicit(json!({ "api_key": TEST_API_KEY }));
758 assert_eq!(
759 TEST_API_KEY,
760 translate_authoritative(&sources)
761 .expect("a real key passes the gate")
762 .shared
763 .endpoints
764 .api_key
765 );
766 }
767
768 #[tokio::test]
769 async fn an_update_that_blanks_the_api_key_is_rejected_keeping_last_known_good() {
770 let (system, agent_tx) = connected_system(json!({ "log_level": "warn" })).await;
773 assert_eq!(TEST_API_KEY, system.config().shared.endpoints.api_key);
774
775 agent_tx
776 .send(ConfigUpdate::Partial(ConfigSetting::explicit("api_key", json!(""))))
777 .await
778 .unwrap();
779 agent_tx
780 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
781 "log_level",
782 json!("error"),
783 )))
784 .await
785 .unwrap();
786
787 await_config(&system, "the later valid update to take effect", |c| {
788 c.control.logging.level == "error"
789 })
790 .await;
791 assert_eq!(TEST_API_KEY, system.config().shared.endpoints.api_key);
792 }
793
794 #[tokio::test]
795 async fn standalone_loads_scalars_written_in_any_form_the_agent_casts() {
796 let system = standalone_system(
801 Some(json!({
802 "use_v3_api": { "series": { "enabled": true } },
803 "dogstatsd_port": "8126",
804 })),
805 None,
806 )
807 .await
808 .expect("scalars in Agent-castable forms boot");
809
810 assert_eq!(
811 system.config().shared.metrics_encoding.v3_series_mode,
812 V3SeriesMode::Enabled
813 );
814 assert_eq!(system.config().domains.dogstatsd.listeners.port, 8126);
815 }
816
817 #[tokio::test]
818 async fn translation_invalid_update_is_rejected_keeping_last_known_good() {
819 let (system, agent_tx) =
820 connected_system(json!({ "log_level": "warn", "dogstatsd_tag_cardinality": "high" })).await;
821 assert_eq!(
822 system.config().domains.dogstatsd.origin.tag_cardinality,
823 OriginTagCardinality::High
824 );
825
826 agent_tx
829 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
830 "dogstatsd_tag_cardinality",
831 json!("bogus"),
832 )))
833 .await
834 .unwrap();
835 agent_tx
836 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
837 "log_level",
838 json!("error"),
839 )))
840 .await
841 .unwrap();
842
843 await_config(&system, "the later valid update to take effect", |c| {
844 c.control.logging.level == "error"
845 })
846 .await;
847 assert_eq!(
850 system.config().domains.dogstatsd.origin.tag_cardinality,
851 OriginTagCardinality::High
852 );
853 }
854
855 #[tokio::test]
856 async fn invalid_metric_tag_value_allowlist_update_keeps_last_known_good() {
857 let initial_allowlist = json!([{
858 "metric_prefix": "requests.",
859 "tag_name": "customer_id",
860 "values": ["customer-1"]
861 }]);
862 let (system, agent_tx) = connected_system(json!({
863 "log_level": "warn",
864 "metric_tag_value_allowlist": initial_allowlist
865 }))
866 .await;
867
868 agent_tx
869 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
870 "metric_tag_value_allowlist",
871 json!([
872 { "metric_prefix": "requests.", "tag_name": "customer_id" },
873 { "metric_prefix": "requests.api.", "tag_name": "customer_id" }
874 ]),
875 )))
876 .await
877 .unwrap();
878 agent_tx
879 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
880 "log_level",
881 json!("error"),
882 )))
883 .await
884 .unwrap();
885
886 await_config(&system, "the later valid update to take effect", |config| {
887 config.control.logging.level == "error"
888 })
889 .await;
890 let config = system.config();
891 assert_eq!(config.domains.dogstatsd.tag_value_allowlist.len(), 1);
892 assert_eq!(
893 config.domains.dogstatsd.tag_value_allowlist[0].metric_prefix,
894 "requests."
895 );
896 assert_eq!(config.domains.dogstatsd.tag_value_allowlist[0].values, ["customer-1"]);
897 }
898
899 #[tokio::test]
900 async fn converges_to_latest_value_under_burst() {
901 let (system, agent_tx) = connected_system(json!({ "log_level": "info" })).await;
902
903 let burst = [
904 "warn", "error", "debug", "trace", "info", "warn", "error", "debug", "trace", "info", "warn", "error",
905 "debug", "trace", "info", "warn", "error", "debug", "trace",
906 ];
907 for (i, level) in burst.iter().enumerate() {
908 agent_tx
909 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
910 "log_level",
911 json!(level),
912 )))
913 .await
914 .unwrap();
915 if i == burst.len() / 2 {
919 agent_tx
920 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
921 "dogstatsd_tag_cardinality",
922 json!("bogus"),
923 )))
924 .await
925 .unwrap();
926 agent_tx
927 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
928 "dogstatsd_tag_cardinality",
929 json!("high"),
930 )))
931 .await
932 .unwrap();
933 }
934 }
935 let final_level = "error";
936 agent_tx
937 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
938 "log_level",
939 json!(final_level),
940 )))
941 .await
942 .unwrap();
943
944 await_config(
945 &system,
946 "the current configuration to converge to the final value",
947 |c| c.control.logging.level == final_level,
948 )
949 .await;
950 assert_eq!(system.config().control.logging.level, final_level);
951 }
952
953 #[tokio::test]
956 async fn a_defaulted_agent_value_does_not_erase_a_local_one() {
957 let (system, agent_tx) = connected_system(json!({ "dd_url": "https://vector.example.com" })).await;
958
959 agent_tx
960 .send(ConfigUpdate::snapshot([ConfigSetting::new(
961 "dd_url",
962 json!("https://app.datadoghq.com"),
963 StreamProvenance::Default,
964 )]))
965 .await
966 .unwrap();
967 agent_tx
969 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
970 "log_level",
971 json!("error"),
972 )))
973 .await
974 .unwrap();
975 await_config(&system, "the trailing update to take effect", |c| {
976 c.control.logging.level == "error"
977 })
978 .await;
979
980 let dd_url = &system.config().shared.endpoints.dd_url;
981 assert!(dd_url.is_explicit());
982 assert_eq!(dd_url.value, "https://vector.example.com");
983 }
984
985 #[tokio::test]
986 async fn demoting_an_agent_value_to_a_default_reveals_the_local_value() {
987 let (system, agent_tx) = connected_system(json!({ "dd_url": "https://vector.example.com" })).await;
988
989 agent_tx
991 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
992 "dd_url",
993 json!("https://app.datadoghq.eu"),
994 )))
995 .await
996 .unwrap();
997 await_config(&system, "the Agent override to take effect", |c| {
998 c.shared.endpoints.dd_url.value == "https://app.datadoghq.eu"
999 })
1000 .await;
1001
1002 agent_tx
1006 .send(ConfigUpdate::Partial(ConfigSetting::new(
1007 "dd_url",
1008 json!("https://app.datadoghq.com"),
1009 StreamProvenance::Default,
1010 )))
1011 .await
1012 .unwrap();
1013
1014 await_config(&system, "the local value to be revealed again", |c| {
1015 c.shared.endpoints.dd_url.value == "https://vector.example.com"
1016 })
1017 .await;
1018 assert!(system.config().shared.endpoints.dd_url.is_explicit());
1019 }
1020
1021 #[tokio::test]
1022 async fn a_snapshot_replaces_the_agent_layer() {
1023 let (system, agent_tx) = connected_system(json!({})).await;
1024
1025 agent_tx
1026 .send(ConfigUpdate::snapshot([ConfigSetting::explicit(
1027 "dogstatsd_port",
1028 json!(9125),
1029 )]))
1030 .await
1031 .unwrap();
1032 await_config(&system, "the first snapshot to take effect", |c| {
1033 c.domains.dogstatsd.listeners.port == 9125
1034 })
1035 .await;
1036
1037 agent_tx
1040 .send(ConfigUpdate::snapshot([ConfigSetting::explicit(
1041 "log_level",
1042 json!("error"),
1043 )]))
1044 .await
1045 .unwrap();
1046
1047 await_config(&system, "the replacing snapshot to take effect", |c| {
1048 c.control.logging.level == "error"
1049 })
1050 .await;
1051 assert_eq!(system.config().domains.dogstatsd.listeners.port, 8125);
1052 }
1053
1054 #[tokio::test]
1055 async fn a_live_view_wakes_when_only_provenance_changes() {
1056 let (system, agent_tx) = connected_system(json!({})).await;
1057 let mut dd_url = system.live(|c| &c.shared.endpoints.dd_url);
1058 assert_eq!(dd_url.provenance, Provenance::Default);
1060 assert_eq!(dd_url.value, "https://app.datadoghq.com");
1061
1062 agent_tx
1065 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1066 "dd_url",
1067 json!("https://app.datadoghq.com"),
1068 )))
1069 .await
1070 .unwrap();
1071
1072 let updated = tokio::time::timeout(Duration::from_secs(2), dd_url.changed())
1073 .await
1074 .expect("the view observes a provenance-only change");
1075 assert_eq!(updated.provenance, Provenance::Explicit);
1076 assert_eq!(updated.value, "https://app.datadoghq.com");
1077 }
1078
1079 #[tokio::test]
1080 async fn live_view_observes_debug_log_update() {
1081 let (system, agent_tx) = connected_system(json!({ "dogstatsd_metrics_stats_enable": false })).await;
1082 let mut view = system.live(|c| &c.domains.dogstatsd.debug_log);
1083 assert!(!view.metrics_stats_enable);
1084
1085 agent_tx
1086 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1087 "dogstatsd_metrics_stats_enable",
1088 json!(true),
1089 )))
1090 .await
1091 .unwrap();
1092
1093 let updated = tokio::time::timeout(Duration::from_secs(2), view.changed())
1094 .await
1095 .expect("view observes the debug-log update");
1096 assert!(updated.metrics_stats_enable);
1097 assert!(view.metrics_stats_enable);
1099 }
1100
1101 #[tokio::test]
1102 async fn field_view_wakes_on_its_field() {
1103 let (system, agent_tx) = connected_system(json!({ "dogstatsd_metrics_stats_enable": false })).await;
1106 let mut stats = system.live(|c| &c.domains.dogstatsd.debug_log.metrics_stats_enable);
1107 assert!(!*stats);
1108
1109 agent_tx
1110 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1111 "dogstatsd_metrics_stats_enable",
1112 json!(true),
1113 )))
1114 .await
1115 .unwrap();
1116
1117 let updated = tokio::time::timeout(Duration::from_secs(2), stats.changed())
1118 .await
1119 .expect("field view observes its field's update");
1120 assert!(updated);
1121 assert!(*stats);
1122 }
1123
1124 #[tokio::test]
1125 async fn live_metric_filter_follows_current_and_legacy_precedence() {
1126 let (system, agent_tx) = connected_system(json!({})).await;
1128 let mut metric_filter = system.live(|c| &c.domains.dogstatsd.metric_filter);
1129 assert!(metric_filter.values.is_empty());
1130
1131 agent_tx
1132 .send(ConfigUpdate::snapshot([
1133 ConfigSetting::explicit("metric_filterlist", json!(["current.duration.max"])),
1134 ConfigSetting::explicit("metric_filterlist_match_prefix", json!(false)),
1135 ConfigSetting::explicit("statsd_metric_blocklist", json!(["legacy.duration"])),
1136 ConfigSetting::explicit("statsd_metric_blocklist_match_prefix", json!(true)),
1137 ]))
1138 .await
1139 .unwrap();
1140
1141 let current = tokio::time::timeout(Duration::from_secs(2), metric_filter.changed())
1142 .await
1143 .expect("view observes the current filterlist taking precedence");
1144 assert_eq!(current.values, vec!["current.duration.max".to_string()]);
1145 assert!(!current.match_prefix);
1146
1147 agent_tx
1148 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1149 "metric_filterlist",
1150 json!([]),
1151 )))
1152 .await
1153 .unwrap();
1154
1155 let legacy = tokio::time::timeout(Duration::from_secs(2), metric_filter.changed())
1156 .await
1157 .expect("view observes the fallback to the legacy blocklist");
1158 assert_eq!(legacy.values, vec!["legacy.duration".to_string()]);
1159 assert!(legacy.match_prefix);
1160
1161 agent_tx
1162 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
1163 "metric_filterlist",
1164 json!(["current.duration.avg"]),
1165 )))
1166 .await
1167 .unwrap();
1168
1169 let restored = tokio::time::timeout(Duration::from_secs(2), metric_filter.changed())
1170 .await
1171 .expect("view observes the current filterlist shadowing the legacy blocklist again");
1172 assert_eq!(restored.values, vec!["current.duration.avg".to_string()]);
1173 assert!(!restored.match_prefix);
1174 }
1175
1176 #[tokio::test]
1177 async fn fixed_view_never_changes() {
1178 let mut view: Live<bool> = Live::new_fixed(true);
1179 assert!(*view);
1180 assert!(tokio::time::timeout(Duration::from_millis(100), view.changed())
1182 .await
1183 .is_err());
1184 }
1185
1186 #[tokio::test]
1187 async fn live_views_reflect_startup_configuration() {
1188 let system = standalone_system(Some(json!({ "dogstatsd_metrics_stats_enable": true })), None)
1189 .await
1190 .expect("system builds");
1191 let config = system.config();
1192
1193 let debug_log = system.live(|c| &c.domains.dogstatsd.debug_log);
1194 assert_eq!(&*debug_log, &config.domains.dogstatsd.debug_log);
1195
1196 let prefix_filter = system.live(|c| &c.domains.dogstatsd.prefix_filter);
1197 assert_eq!(&*prefix_filter, &config.domains.dogstatsd.prefix_filter);
1198
1199 let multi_region_failover = system.live(|c| &c.domains.multi_region_failover);
1200 assert_eq!(&*multi_region_failover, &config.domains.multi_region_failover);
1201 }
1202
1203 #[test]
1204 fn translate_small_map_through_witness_and_seed() {
1205 let sources = SourceTree::all_explicit(json!({
1208 "api_key": "abc",
1209 "dd_url": "https://custom.example.com",
1210 "dogstatsd_port": 9125,
1211 "dogstatsd_tag_cardinality": "high",
1212 "expected_tags_duration": "15s",
1213 "provider_kind": "gke-autopilot",
1214 "eks_fargate": true,
1215 "kubernetes_kubelet_nodename": "fargate-node",
1216 "cluster_name": "fargate-cluster",
1217 "telemetry": { "dogstatsd_origin": true },
1218 "dogstatsd_tcp_port": 8126,
1219 }));
1220 let value = sources.to_value();
1221 let datadog: DatadogConfiguration = serde_json::from_value(value.clone()).expect("datadog source deserializes");
1222 let saluki: SalukiOnly = serde_json::from_value(value).expect("saluki-only source deserializes");
1223
1224 let (config, errors) = translate(&datadog, &saluki, &sources);
1225 assert!(errors.is_none(), "translation of a valid map records no error");
1226
1227 assert_eq!(config.domains.dogstatsd.listeners.port, 9125);
1229 assert_eq!(
1231 config.domains.dogstatsd.origin.tag_cardinality,
1232 OriginTagCardinality::High
1233 );
1234 assert_eq!(config.shared.tags.expected_tags_duration, Duration::from_secs(15));
1236 assert_eq!(config.shared.static_tags.provider_kind, "gke-autopilot");
1238 assert!(config.shared.static_tags.eks_fargate);
1239 assert_eq!(config.shared.static_tags.kubernetes_kubelet_nodename, "fargate-node");
1240 assert_eq!(config.shared.static_tags.cluster_name, "fargate-cluster");
1241 assert!(config.domains.dogstatsd.telemetry.origin_breakdown);
1243 assert_eq!(config.shared.endpoints.api_key, "abc");
1245 assert!(config.shared.endpoints.dd_url.is_explicit());
1246 assert_eq!(config.shared.endpoints.dd_url.value, "https://custom.example.com");
1247 assert_eq!(config.domains.dogstatsd.listeners.tcp_port, 8126);
1249 }
1250
1251 #[test]
1253 fn datadog_aggregation_keys_reach_the_model() {
1254 let sources = SourceTree::all_explicit(json!({
1255 "dogstatsd_expiry_seconds": 60,
1256 "dogstatsd_flush_incomplete_buckets": true,
1257 }));
1258 let value = sources.to_value();
1259 let datadog: DatadogConfiguration = serde_json::from_value(value.clone()).expect("datadog source deserializes");
1260 let saluki: SalukiOnly = serde_json::from_value(value).expect("saluki-only source deserializes");
1261
1262 let (config, errors) = translate(&datadog, &saluki, &sources);
1263 assert!(errors.is_none(), "translation of a valid map records no error");
1264
1265 let aggregation = &config.domains.dogstatsd.aggregation;
1266 assert_eq!(aggregation.counter_expiry_seconds, Some(60));
1267 assert!(aggregation.flush_open_windows);
1268 }
1269}