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],
26 pub path: &'static [&'static str],
28 pub decode: EnvDecode,
30}
31
32pub fn apply_datadog_env(base: &mut Value, overwrite: bool) -> Result<(), String> {
43 let Some(root) = base.as_object_mut() else {
44 return Ok(());
45 };
46 let env = EnvSnapshot::capture();
47 for key in DATADOG_ENV_KEYS {
48 apply_one(root, &env, key.env_vars, key.path, key.decode, overwrite)?;
49 }
50 apply_proxy_env(root, &env, overwrite);
51 Ok(())
52}
53
54pub fn datadog_leaf_paths() -> impl Iterator<Item = &'static [&'static str]> {
60 DATADOG_ENV_KEYS.iter().map(|key| key.path)
61}
62
63pub fn apply_env_at_path(
72 base: &mut Value, env_vars: &[&str], path: &[&str], decode: EnvDecode, overwrite: bool,
73) -> Result<(), String> {
74 let Some(root) = base.as_object_mut() else {
75 return Ok(());
76 };
77 let env = EnvSnapshot::capture();
78 apply_one(root, &env, env_vars, path, decode, overwrite)
79}
80
81fn apply_proxy_env(root: &mut Map<String, Value>, env: &EnvSnapshot, overwrite: bool) {
92 const PROXY: &[(&str, &str, &[&str])] = &[
93 ("DD_PROXY_HTTP", "HTTP_PROXY", &["proxy", "http"]),
94 ("DD_PROXY_HTTPS", "HTTPS_PROXY", &["proxy", "https"]),
95 ];
96 for (dd_var, canonical_var, path) in PROXY {
97 if env.contains(dd_var) {
100 continue;
101 }
102 if let Some(raw) = env.lookup(&[canonical_var]) {
103 write_at_path(root, path, Value::String(raw), overwrite);
104 }
105 }
106}
107
108fn apply_one(
110 root: &mut Map<String, Value>, env: &EnvSnapshot, env_vars: &[&str], path: &[&str], how: EnvDecode, overwrite: bool,
111) -> Result<(), String> {
112 let Some(raw) = env.lookup(env_vars) else {
113 return Ok(());
114 };
115 let value = decode(&raw, how).map_err(|msg| {
116 let name = env_vars.first().copied().unwrap_or_default();
117 format!("environment variable `{name}` ({}): {msg}", path.join("."))
118 })?;
119 write_at_path(root, path, value, overwrite);
120 Ok(())
121}
122
123struct EnvSnapshot {
131 vars: HashMap<String, String>,
133}
134
135impl EnvSnapshot {
136 fn capture() -> Self {
138 Self::from_vars(std::env::vars())
139 }
140
141 fn from_vars(vars: impl IntoIterator<Item = (String, String)>) -> Self {
147 let mut snapshot = HashMap::new();
148 for (name, value) in vars {
149 if value.is_empty() {
150 continue;
151 }
152 snapshot.entry(name.to_lowercase()).or_insert(value);
153 }
154 Self { vars: snapshot }
155 }
156
157 fn lookup(&self, names: &[&str]) -> Option<String> {
160 names
161 .iter()
162 .find_map(|name| self.vars.get(&name.to_lowercase()).cloned())
163 }
164
165 fn contains(&self, name: &str) -> bool {
167 self.vars.contains_key(&name.to_lowercase())
168 }
169}
170
171fn write_at_path(root: &mut Map<String, Value>, path: &[&str], value: Value, overwrite: bool) {
174 let Some((leaf, sections)) = path.split_last() else {
175 return;
176 };
177 if !overwrite && path_present(root, path) {
178 return;
179 }
180 let mut current = root;
181 for segment in sections {
182 let node = current
183 .entry((*segment).to_string())
184 .or_insert_with(|| Value::Object(Map::new()));
185 if !node.is_object() {
186 *node = Value::Object(Map::new());
187 }
188 current = node.as_object_mut().expect("node was just ensured to be an object");
189 }
190 current.insert((*leaf).to_string(), value);
191}
192
193fn path_present(root: &Map<String, Value>, path: &[&str]) -> bool {
195 let Some((first, rest)) = path.split_first() else {
196 return false;
197 };
198 let mut current = match root.get(*first) {
199 Some(v) => v,
200 None => return false,
201 };
202 for segment in rest {
203 match current.get(*segment) {
204 Some(v) => current = v,
205 None => return false,
206 }
207 }
208 true
209}
210
211#[cfg(test)]
212mod tests {
213 use serde_json::json;
214
215 use super::*;
216
217 static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
220
221 fn at<'a>(base: &'a Value, path: &[&str]) -> Option<&'a Value> {
222 let mut cur = base;
223 for seg in path {
224 cur = cur.get(seg)?;
225 }
226 Some(cur)
227 }
228
229 #[test]
230 fn decodes_scalars_and_lists_to_shape() {
231 let _guard = ENV_MUTEX.lock().unwrap();
232 std::env::set_var("DD_DOGSTATSD_PORT", "9125");
233 std::env::set_var("DD_API_KEY", "00000");
234 std::env::set_var("DD_DOGSTATSD_TAGS", "env:prod team:core");
235
236 let mut base = json!({});
237 apply_datadog_env(&mut base, true).unwrap();
238
239 std::env::remove_var("DD_DOGSTATSD_PORT");
240 std::env::remove_var("DD_API_KEY");
241 std::env::remove_var("DD_DOGSTATSD_TAGS");
242
243 assert_eq!(at(&base, &["dogstatsd_port"]), Some(&json!(9125)));
246 assert_eq!(at(&base, &["api_key"]), Some(&json!("00000")));
247 assert_eq!(at(&base, &["dogstatsd_tags"]), Some(&json!(["env:prod", "team:core"])));
248 }
249
250 #[test]
251 fn writes_at_nested_path() {
252 let _guard = ENV_MUTEX.lock().unwrap();
253 std::env::set_var("DD_APM_ERROR_TPS", "12.5");
254
255 let mut base = json!({});
256 apply_datadog_env(&mut base, true).unwrap();
257 std::env::remove_var("DD_APM_ERROR_TPS");
258
259 assert_eq!(at(&base, &["apm_config", "errors_per_second"]), Some(&json!(12.5)));
260 }
261
262 #[test]
263 fn precedence_overwrite_versus_fill() {
264 let _guard = ENV_MUTEX.lock().unwrap();
265 std::env::set_var("DD_DOGSTATSD_PORT", "9125");
266
267 let mut env_wins = json!({ "dogstatsd_port": 8125 });
268 apply_datadog_env(&mut env_wins, true).unwrap();
269 assert_eq!(at(&env_wins, &["dogstatsd_port"]), Some(&json!(9125)));
270
271 let mut file_wins = json!({ "dogstatsd_port": 8125 });
272 apply_datadog_env(&mut file_wins, false).unwrap();
273 assert_eq!(at(&file_wins, &["dogstatsd_port"]), Some(&json!(8125)));
274
275 std::env::remove_var("DD_DOGSTATSD_PORT");
276 }
277
278 #[test]
279 fn env_var_names_are_matched_case_insensitively() {
280 let env = EnvSnapshot::from_vars([
281 ("dd_dogstatsd_port".to_string(), "9125".to_string()),
282 ("Dd_Api_Key".to_string(), "00000".to_string()),
283 ]);
284
285 assert_eq!(env.lookup(&["DD_DOGSTATSD_PORT"]).as_deref(), Some("9125"));
286 assert_eq!(env.lookup(&["DD_API_KEY"]).as_deref(), Some("00000"));
287 }
288
289 #[test]
290 fn canonical_proxy_is_a_fallback_below_dd_form() {
291 let _guard = ENV_MUTEX.lock().unwrap();
292
293 std::env::set_var("HTTP_PROXY", "http://canonical:3128");
295 let mut base = json!({});
296 apply_datadog_env(&mut base, true).unwrap();
297 assert_eq!(at(&base, &["proxy", "http"]), Some(&json!("http://canonical:3128")));
298
299 std::env::set_var("DD_PROXY_HTTP", "http://prefixed:3128");
301 let mut base = json!({});
302 apply_datadog_env(&mut base, true).unwrap();
303 assert_eq!(at(&base, &["proxy", "http"]), Some(&json!("http://prefixed:3128")));
304
305 std::env::remove_var("HTTP_PROXY");
306 std::env::remove_var("DD_PROXY_HTTP");
307 }
308
309 #[test]
310 fn lowercase_proxy_env_is_honored() {
311 let env = EnvSnapshot::from_vars([("http_proxy".to_string(), "http://lower:3128".to_string())]);
312 let mut base = json!({});
313 apply_proxy_env(base.as_object_mut().unwrap(), &env, true);
314
315 assert_eq!(at(&base, &["proxy", "http"]), Some(&json!("http://lower:3128")));
316 }
317
318 #[test]
319 fn malformed_value_propagates() {
320 let _guard = ENV_MUTEX.lock().unwrap();
321 std::env::set_var("DD_DOGSTATSD_PORT", "not-a-number");
322 let mut base = json!({});
323 let result = apply_datadog_env(&mut base, true);
324 std::env::remove_var("DD_DOGSTATSD_PORT");
325 assert!(result.is_err());
326 }
327}