agent_data_plane_config_system/
system.rs

1//! [`ConfigurationSystem`]: the runtime configuration, translated from the raw sources and kept
2//! current as the Datadog Agent streams updates.
3
4use 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/// An error building the translated configuration from the raw sources.
23#[derive(Debug, Snafu)]
24pub enum Error {
25    /// The configuration value could not be read from the raw configuration map.
26    #[snafu(context(false), display("{source}"))]
27    Source {
28        /// The underlying configuration error.
29        source: ConfigurationError,
30    },
31
32    /// A source model could not be deserialized from the merged configuration value.
33    #[snafu(context(false), display("{source}"))]
34    Deserialize {
35        /// The underlying deserialization error.
36        source: serde_json::Error,
37    },
38
39    /// The Datadog Agent closed the configuration stream before sending the initial snapshot.
40    #[snafu(display("configuration stream closed before the initial snapshot"))]
41    StreamClosed,
42
43    /// The typed base could not be built from the file and environment.
44    #[snafu(display("failed to build the configuration base: {message}"))]
45    Base {
46        /// What went wrong reading the file, parsing YAML, or decoding an environment variable.
47        message: String,
48    },
49
50    /// Translating the sources into the model failed on one or more keys.
51    #[snafu(display("{source}"))]
52    Translate {
53        /// Every translation error recorded.
54        source: TranslateErrors,
55    },
56
57    /// The fully merged configuration resolved no usable Datadog API key.
58    #[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
68/// The runtime configuration, translated from the raw sources and kept current.
69///
70/// The configuration system is the single owner of the Datadog Agent's `ConfigUpdate` stream. It
71/// folds each update onto the local source base to build the typed [`SalukiConfiguration`] directly,
72/// and forwards the same update to a legacy [`GenericConfiguration`] compatibility map so
73/// un-migrated components can still read by key. The current configuration lives in an [`ArcSwap`]
74/// cell so readers load a whole, self-consistent version with no lock, while the update task
75/// replaces it in one atomic store.
76pub struct ConfigurationSystem {
77    raw_map: GenericConfiguration,
78    current: Arc<ArcSwap<SalukiConfiguration>>,
79    // Fired once after each accepted update so live views wake and re-project. Shared with the
80    // update task via `Arc` because `watch::Sender` is not `Clone` and both the system (to mint
81    // views) and the task (to notify) need it.
82    tick: Arc<watch::Sender<()>>,
83}
84
85impl ConfigurationSystem {
86    /// Connected authority: takes ownership of the Datadog Agent's config stream, forwards each
87    /// update to the compatibility map, and builds the typed model directly from the stream folded
88    /// onto the local `base` (file + environment).
89    ///
90    /// Blocks for the first authoritative snapshot and is the strict startup gate: a snapshot that
91    /// never arrives, cannot be deserialized, or fails translation aborts the boot. `async` because
92    /// the update task requires a Tokio runtime; keeping that requirement visible here avoids a
93    /// panic deep inside `tokio::spawn`.
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if the stream closes before the first snapshot, or the initial configuration
98    /// cannot be deserialized or translated.
99    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        // The first stream message is the authoritative initial snapshot.
104        let first = agent_rx.recv().await.ok_or(Error::StreamClosed)?;
105
106        // Fold it into the accumulating Agent layer and forward it to the compat map, then wait for
107        // the compat map to apply it so `raw_map()` is populated before any consumer reads it.
108        let mut agent = SourceTree::empty();
109        fold(&mut agent, &first);
110        forward(&compat_tx, first).await;
111        compat_map.ready().await;
112
113        // Startup is the strict gate: this is the first, authoritative Agent snapshot, so any error
114        // fails the boot and we never run on bad config. At runtime (see `agent_loop`) the same
115        // check instead rejects the offending update and keeps the last-known-good configuration,
116        // because a runtime update must never take the system down.
117        let config = translate_authoritative(&base.overlay(&agent))?;
118
119        let current = Arc::new(ArcSwap::from_pointee(config));
120        // The initial receiver is dropped immediately; `send_replace` works with zero receivers, and
121        // each live view subscribes its own receiver from the sender.
122        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(&current),
131            Arc::clone(&tick),
132        ));
133
134        Ok(Self {
135            raw_map: compat_map,
136            current,
137            tick,
138        })
139    }
140
141    /// Installs a static configuration without an update task.
142    ///
143    /// Live views retain their initial values because this system sends no update notifications.
144    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    /// Returns a live view of the given projection of the current configuration. Narrow further with
155    /// [`Live::project`]. This is the only way a consumer subscribes to runtime updates.
156    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    /// Loads the current translated configuration.
164    ///
165    /// The returned guard pins one whole version; a concurrent refresh never tears the read.
166    pub fn config(&self) -> arc_swap::Guard<Arc<SalukiConfiguration>> {
167        self.current.load()
168    }
169
170    /// Returns a shared handle to the current-configuration cell for readers that load it
171    /// independently.
172    pub fn current_handle(&self) -> Arc<ArcSwap<SalukiConfiguration>> {
173        Arc::clone(&self.current)
174    }
175
176    /// Returns the raw source map for consumers that read configuration by key.
177    pub fn raw_map(&self) -> GenericConfiguration {
178        self.raw_map.clone()
179    }
180
181    /// Returns a callback that reads the current raw configuration as JSON.
182    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
188/// Owns the Datadog Agent config stream for the life of the process: validates each update against
189/// the typed model, commits it on success, and forwards it to the by-key configuration view. Ends
190/// when the stream closes.
191///
192/// Each update is processed individually (no burst collapse) so a rejection can be attributed to the
193/// exact update that caused it. Updates are infrequent, so re-translating per update is cheap.
194async 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        // Validate-then-commit: fold onto a tentative copy of the Agent layer and drive the typed
200        // model from it. Only a fully successful update advances the committed layer, so a rejected
201        // value never lingers to re-poison a later merge.
202        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        // The compatibility map receives every update faithfully, whether or not the typed path
219        // accepted it: un-migrated components keep the Agent's permissive behavior during migration.
220        // The updater owns the receiver; if it is gone, no un-migrated component is reading the
221        // by-key view, so dropping the forward is fine.
222        forward(&compat_tx, update).await;
223    }
224}
225
226/// Folds one update into the accumulating Agent layer.
227///
228/// `Snapshot` replaces the layer; `Partial` applies one (possibly dotted) key, the same handling the
229/// `saluki-config` updater uses, so this layer applies Agent updates the same way as the compatibility
230/// view.
231///
232/// Each setting's provenance is retained, which is what lets a later update that demotes a value to
233/// an Agent default stop shadowing the local value it had been overriding.
234fn 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
241/// Forwards one update to the compatibility map's updater.
242async fn forward(compat_tx: &mpsc::Sender<ConfigUpdate>, update: ConfigUpdate) {
243    let _ = compat_tx.send(update).await;
244}
245
246/// Deserializes and translates merged source values, rejecting partially translated configuration.
247///
248/// # Errors
249///
250/// Returns an error if either source model cannot be deserialized or any key fails translation.
251pub(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
260/// Translates merged sources that are authoritative for the running process, rejecting a
261/// configuration ADP cannot run on.
262///
263/// This is [`translate_strict`] plus [`validate`]. Use it where the merged sources are complete: the
264/// Datadog Agent's snapshot layered over the local base, or the local base alone in standalone mode.
265///
266/// # Errors
267///
268/// Returns an error if translation fails, or if the translated configuration fails validation.
269pub(crate) fn translate_authoritative(merged: &SourceTree) -> Result<SalukiConfiguration> {
270    let config = translate_strict(merged)?;
271    validate(&config)?;
272    Ok(config)
273}
274
275/// Checks the invariants a configuration must satisfy for this process to do useful work.
276///
277/// Translation alone cannot make these checks. It converts one key at a time and every schema key
278/// has a default, so a setting the operator never supplied is indistinguishable from one they did
279/// until the whole merged configuration is in hand.
280///
281/// Apply this only to an authoritative configuration. The local snapshot
282/// [`LoadedConfiguration::load`][crate::LoadedConfiguration::load] produces is incomplete by design:
283/// under the Datadog Agent the API key arrives over the configuration stream, so a local-only
284/// snapshot legitimately has none, and CLI subcommands read that snapshot without ever submitting a
285/// payload.
286///
287/// # Errors
288///
289/// Returns [`Error::MissingApiKey`] if no usable API key resolved. Every payload ADP submits is
290/// authenticated with this key, so an empty one turns each flush into a rejected request that the
291/// forwarder then retries. Failing here names the cause once instead of leaving an operator to infer
292/// it from a stream of authentication failures.
293pub(crate) fn validate(config: &SalukiConfiguration) -> Result<()> {
294    // A blank key is as unusable as an absent one, and a padded key is a typo we should name rather
295    // than send.
296    if config.shared.endpoints.api_key.trim().is_empty() {
297        return Err(Error::MissingApiKey);
298    }
299
300    Ok(())
301}
302
303// TODO: A map/array-valued schema leaf is replaced wholesale when any source (file, environment, or
304// the Agent config stream) supplies it. Verify this is the intended semantic for the remote Agent
305// config stream: ADP is that stream's first consumer, so the correct behavior for a stream update to
306// a map-shaped setting may not have been defined yet.
307
308/// The sources deserialized from the merged configuration value, separated by source authority.
309struct Sources {
310    datadog: DatadogConfiguration,
311    saluki: SalukiOnly,
312}
313
314/// Deserializes both source models from the merged configuration value.
315///
316/// The source models use ordinary serde-compatible field types, so deserializing from
317/// `serde_json::Value` preserves the values. Both read the canonical nested shape: the local base
318/// is built that way by the schema-driven environment readers, and the Datadog Agent's stream
319/// delivers dotted keys that are nested on arrival.
320fn 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
326/// Translates the Datadog and Saluki-only sources into one [`SalukiConfiguration`], returning every
327/// error recorded while converting an individual Datadog value.
328///
329/// The Datadog `drive` feeds every supported key to a `DatadogTranslator`; a value that cannot be
330/// converted leaves its field at the model default and records an error. The Saluki-only values
331/// then seed their disjoint destinations, which cannot fail. The returned configuration is always
332/// complete: every valid value is present, and every invalid one holds its default.
333///
334/// `sources` is the same merged layer the models were deserialized from. The translator consults it
335/// for provenance, which a deserialized source model cannot supply: a schema key with a default is
336/// always present, so its value alone cannot say whether an input set it explicitly.
337fn 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    /// API key the connected-system fixture puts in the local base.
365    ///
366    /// An authoritative configuration must resolve one (see [`super::validate`]), and putting it in
367    /// the base rather than in a streamed snapshot keeps it in place across the snapshot replacements
368    /// these tests exercise.
369    const TEST_API_KEY: &str = "test-api-key";
370
371    /// Builds a standalone system whose authority is the local sources (`file` + `env`).
372    ///
373    /// Translates without validating so a test can state only the setting it is exercising. The
374    /// production standalone path validates; `loaded.rs` covers that.
375    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    /// Builds a connected system whose base is `base` and whose authority is the returned Agent
385    /// stream. The initial (empty) snapshot is queued before the system blocks on it, so the caller
386    /// gets back a stream ready for `Partial`/`Snapshot` updates.
387    ///
388    /// `base` is given an [`api_key`][TEST_API_KEY] unless it states its own, so a caller varying
389    /// some unrelated setting need not restate what validation requires.
390    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    /// Polls the current configuration until `predicate` holds, failing if it never does.
406    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                // The Agent sends an object-valued setting whole, and these entry keys contain dots.
530                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        // Sources deliver the Agent's canonical nested shape, which is what the Datadog
593        // deserializer reads. A string list supplied as one space-separated string (the form an
594        // environment variable carries) is still split on whitespace at the leaf.
595        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        // Nothing is set here, so both fields must come back as the schema defaults; the component
620        // layer no longer supplies fallbacks of its own.
621        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        // Nothing translates `autoscaling_failover_enabled` into the nested slot: resolving an
643        // environment variable to its canonical path is the environment readers' job, and they do it
644        // before a value ever reaches this point. A flattened key arriving from any other source is
645        // simply not a key the model knows.
646        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        // Startup is the strict gate: a value figment accepts but the model rejects fails the load,
656        // so the process never boots on bad config.
657        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        // Component builders used to discover this after translation. Reject zero before publishing
677        // an invalid typed model.
678        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        // A byte-size setting documented as accepting a bare integer (`10485760`) rather than a
721        // string (`"10MB"`) must not abort the strict startup gate. The typed model normalizes it,
722        // and the translator resolves it to the same byte count.
723        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        // Nothing ADP submits is accepted without a key, so the authoritative gate names the cause
733        // rather than letting every flush fail authentication. Translation alone accepts this: the
734        // schema default for `api_key` is the empty string, so nothing is missing to translate.
735        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        // An explicitly blank key is as unusable as an absent one, and whitespace is a typo worth
747        // reporting rather than submitting.
748        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        // Validation covers runtime updates too: an update that leaves the process unable to submit
771        // anything is rejected like any other invalid one, and the working key stays in place.
772        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        // The Agent reads a setting by casting whatever its configuration holds to the accessor's
797        // type, so a boolean written where the schema declares a string, or a quoted integer, is a
798        // configuration it accepts. Each must reach the typed model instead of aborting the strict
799        // startup gate.
800        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        // Send a translation-invalid update, then a valid update to a different field. Updates are
827        // processed in order, so once the second is observed the first has already been handled.
828        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        // The invalid update was rejected whole: the field keeps its last-known-good value rather
848        // than falling back to a default, and the later valid update still applied.
849        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            // Interleave a translation-invalid update mid-burst, then correct it. The invalid value
916            // is rejected whole (last-known-good retained) rather than wedging the task, so the
917            // baseline keeps converging on the latest valid value regardless of the transient bad one.
918            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    // Issue #1965: the Core Agent streams every setting it knows about, including the ones nobody
954    // configured, so its schema defaults must not overwrite the local file.
955    #[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        // A later unrelated update is observable, so once it lands the snapshot above has been handled.
968        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        // The Agent takes over the setting.
990        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        // The operator removes it from the Agent's configuration, so the Agent now reports its own
1003        // default. The Agent layer must stop shadowing the local value rather than pinning the value
1004        // it last held.
1005        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        // A snapshot is the producer's complete state, so a setting it omits is no longer set and the
1038        // port returns to the schema default rather than lingering at 9125.
1039        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        // Nothing has set the URL, so it is the schema default the Agent supplies.
1059        assert_eq!(dd_url.provenance, Provenance::Default);
1060        assert_eq!(dd_url.value, "https://app.datadoghq.com");
1061
1062        // The same URL, now deliberately chosen. The value is unchanged, so only provenance can carry
1063        // the fact that it became an override.
1064        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        // `Deref` reflects the value returned by the last `changed`.
1098        assert!(view.metrics_stats_enable);
1099    }
1100
1101    #[tokio::test]
1102    async fn field_view_wakes_on_its_field() {
1103        // Projecting straight to a single field needs no schema change and no central registration:
1104        // the granularity is chosen at the call site.
1105        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        // Regression: clearing `metric_filterlist` must restore the legacy list and match mode.
1127        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        // A fixed view never resolves, so this bound is deterministic rather than timing-dependent.
1181        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        // A small raw source map exercising a scalar conversion, an enum parse, a duration parse, the
1206        // raw endpoint inputs, and one seeded Saluki-only field.
1207        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        // Driven scalar conversion: i64 -> u16.
1228        assert_eq!(config.domains.dogstatsd.listeners.port, 9125);
1229        // Driven enum parse.
1230        assert_eq!(
1231            config.domains.dogstatsd.origin.tag_cardinality,
1232            OriginTagCardinality::High
1233        );
1234        // Driven `format: duration` parse: a Go duration string becomes a `Duration`.
1235        assert_eq!(config.shared.tags.expected_tags_duration, Duration::from_secs(15));
1236        // Shared deployment inputs used to derive static tags for both DogStatsD and OTLP.
1237        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        // Driven bool in a nested Datadog section.
1242        assert!(config.domains.dogstatsd.telemetry.origin_breakdown);
1243        // Raw endpoint inputs: carried through without selecting a primary endpoint here.
1244        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        // Seeded Saluki-only field.
1248        assert_eq!(config.domains.dogstatsd.listeners.tcp_port, 8126);
1249    }
1250
1251    /// Datadog-defined aggregation keys reach their typed model fields through the witness translator.
1252    #[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}