1use std::path::Path;
10
11use agent_data_plane_config::SalukiConfiguration;
12use datadog_agent_config::apply_datadog_env;
13use saluki_config::dynamic::ConfigUpdate;
14use serde_json::Value;
15use tokio::sync::mpsc;
16
17use crate::saluki_env_overlay;
18use crate::source::SourceTree;
19use crate::system::{translate_strict, validate, ConfigurationSystem, ConfigurationUpdates, Error};
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum EnvPrecedence {
24 BeforeFile,
28 AfterFile,
31 Disabled,
33}
34
35pub struct LoadedConfiguration {
37 base: SourceTree,
39 local: SalukiConfiguration,
42}
43
44impl LoadedConfiguration {
45 pub async fn load(path: impl AsRef<Path>, env: EnvPrecedence) -> Result<Self, Error> {
56 let base = SourceTree::all_explicit(build_base(path.as_ref(), env)?);
59 let local = translate_strict(&base)?;
60 Ok(Self { base, local })
61 }
62
63 pub fn local(&self) -> &SalukiConfiguration {
67 &self.local
68 }
69
70 pub async fn run(
81 self, config_stream: mpsc::Receiver<ConfigUpdate>,
82 ) -> Result<(ConfigurationSystem, ConfigurationUpdates), Error> {
83 ConfigurationSystem::connected(config_stream, self.base).await
84 }
85
86 pub async fn standalone(self) -> Result<ConfigurationSystem, Error> {
96 validate(&self.local)?;
97 Ok(ConfigurationSystem::standalone(self.local, self.base))
98 }
99}
100
101fn build_base(path: &Path, env: EnvPrecedence) -> Result<Value, Error> {
109 let text = std::fs::read_to_string(path).map_err(|e| Error::Base {
110 message: format!("read `{}`: {e}", path.display()),
111 })?;
112 let mut base: Value = saluki_config::yaml::from_str(&text).map_err(|e| Error::Base {
113 message: format!("parse `{}`: {e}", path.display()),
114 })?;
115 drop_nulls(&mut base);
116 if base.is_null() {
117 base = Value::Object(serde_json::Map::new());
118 }
119
120 let overwrite = match env {
121 EnvPrecedence::Disabled => return Ok(base),
122 EnvPrecedence::AfterFile => true,
123 EnvPrecedence::BeforeFile => false,
124 };
125 apply_datadog_env(&mut base, overwrite).map_err(|message| Error::Base { message })?;
126 saluki_env_overlay::apply_env(&mut base, overwrite).map_err(|message| Error::Base { message })?;
127 Ok(base)
128}
129
130fn drop_nulls(value: &mut Value) {
133 if let Value::Object(map) = value {
134 map.retain(|_, v| !v.is_null());
135 for v in map.values_mut() {
136 drop_nulls(v);
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use bytesize::ByteSize;
144 use saluki_config::dynamic::ConfigSetting;
145 use saluki_config::test_env_lock;
146 use serde_json::json;
147
148 use super::*;
149
150 #[test]
151 fn build_base_composes_file_and_environment_by_precedence() {
152 let _guard = test_env_lock();
153 let path = std::env::temp_dir().join(format!("adp_build_base_{}.yaml", std::process::id()));
154 std::fs::write(
155 &path,
156 "dogstatsd_port: 8125\ndogstatsd_non_local_traffic: false\nempty_key:\n",
157 )
158 .unwrap();
159 std::env::set_var("DD_DOGSTATSD_PORT", "9125");
160
161 let base = build_base(&path, EnvPrecedence::AfterFile).expect("base builds");
164 assert_eq!(base.get("dogstatsd_port"), Some(&json!(9125)));
165 assert_eq!(base.get("dogstatsd_non_local_traffic"), Some(&json!(false)));
166 assert!(base.get("empty_key").is_none());
167
168 let base = build_base(&path, EnvPrecedence::BeforeFile).expect("base builds");
170 assert_eq!(base.get("dogstatsd_port"), Some(&json!(8125)));
171
172 let base = build_base(&path, EnvPrecedence::Disabled).expect("base builds");
174 assert_eq!(base.get("dogstatsd_port"), Some(&json!(8125)));
175
176 std::env::remove_var("DD_DOGSTATSD_PORT");
177 std::fs::remove_file(&path).ok();
178 }
179
180 #[tokio::test]
181 async fn local_exposes_translated_configuration() {
182 let path = std::env::temp_dir().join(format!("adp_local_{}.yaml", std::process::id()));
184 std::fs::write(&path, "log_level: warn\ndogstatsd_port: 9125\n").unwrap();
185
186 let loaded = LoadedConfiguration::load(&path, EnvPrecedence::Disabled)
187 .await
188 .expect("local sources load");
189 let config = loaded.local();
190
191 assert_eq!(config.control.logging.level, "warn");
192 assert_eq!(config.domains.dogstatsd.listeners.port, 9125);
193
194 std::fs::remove_file(&path).ok();
195 }
196
197 #[tokio::test]
198 async fn load_rejects_translation_invalid_local_sources() {
199 let path = std::env::temp_dir().join(format!("adp_local_bad_{}.yaml", std::process::id()));
200 std::fs::write(&path, "dogstatsd_tag_cardinality: bogus\n").unwrap();
202
203 let result = LoadedConfiguration::load(&path, EnvPrecedence::Disabled).await;
204
205 std::fs::remove_file(&path).ok();
206 assert!(matches!(result, Err(Error::Translate { .. })));
207 }
208
209 #[tokio::test]
210 async fn load_leaves_an_incomplete_local_snapshot_to_the_selected_authority() {
211 let path = std::env::temp_dir().join(format!("adp_no_api_key_{}.yaml", std::process::id()));
215 std::fs::write(&path, "log_level: warn\n").unwrap();
216
217 let loaded = LoadedConfiguration::load(&path, EnvPrecedence::Disabled)
218 .await
219 .expect("a local snapshot without an API key loads");
220 assert_eq!("", loaded.local().shared.endpoints.api_key);
221
222 let result = loaded.standalone().await;
224
225 std::fs::remove_file(&path).ok();
226 assert!(matches!(result, Err(Error::MissingApiKey)));
227 }
228
229 async fn load_from_file(test: &str, contents: &str) -> LoadedConfiguration {
233 let path = std::env::temp_dir().join(format!("adp_{test}_{}.yaml", std::process::id()));
234 std::fs::write(&path, contents).unwrap();
235 let loaded = LoadedConfiguration::load(&path, EnvPrecedence::Disabled)
236 .await
237 .expect("local sources load");
238 std::fs::remove_file(&path).ok();
239 loaded
240 }
241
242 async fn await_log_level(system: &ConfigurationSystem, level: &str) {
244 tokio::time::timeout(std::time::Duration::from_secs(2), async {
245 while system.config().control.logging.level != level {
246 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
247 }
248 })
249 .await
250 .unwrap_or_else(|_| panic!("the configuration should report the log level `{level}`"));
251 }
252
253 fn log_level_update(level: &str) -> ConfigUpdate {
254 ConfigUpdate::Partial(ConfigSetting::explicit("log_level", json!(level)))
255 }
256
257 #[tokio::test]
258 async fn run_applies_later_updates_only_once_the_updates_run() {
259 let loaded = load_from_file("run_updates", "api_key: test-api-key\nlog_level: info\n").await;
260
261 let (agent_tx, agent_rx) = mpsc::channel(8);
263 agent_tx.send(ConfigUpdate::snapshot([])).await.unwrap();
264 let (system, updates) = loaded.run(agent_rx).await.expect("the initial snapshot is accepted");
265 assert_eq!(system.config().control.logging.level, "info");
266
267 agent_tx.send(log_level_update("warn")).await.unwrap();
270 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
271 assert_eq!(system.config().control.logging.level, "info");
272
273 tokio::spawn(updates.run());
274 await_log_level(&system, "warn").await;
275 }
276
277 #[tokio::test]
278 async fn updates_continue_where_a_stopped_run_left_off() {
279 let loaded = load_from_file("run_restart", "api_key: test-api-key\nlog_level: info\n").await;
280
281 let (agent_tx, agent_rx) = mpsc::channel(8);
282 agent_tx.send(ConfigUpdate::snapshot([])).await.unwrap();
283 let (system, updates) = loaded.run(agent_rx).await.expect("the initial snapshot is accepted");
284
285 let first_run = tokio::spawn(updates.run());
286 agent_tx.send(log_level_update("warn")).await.unwrap();
287 await_log_level(&system, "warn").await;
288
289 first_run.abort();
292 let _ = first_run.await;
293 agent_tx.send(log_level_update("error")).await.unwrap();
294
295 tokio::spawn(updates.run());
296 await_log_level(&system, "error").await;
297 }
298
299 #[tokio::test]
300 async fn updates_fail_once_the_stream_closes() {
301 let loaded = load_from_file("run_ended", "api_key: test-api-key\n").await;
302
303 let (agent_tx, agent_rx) = mpsc::channel(8);
304 agent_tx.send(ConfigUpdate::snapshot([])).await.unwrap();
305 let (_system, updates) = loaded.run(agent_rx).await.expect("the initial snapshot is accepted");
306 drop(agent_tx);
307
308 assert!(matches!(updates.run().await, Err(Error::UpdateStreamClosed)));
310 assert!(matches!(updates.run().await, Err(Error::UpdateStreamClosed)));
311 }
312
313 #[tokio::test]
314 async fn run_fails_if_the_stream_closes_before_the_initial_snapshot() {
315 let loaded = load_from_file("run_closed", "api_key: test-api-key\n").await;
316
317 let (agent_tx, agent_rx) = mpsc::channel::<ConfigUpdate>(1);
318 drop(agent_tx);
319
320 let result = loaded.run(agent_rx).await;
321 assert!(matches!(result, Err(Error::StreamClosed)));
322 }
323
324 fn block_on<F: std::future::Future>(future: F) -> F::Output {
328 tokio::runtime::Builder::new_current_thread()
329 .enable_time()
330 .build()
331 .expect("runtime builds")
332 .block_on(future)
333 }
334
335 #[test]
336 fn a_saluki_only_environment_variable_reaches_the_model() {
337 let _guard = test_env_lock();
340 let path = std::env::temp_dir().join(format!("adp_saluki_env_{}.yaml", std::process::id()));
341 std::fs::write(&path, "{}\n").unwrap();
342 std::env::set_var("DD_DATA_PLANE_STANDALONE_MODE", "true");
343
344 let loaded = block_on(LoadedConfiguration::load(&path, EnvPrecedence::AfterFile)).expect("local sources load");
345
346 std::env::remove_var("DD_DATA_PLANE_STANDALONE_MODE");
347 std::fs::remove_file(&path).ok();
348 assert!(loaded.local().control.standalone_mode);
349 }
350
351 #[test]
352 fn trace_env_environment_variables_reach_the_model() {
353 let _guard = test_env_lock();
357 let path = std::env::temp_dir().join(format!("adp_trace_env_{}.yaml", std::process::id()));
358 std::fs::write(&path, "{}\n").unwrap();
359 std::env::set_var("DD_TAGS", "a:b env:tagged");
360 std::env::set_var("DD_EXTRA_TAGS", "env:extra");
361
362 std::env::set_var("DD_APM_ENV", "apm");
363 std::env::set_var("DD_ENV", "prod");
364 let set = block_on(LoadedConfiguration::load(&path, EnvPrecedence::AfterFile)).expect("local sources load");
365
366 std::env::set_var("DD_APM_ENV", "");
367 std::env::set_var("DD_ENV", "");
368 let empty = block_on(LoadedConfiguration::load(&path, EnvPrecedence::AfterFile)).expect("local sources load");
369
370 for var in ["DD_APM_ENV", "DD_ENV", "DD_TAGS", "DD_EXTRA_TAGS"] {
371 std::env::remove_var(var);
372 }
373 std::fs::remove_file(&path).ok();
374
375 let traces = &set.local().domains.traces;
376 assert_eq!(traces.apm_env.as_deref(), Some("apm"));
377 assert_eq!(traces.env.as_deref(), Some("prod"));
378
379 let traces = &empty.local().domains.traces;
380 assert_eq!(traces.apm_env, None);
381 assert_eq!(traces.env, None);
382 let tags = &empty.local().shared.tags;
383 assert_eq!(tags.tags, ["a:b", "env:tagged"]);
384 assert_eq!(tags.extra_tags, ["env:extra"]);
385 }
386
387 #[test]
388 fn ottl_filter_error_mode_environment_variable_reaches_the_model() {
389 use agent_data_plane_config::domains::traces::OttlErrorMode;
393
394 let _guard = test_env_lock();
395 let path = std::env::temp_dir().join(format!("adp_ottl_env_{}.yaml", std::process::id()));
396 std::fs::write(&path, "{}\n").unwrap();
397 std::env::set_var("DD_OTTL_FILTER_CONFIG_ERROR_MODE", "silent");
398
399 let loaded = block_on(LoadedConfiguration::load(&path, EnvPrecedence::AfterFile)).expect("local sources load");
400
401 std::env::remove_var("DD_OTTL_FILTER_CONFIG_ERROR_MODE");
402 std::fs::remove_file(&path).ok();
403 assert_eq!(
404 loaded.local().domains.traces.ottl_filter.error_mode,
405 OttlErrorMode::Silent
406 );
407 }
408
409 #[test]
410 fn a_structured_saluki_only_environment_variable_reaches_the_model() {
411 let _guard = test_env_lock();
412 let path = std::env::temp_dir().join(format!("adp_saluki_structured_env_{}.yaml", std::process::id()));
413 std::fs::write(&path, "{}\n").unwrap();
414 std::env::set_var(
415 "DD_METRIC_TAG_VALUE_ALLOWLIST",
416 r#"[{"metric_prefix":"requests.","tag_name":"customer_id","values":["customer-1"],"on_miss":"replace","replacement":"other"}]"#,
417 );
418
419 let loaded = block_on(LoadedConfiguration::load(&path, EnvPrecedence::AfterFile)).expect("local sources load");
420
421 std::env::remove_var("DD_METRIC_TAG_VALUE_ALLOWLIST");
422 std::fs::remove_file(&path).ok();
423 let entries = &loaded.local().domains.dogstatsd.tag_value_allowlist;
424 assert_eq!(entries.len(), 1);
425 assert_eq!(entries[0].metric_prefix, "requests.");
426 assert_eq!(entries[0].tag_name, "customer_id");
427 assert_eq!(entries[0].values, ["customer-1"]);
428 assert_eq!(
429 entries[0].on_miss,
430 agent_data_plane_config::domains::dogstatsd::TagValueMismatchAction::Replace
431 );
432 assert_eq!(entries[0].replacement, "other");
433 }
434
435 #[test]
436 fn metric_tag_value_allowlist_from_file_preserves_whitespace() {
437 let path = std::env::temp_dir().join(format!("adp_tag_value_allowlist_{}.yaml", std::process::id()));
438 std::fs::write(
439 &path,
440 "metric_tag_value_allowlist:\n - metric_prefix: ' requests. '\n tag_name: ' customer_id '\n values: [' customer-1 ']\n on_miss: replace\n replacement: ' other '\n",
441 )
442 .unwrap();
443
444 let loaded = block_on(LoadedConfiguration::load(&path, EnvPrecedence::Disabled))
445 .expect("allow-list strings containing whitespace should load");
446
447 std::fs::remove_file(&path).ok();
448 let entries = &loaded.local().domains.dogstatsd.tag_value_allowlist;
449 assert_eq!(entries.len(), 1);
450 assert_eq!(entries[0].metric_prefix, " requests. ");
451 assert_eq!(entries[0].tag_name, " customer_id ");
452 assert_eq!(entries[0].values, [" customer-1 "]);
453 assert_eq!(entries[0].replacement, " other ");
454 }
455
456 #[test]
457 fn invalid_metric_tag_value_allowlist_from_environment_fails_load() {
458 let _guard = test_env_lock();
459 let path = std::env::temp_dir().join(format!("adp_bad_tag_value_allowlist_env_{}.yaml", std::process::id()));
460 std::fs::write(&path, "{}\n").unwrap();
461 std::env::set_var(
462 "DD_METRIC_TAG_VALUE_ALLOWLIST",
463 r#"[{"metric_prefix":"requests.","tag_name":"customer_id"},{"metric_prefix":"requests.api.","tag_name":"customer_id"}]"#,
464 );
465
466 let result = block_on(LoadedConfiguration::load(&path, EnvPrecedence::AfterFile));
467
468 std::env::remove_var("DD_METRIC_TAG_VALUE_ALLOWLIST");
469 std::fs::remove_file(&path).ok();
470 let error = match result {
471 Err(error) => error,
472 Ok(_) => panic!("overlapping environment allow-list should fail loading"),
473 };
474 assert!(matches!(error, Error::Deserialize { .. }));
475 assert!(error.to_string().contains("overlapping metric prefixes"));
476 }
477
478 #[test]
479 fn the_adp_zstd_override_reaches_the_model_from_the_environment() {
480 let _guard = test_env_lock();
483 let path = std::env::temp_dir().join(format!("adp_zstd_env_{}.yaml", std::process::id()));
484 std::fs::write(&path, "{}\n").unwrap();
485 std::env::set_var("DD_DATA_PLANE_SERIALIZER_ZSTD_COMPRESSOR_LEVEL", "7");
486
487 let loaded = block_on(LoadedConfiguration::load(&path, EnvPrecedence::AfterFile)).expect("local sources load");
488 let level = loaded.local().shared.endpoints.compression.effective_zstd_level();
489
490 std::env::remove_var("DD_DATA_PLANE_SERIALIZER_ZSTD_COMPRESSOR_LEVEL");
491 std::fs::remove_file(&path).ok();
492 assert_eq!(level, 7);
493 }
494
495 #[test]
496 fn the_adp_stop_timeout_override_reaches_the_model_from_the_environment() {
497 let _guard = test_env_lock();
498 let path = std::env::temp_dir().join(format!("adp_stop_timeout_env_{}.yaml", std::process::id()));
499 std::fs::write(&path, "{}\n").unwrap();
500 std::env::set_var("DD_DATA_PLANE_STOP_TIMEOUT", "45");
501
502 let loaded = block_on(LoadedConfiguration::load(&path, EnvPrecedence::AfterFile)).expect("local sources load");
503 let stop_timeout = loaded.local().control.stop_timeout;
504
505 std::env::remove_var("DD_DATA_PLANE_STOP_TIMEOUT");
506 std::fs::remove_file(&path).ok();
507 assert_eq!(stop_timeout, Some(std::time::Duration::from_secs(45)));
508 }
509
510 struct ScopedEnv(Vec<&'static str>);
514
515 impl ScopedEnv {
516 fn set(vars: &[(&'static str, &str)]) -> Self {
517 for (name, value) in vars {
518 std::env::set_var(name, value);
519 }
520 Self(vars.iter().map(|(name, _)| *name).collect())
521 }
522 }
523
524 impl Drop for ScopedEnv {
525 fn drop(&mut self) {
526 for name in &self.0 {
527 std::env::remove_var(name);
528 }
529 }
530 }
531
532 fn load_with_environment(
534 test: &str, contents: &str, vars: &[(&'static str, &str)],
535 ) -> Result<LoadedConfiguration, Error> {
536 let _guard = test_env_lock();
537 let path = std::env::temp_dir().join(format!("adp_{test}_{}.yaml", std::process::id()));
538 std::fs::write(&path, contents).unwrap();
539 let _env = ScopedEnv::set(vars);
540
541 let loaded = block_on(LoadedConfiguration::load(&path, EnvPrecedence::AfterFile));
542
543 std::fs::remove_file(&path).ok();
544 loaded
545 }
546
547 #[test]
548 fn agent_spellings_of_valid_local_inputs_load_and_survive_the_stream() {
549 let loaded = load_with_environment(
552 "agent_spellings",
553 "api_key: test-api-key\ndogstatsd_buffer_size: \"010\"\n",
554 &[
555 ("DD_DOGSTATSD_PORT", "0x10"),
556 ("DD_AGENT_IPC_GRPC_MAX_MESSAGE_SIZE", "0x10"),
557 ("DD_CMD_PORT", "0x1F90"),
558 ("DD_APM_OBFUSCATION_ELASTICSEARCH_KEEP_VALUES", r#"["cr-a","cr-b"]"#),
559 ],
560 )
561 .expect("the Agent's spellings load");
562
563 let local = loaded.local();
564 assert_eq!(local.domains.dogstatsd.listeners.port, 16);
565 assert_eq!(local.control.ipc.grpc_max_message_size, 16);
566 assert_eq!(local.control.ipc.cmd_port, 8080);
567 assert_eq!(
568 local.domains.traces.obfuscation.elasticsearch.keep_values,
569 ["cr-a", "cr-b"]
570 );
571 assert_eq!(local.domains.dogstatsd.listeners.buffer_size, 8);
572
573 block_on(async {
575 let (agent_tx, agent_rx) = mpsc::channel(8);
576 agent_tx
577 .send(ConfigUpdate::snapshot([ConfigSetting::explicit(
578 "dogstatsd_port",
579 json!(9125),
580 )]))
581 .await
582 .unwrap();
583 let (system, _updates) = loaded.run(agent_rx).await.expect("the initial snapshot is accepted");
584
585 let config = system.config();
586 assert_eq!(config.domains.dogstatsd.listeners.port, 9125);
587 assert_eq!(config.control.ipc.cmd_port, 8080);
588 assert_eq!(config.domains.dogstatsd.listeners.buffer_size, 8);
589 });
590 }
591
592 async fn await_dogstatsd_port(system: &ConfigurationSystem, port: u16) {
594 tokio::time::timeout(std::time::Duration::from_secs(2), async {
595 while system.config().domains.dogstatsd.listeners.port != port {
596 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
597 }
598 })
599 .await
600 .unwrap_or_else(|_| panic!("the configuration should report the DogStatsD port {port}"));
601 }
602
603 #[test]
604 fn an_invalid_octal_integer_reads_as_zero_from_every_source() {
605 let loaded = load_with_environment(
608 "invalid_octal",
609 "api_key: test-api-key\ndogstatsd_buffer_size: \"0128\"\n",
610 &[
611 ("DD_DOGSTATSD_PORT", "08"),
612 ("DD_CMD_PORT", "08.0"),
613 ("DD_AGENT_IPC_GRPC_MAX_MESSAGE_SIZE", "-09"),
614 ],
615 )
616 .expect("an invalid octal integer loads");
617
618 let local = loaded.local();
619 assert_eq!(local.domains.dogstatsd.listeners.port, 0);
620 assert_eq!(local.control.ipc.cmd_port, 0);
621 assert_eq!(local.control.ipc.grpc_max_message_size, 0);
622 assert_eq!(local.domains.dogstatsd.listeners.buffer_size, 0);
623
624 block_on(async {
625 let (agent_tx, agent_rx) = mpsc::channel(8);
627 agent_tx
628 .send(ConfigUpdate::snapshot([
629 ConfigSetting::explicit("dogstatsd_port", json!("010")),
630 ConfigSetting::explicit("dogstatsd_buffer_size", json!("09")),
631 ]))
632 .await
633 .unwrap();
634 let (system, updates) = loaded.run(agent_rx).await.expect("the initial snapshot is accepted");
635 assert_eq!(system.config().domains.dogstatsd.listeners.port, 8);
636 assert_eq!(system.config().domains.dogstatsd.listeners.buffer_size, 0);
637
638 tokio::spawn(updates.run());
640 agent_tx
641 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
642 "dogstatsd_port",
643 json!("0x10"),
644 )))
645 .await
646 .unwrap();
647 await_dogstatsd_port(&system, 16).await;
648 agent_tx
649 .send(ConfigUpdate::Partial(ConfigSetting::explicit(
650 "dogstatsd_port",
651 json!("-08"),
652 )))
653 .await
654 .unwrap();
655 await_dogstatsd_port(&system, 0).await;
656 });
657 }
658
659 #[test]
660 fn a_malformed_integer_outside_the_invalid_octal_family_still_stops_startup() {
661 let result = load_with_environment("invalid_octal_boundary", "{}\n", &[("DD_DOGSTATSD_PORT", "08.")]);
662 match result {
663 Err(Error::Base { message }) => assert_eq!(
664 message,
665 "environment variable `DD_DOGSTATSD_PORT` (dogstatsd_port): invalid integer `08.`"
666 ),
667 Err(error) => panic!("expected a base error, got {error}"),
668 Ok(_) => panic!("`08.` should be rejected"),
669 }
670 }
671
672 #[test]
673 fn a_saluki_only_integer_keeps_its_decimal_grammar() {
674 let loaded = load_with_environment("saluki_integer", "{}\n", &[("DD_DOGSTATSD_TCP_PORT", "010")])
676 .expect("a zero-padded decimal loads");
677 assert_eq!(loaded.local().domains.dogstatsd.listeners.tcp_port, 10);
678
679 let loaded = load_with_environment("saluki_invalid_octal", "{}\n", &[("DD_DOGSTATSD_TCP_PORT", "08")])
680 .expect("a zero-padded decimal loads");
681 assert_eq!(loaded.local().domains.dogstatsd.listeners.tcp_port, 8);
682
683 let result = load_with_environment("saluki_hex", "{}\n", &[("DD_DOGSTATSD_TCP_PORT", "0x10")]);
684 match result {
685 Err(Error::Base { message }) => assert_eq!(
686 message,
687 "environment variable `DD_DOGSTATSD_TCP_PORT` (dogstatsd_tcp_port): invalid integer `0x10`"
688 ),
689 Err(error) => panic!("expected a base error, got {error}"),
690 Ok(_) => panic!("a hexadecimal Saluki-only integer should be rejected"),
691 }
692 }
693
694 #[test]
695 fn a_saluki_only_float_rejects_go_only_spellings() {
696 let result = load_with_environment("saluki_float", "{}\n", &[("DD_MEMORY_SLOP_FACTOR", "0x1p-2")]);
698 match result {
699 Err(Error::Base { message }) => assert_eq!(
700 message,
701 "environment variable `DD_MEMORY_SLOP_FACTOR` (memory_slop_factor): invalid number `0x1p-2`"
702 ),
703 Err(error) => panic!("expected a base error, got {error}"),
704 Ok(_) => panic!("a hexadecimal Saluki-only float should be rejected"),
705 }
706 }
707
708 #[test]
709 fn a_saluki_only_sequence_still_splits_on_whitespace() {
710 let loaded = load_with_environment(
711 "saluki_sequence",
712 "{}\n",
713 &[("DD_OTTL_FILTER_CONFIG_TRACES_SPAN", r#"["a","b"] c"#)],
714 )
715 .expect("a whitespace-separated sequence loads");
716 assert_eq!(
717 loaded.local().domains.traces.ottl_filter.span_conditions,
718 [r#"["a","b"]"#, "c"]
719 );
720 }
721
722 #[test]
723 fn build_base_rejects_a_malformed_environment_value() {
724 let _guard = test_env_lock();
725 let path = std::env::temp_dir().join(format!("adp_build_base_bad_{}.yaml", std::process::id()));
726 std::fs::write(&path, "dogstatsd_port: 8125\n").unwrap();
727 std::env::set_var("DD_DOGSTATSD_PORT", "not-a-number");
728
729 let result = build_base(&path, EnvPrecedence::AfterFile);
730
731 std::env::remove_var("DD_DOGSTATSD_PORT");
732 std::fs::remove_file(&path).ok();
733 assert!(matches!(result, Err(Error::Base { .. })));
734 }
735
736 #[test]
737 fn build_base_accepts_a_human_readable_dogstatsd_interner_size() {
738 let _guard = test_env_lock();
739 let path = std::env::temp_dir().join(format!("adp_build_base_interner_{}.yaml", std::process::id()));
740 std::fs::write(&path, "{}\n").unwrap();
741 std::env::set_var("DD_DOGSTATSD_STRING_INTERNER_SIZE_BYTES", "12MiB");
742
743 let base = SourceTree::all_explicit(build_base(&path, EnvPrecedence::AfterFile).expect("base builds"));
744 let config = translate_strict(&base).expect("human-readable byte size translates");
745
746 std::env::remove_var("DD_DOGSTATSD_STRING_INTERNER_SIZE_BYTES");
747 std::fs::remove_file(&path).ok();
748 assert_eq!(
749 config.domains.dogstatsd.contexts.string_interner_size_bytes,
750 Some(ByteSize::mib(12).as_u64())
751 );
752 }
753}