1use std::collections::HashMap;
14
15use serde_json::{Map, Value};
16
17use crate::env_decode::{decode, EnvDecode};
18use crate::generated::env_keys::DATADOG_ENV_KEYS;
19
20#[derive(Clone, Copy, Debug)]
22pub struct EnvKey {
23 pub env_vars: &'static [&'static str],
27 pub path: &'static [&'static str],
29 pub decode: EnvDecode,
31}
32
33pub fn apply_datadog_env(base: &mut Value, overwrite: bool) -> Result<(), String> {
44 apply_env_snapshot(base, EnvSnapshot::capture(), overwrite)
45}
46
47pub fn apply_datadog_env_vars(
57 base: &mut Value, vars: impl IntoIterator<Item = (String, String)>, overwrite: bool,
58) -> Result<(), String> {
59 apply_env_snapshot(base, EnvSnapshot::from_vars(vars), overwrite)
60}
61
62fn apply_env_snapshot(base: &mut Value, env: EnvSnapshot, overwrite: bool) -> Result<(), String> {
63 let Some(root) = base.as_object_mut() else {
64 return Ok(());
65 };
66 for key in DATADOG_ENV_KEYS {
67 apply_one(root, &env, key.env_vars, key.path, key.decode, overwrite)?;
68 }
69 apply_proxy_env(root, &env, overwrite);
70 Ok(())
71}
72
73pub fn datadog_leaf_paths() -> impl Iterator<Item = &'static [&'static str]> {
79 DATADOG_ENV_KEYS.iter().map(|key| key.path)
80}
81
82pub fn apply_env_at_path(
91 base: &mut Value, env_vars: &[&str], path: &[&str], decode: EnvDecode, overwrite: bool,
92) -> Result<(), String> {
93 let Some(root) = base.as_object_mut() else {
94 return Ok(());
95 };
96 let env = EnvSnapshot::capture();
97 apply_one(root, &env, env_vars, path, decode, overwrite)
98}
99
100fn apply_proxy_env(root: &mut Map<String, Value>, env: &EnvSnapshot, overwrite: bool) {
111 const PROXY: &[(&str, &str, &[&str])] = &[
112 ("DD_PROXY_HTTP", "HTTP_PROXY", &["proxy", "http"]),
113 ("DD_PROXY_HTTPS", "HTTPS_PROXY", &["proxy", "https"]),
114 ];
115 for (dd_var, canonical_var, path) in PROXY {
116 if env.contains(dd_var) {
119 continue;
120 }
121 if let Some((_, raw)) = env.lookup(&[canonical_var]) {
122 write_at_path(root, path, Value::String(raw.to_owned()), overwrite);
123 }
124 }
125}
126
127fn apply_one(
129 root: &mut Map<String, Value>, env: &EnvSnapshot, env_vars: &[&str], path: &[&str], how: EnvDecode, overwrite: bool,
130) -> Result<(), String> {
131 let Some((name, raw)) = env.lookup(env_vars) else {
132 return Ok(());
133 };
134 let value = decode(raw, how).map_err(|msg| format!("environment variable `{name}` ({}): {msg}", path.join(".")))?;
135 write_at_path(root, path, value, overwrite);
136 Ok(())
137}
138
139struct EnvSnapshot {
146 exact: HashMap<String, String>,
148 folded: HashMap<String, (String, String)>,
150}
151
152impl EnvSnapshot {
153 fn capture() -> Self {
155 Self::from_vars(std::env::vars())
156 }
157
158 fn from_vars(vars: impl IntoIterator<Item = (String, String)>) -> Self {
163 let mut exact = HashMap::new();
164 let mut folded = HashMap::new();
165 for (name, value) in vars {
166 if value.is_empty() {
167 continue;
168 }
169 folded
170 .entry(name.to_lowercase())
171 .or_insert_with(|| (name.clone(), value.clone()));
172 exact.entry(name).or_insert(value);
173 }
174 Self { exact, folded }
175 }
176
177 fn lookup(&self, names: &[&str]) -> Option<(&str, &str)> {
183 names
184 .iter()
185 .find_map(|name| self.exact.get_key_value(*name))
186 .map(|(name, value)| (name.as_str(), value.as_str()))
187 .or_else(|| {
188 names
189 .iter()
190 .find_map(|name| self.folded.get(&name.to_lowercase()))
191 .map(|(name, value)| (name.as_str(), value.as_str()))
192 })
193 }
194
195 fn contains(&self, name: &str) -> bool {
197 self.folded.contains_key(&name.to_lowercase())
198 }
199}
200
201fn write_at_path(root: &mut Map<String, Value>, path: &[&str], value: Value, overwrite: bool) {
204 let Some((leaf, sections)) = path.split_last() else {
205 return;
206 };
207 if !overwrite && path_present(root, path) {
208 return;
209 }
210 let mut current = root;
211 for segment in sections {
212 let node = current
213 .entry((*segment).to_string())
214 .or_insert_with(|| Value::Object(Map::new()));
215 if !node.is_object() {
216 *node = Value::Object(Map::new());
217 }
218 current = node.as_object_mut().expect("node was just ensured to be an object");
219 }
220 current.insert((*leaf).to_string(), value);
221}
222
223fn path_present(root: &Map<String, Value>, path: &[&str]) -> bool {
225 let Some((first, rest)) = path.split_first() else {
226 return false;
227 };
228 let mut current = match root.get(*first) {
229 Some(v) => v,
230 None => return false,
231 };
232 for segment in rest {
233 match current.get(*segment) {
234 Some(v) => current = v,
235 None => return false,
236 }
237 }
238 true
239}
240
241#[cfg(test)]
242mod tests {
243 use serde_json::json;
244
245 use super::*;
246
247 static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
250
251 fn at<'a>(base: &'a Value, path: &[&str]) -> Option<&'a Value> {
252 let mut cur = base;
253 for seg in path {
254 cur = cur.get(seg)?;
255 }
256 Some(cur)
257 }
258
259 #[test]
260 fn decodes_scalars_and_lists_to_shape() {
261 let _guard = ENV_MUTEX.lock().unwrap();
262 std::env::set_var("DD_DOGSTATSD_PORT", "9125");
263 std::env::set_var("DD_API_KEY", "00000");
264 std::env::set_var("DD_DOGSTATSD_TAGS", "env:prod team:core");
265
266 let mut base = json!({});
267 apply_datadog_env(&mut base, true).unwrap();
268
269 std::env::remove_var("DD_DOGSTATSD_PORT");
270 std::env::remove_var("DD_API_KEY");
271 std::env::remove_var("DD_DOGSTATSD_TAGS");
272
273 assert_eq!(at(&base, &["dogstatsd_port"]), Some(&json!(9125)));
276 assert_eq!(at(&base, &["api_key"]), Some(&json!("00000")));
277 assert_eq!(at(&base, &["dogstatsd_tags"]), Some(&json!(["env:prod", "team:core"])));
278 }
279
280 #[test]
281 fn writes_at_nested_path() {
282 let _guard = ENV_MUTEX.lock().unwrap();
283 std::env::set_var("DD_APM_ERROR_TPS", "12.5");
284
285 let mut base = json!({});
286 apply_datadog_env(&mut base, true).unwrap();
287 std::env::remove_var("DD_APM_ERROR_TPS");
288
289 assert_eq!(at(&base, &["apm_config", "errors_per_second"]), Some(&json!(12.5)));
290 }
291
292 #[test]
293 fn precedence_overwrite_versus_fill() {
294 let _guard = ENV_MUTEX.lock().unwrap();
295 std::env::set_var("DD_DOGSTATSD_PORT", "9125");
296
297 let mut env_wins = json!({ "dogstatsd_port": 8125 });
298 apply_datadog_env(&mut env_wins, true).unwrap();
299 assert_eq!(at(&env_wins, &["dogstatsd_port"]), Some(&json!(9125)));
300
301 let mut file_wins = json!({ "dogstatsd_port": 8125 });
302 apply_datadog_env(&mut file_wins, false).unwrap();
303 assert_eq!(at(&file_wins, &["dogstatsd_port"]), Some(&json!(8125)));
304
305 std::env::remove_var("DD_DOGSTATSD_PORT");
306 }
307
308 #[test]
309 fn env_var_names_are_matched_case_insensitively() {
310 let env = EnvSnapshot::from_vars([
311 ("dd_dogstatsd_port".to_string(), "9125".to_string()),
312 ("Dd_Api_Key".to_string(), "00000".to_string()),
313 ]);
314
315 assert_eq!(env.lookup(&["DD_DOGSTATSD_PORT"]), Some(("dd_dogstatsd_port", "9125")));
316 assert_eq!(env.lookup(&["DD_API_KEY"]), Some(("Dd_Api_Key", "00000")));
317 }
318
319 fn vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
320 pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
321 }
322
323 fn apply_vars(pairs: &[(&str, &str)]) -> Result<Value, String> {
324 let mut base = json!({});
325 apply_datadog_env_vars(&mut base, vars(pairs), true)?;
326 Ok(base)
327 }
328
329 #[test]
330 fn the_first_alias_in_declared_order_wins() {
331 for pairs in [
333 [("DD_APM_MAX_EPS", "100"), ("DD_MAX_EPS", "200")],
334 [("DD_MAX_EPS", "200"), ("DD_APM_MAX_EPS", "100")],
335 ] {
336 let base = apply_vars(&pairs).unwrap();
337 assert_eq!(at(&base, &["apm_config", "max_events_per_second"]), Some(&json!(100.0)));
338 }
339
340 let base = apply_vars(&[("DD_MAX_EPS", "200")]).unwrap();
341 assert_eq!(at(&base, &["apm_config", "max_events_per_second"]), Some(&json!(200.0)));
342 }
343
344 #[test]
345 fn an_empty_first_alias_falls_through_to_the_next() {
346 let base = apply_vars(&[("DD_APM_MAX_EPS", ""), ("DD_MAX_EPS", "200")]).unwrap();
347 assert_eq!(at(&base, &["apm_config", "max_events_per_second"]), Some(&json!(200.0)));
348 }
349
350 #[test]
351 fn an_exact_case_name_beats_a_different_case_duplicate() {
352 for pairs in [
354 [("dd_dogstatsd_port", "2"), ("DD_DOGSTATSD_PORT", "1")],
355 [("DD_DOGSTATSD_PORT", "1"), ("dd_dogstatsd_port", "2")],
356 ] {
357 let base = apply_vars(&pairs).unwrap();
358 assert_eq!(at(&base, &["dogstatsd_port"]), Some(&json!(1)));
359 }
360 }
361
362 #[test]
363 fn an_exact_case_later_alias_beats_a_different_case_earlier_alias() {
364 let base = apply_vars(&[("dd_apm_max_eps", "100"), ("DD_MAX_EPS", "200")]).unwrap();
365 assert_eq!(at(&base, &["apm_config", "max_events_per_second"]), Some(&json!(200.0)));
366 }
367
368 #[test]
369 fn the_decode_error_names_the_variable_that_was_read() {
370 let error = apply_vars(&[("DD_MAX_EPS", "fast")]).unwrap_err();
371 assert!(error.starts_with("environment variable `DD_MAX_EPS` (apm_config.max_events_per_second): "));
372
373 let error = apply_vars(&[("dd_max_eps", "fast")]).unwrap_err();
375 assert!(error.starts_with("environment variable `dd_max_eps` (apm_config.max_events_per_second): "));
376 }
377
378 #[test]
379 fn agent_integer_and_list_spellings_decode_to_shape() {
380 let base = apply_vars(&[
381 ("DD_DOGSTATSD_PORT", "0x10"),
382 ("DD_APM_OBFUSCATION_ELASTICSEARCH_KEEP_VALUES", r#"["cr-a","cr-b"]"#),
383 ])
384 .unwrap();
385 assert_eq!(at(&base, &["dogstatsd_port"]), Some(&json!(16)));
386 assert_eq!(
387 at(&base, &["apm_config", "obfuscation", "elasticsearch", "keep_values"]),
388 Some(&json!(["cr-a", "cr-b"]))
389 );
390 }
391
392 #[test]
393 fn canonical_proxy_is_a_fallback_below_dd_form() {
394 let _guard = ENV_MUTEX.lock().unwrap();
395
396 std::env::set_var("HTTP_PROXY", "http://canonical:3128");
398 let mut base = json!({});
399 apply_datadog_env(&mut base, true).unwrap();
400 assert_eq!(at(&base, &["proxy", "http"]), Some(&json!("http://canonical:3128")));
401
402 std::env::set_var("DD_PROXY_HTTP", "http://prefixed:3128");
404 let mut base = json!({});
405 apply_datadog_env(&mut base, true).unwrap();
406 assert_eq!(at(&base, &["proxy", "http"]), Some(&json!("http://prefixed:3128")));
407
408 std::env::remove_var("HTTP_PROXY");
409 std::env::remove_var("DD_PROXY_HTTP");
410 }
411
412 #[test]
413 fn lowercase_proxy_env_is_honored() {
414 let env = EnvSnapshot::from_vars([("http_proxy".to_string(), "http://lower:3128".to_string())]);
415 let mut base = json!({});
416 apply_proxy_env(base.as_object_mut().unwrap(), &env, true);
417
418 assert_eq!(at(&base, &["proxy", "http"]), Some(&json!("http://lower:3128")));
419 }
420
421 #[test]
422 fn malformed_value_propagates() {
423 let _guard = ENV_MUTEX.lock().unwrap();
424 std::env::set_var("DD_DOGSTATSD_PORT", "not-a-number");
425 let mut base = json!({});
426 let result = apply_datadog_env(&mut base, true);
427 std::env::remove_var("DD_DOGSTATSD_PORT");
428 assert!(result.is_err());
429 }
430}