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 apply_env_snapshot(base, EnvSnapshot::capture(), overwrite)
44}
45
46pub fn apply_datadog_env_vars(
56 base: &mut Value, vars: impl IntoIterator<Item = (String, String)>, overwrite: bool,
57) -> Result<(), String> {
58 apply_env_snapshot(base, EnvSnapshot::from_vars(vars), overwrite)
59}
60
61fn apply_env_snapshot(base: &mut Value, env: EnvSnapshot, overwrite: bool) -> Result<(), String> {
62 let Some(root) = base.as_object_mut() else {
63 return Ok(());
64 };
65 for key in DATADOG_ENV_KEYS {
66 apply_one(root, &env, key.env_vars, key.path, key.decode, overwrite)?;
67 }
68 apply_proxy_env(root, &env, overwrite);
69 Ok(())
70}
71
72pub fn datadog_leaf_paths() -> impl Iterator<Item = &'static [&'static str]> {
78 DATADOG_ENV_KEYS.iter().map(|key| key.path)
79}
80
81pub fn apply_env_at_path(
90 base: &mut Value, env_vars: &[&str], path: &[&str], decode: EnvDecode, overwrite: bool,
91) -> Result<(), String> {
92 let Some(root) = base.as_object_mut() else {
93 return Ok(());
94 };
95 let env = EnvSnapshot::capture();
96 apply_one(root, &env, env_vars, path, decode, overwrite)
97}
98
99fn apply_proxy_env(root: &mut Map<String, Value>, env: &EnvSnapshot, overwrite: bool) {
110 const PROXY: &[(&str, &str, &[&str])] = &[
111 ("DD_PROXY_HTTP", "HTTP_PROXY", &["proxy", "http"]),
112 ("DD_PROXY_HTTPS", "HTTPS_PROXY", &["proxy", "https"]),
113 ];
114 for (dd_var, canonical_var, path) in PROXY {
115 if env.contains(dd_var) {
118 continue;
119 }
120 if let Some(raw) = env.lookup(&[canonical_var]) {
121 write_at_path(root, path, Value::String(raw), overwrite);
122 }
123 }
124}
125
126fn apply_one(
128 root: &mut Map<String, Value>, env: &EnvSnapshot, env_vars: &[&str], path: &[&str], how: EnvDecode, overwrite: bool,
129) -> Result<(), String> {
130 let Some(raw) = env.lookup(env_vars) else {
131 return Ok(());
132 };
133 let value = decode(&raw, how).map_err(|msg| {
134 let name = env_vars.first().copied().unwrap_or_default();
135 format!("environment variable `{name}` ({}): {msg}", path.join("."))
136 })?;
137 write_at_path(root, path, value, overwrite);
138 Ok(())
139}
140
141struct EnvSnapshot {
148 vars: HashMap<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 snapshot = HashMap::new();
164 for (name, value) in vars {
165 if value.is_empty() {
166 continue;
167 }
168 snapshot.entry(name.to_lowercase()).or_insert(value);
169 }
170 Self { vars: snapshot }
171 }
172
173 fn lookup(&self, names: &[&str]) -> Option<String> {
176 names
177 .iter()
178 .find_map(|name| self.vars.get(&name.to_lowercase()).cloned())
179 }
180
181 fn contains(&self, name: &str) -> bool {
183 self.vars.contains_key(&name.to_lowercase())
184 }
185}
186
187fn write_at_path(root: &mut Map<String, Value>, path: &[&str], value: Value, overwrite: bool) {
190 let Some((leaf, sections)) = path.split_last() else {
191 return;
192 };
193 if !overwrite && path_present(root, path) {
194 return;
195 }
196 let mut current = root;
197 for segment in sections {
198 let node = current
199 .entry((*segment).to_string())
200 .or_insert_with(|| Value::Object(Map::new()));
201 if !node.is_object() {
202 *node = Value::Object(Map::new());
203 }
204 current = node.as_object_mut().expect("node was just ensured to be an object");
205 }
206 current.insert((*leaf).to_string(), value);
207}
208
209fn path_present(root: &Map<String, Value>, path: &[&str]) -> bool {
211 let Some((first, rest)) = path.split_first() else {
212 return false;
213 };
214 let mut current = match root.get(*first) {
215 Some(v) => v,
216 None => return false,
217 };
218 for segment in rest {
219 match current.get(*segment) {
220 Some(v) => current = v,
221 None => return false,
222 }
223 }
224 true
225}
226
227#[cfg(test)]
228mod tests {
229 use serde_json::json;
230
231 use super::*;
232
233 static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
236
237 fn at<'a>(base: &'a Value, path: &[&str]) -> Option<&'a Value> {
238 let mut cur = base;
239 for seg in path {
240 cur = cur.get(seg)?;
241 }
242 Some(cur)
243 }
244
245 #[test]
246 fn decodes_scalars_and_lists_to_shape() {
247 let _guard = ENV_MUTEX.lock().unwrap();
248 std::env::set_var("DD_DOGSTATSD_PORT", "9125");
249 std::env::set_var("DD_API_KEY", "00000");
250 std::env::set_var("DD_DOGSTATSD_TAGS", "env:prod team:core");
251
252 let mut base = json!({});
253 apply_datadog_env(&mut base, true).unwrap();
254
255 std::env::remove_var("DD_DOGSTATSD_PORT");
256 std::env::remove_var("DD_API_KEY");
257 std::env::remove_var("DD_DOGSTATSD_TAGS");
258
259 assert_eq!(at(&base, &["dogstatsd_port"]), Some(&json!(9125)));
262 assert_eq!(at(&base, &["api_key"]), Some(&json!("00000")));
263 assert_eq!(at(&base, &["dogstatsd_tags"]), Some(&json!(["env:prod", "team:core"])));
264 }
265
266 #[test]
267 fn writes_at_nested_path() {
268 let _guard = ENV_MUTEX.lock().unwrap();
269 std::env::set_var("DD_APM_ERROR_TPS", "12.5");
270
271 let mut base = json!({});
272 apply_datadog_env(&mut base, true).unwrap();
273 std::env::remove_var("DD_APM_ERROR_TPS");
274
275 assert_eq!(at(&base, &["apm_config", "errors_per_second"]), Some(&json!(12.5)));
276 }
277
278 #[test]
279 fn precedence_overwrite_versus_fill() {
280 let _guard = ENV_MUTEX.lock().unwrap();
281 std::env::set_var("DD_DOGSTATSD_PORT", "9125");
282
283 let mut env_wins = json!({ "dogstatsd_port": 8125 });
284 apply_datadog_env(&mut env_wins, true).unwrap();
285 assert_eq!(at(&env_wins, &["dogstatsd_port"]), Some(&json!(9125)));
286
287 let mut file_wins = json!({ "dogstatsd_port": 8125 });
288 apply_datadog_env(&mut file_wins, false).unwrap();
289 assert_eq!(at(&file_wins, &["dogstatsd_port"]), Some(&json!(8125)));
290
291 std::env::remove_var("DD_DOGSTATSD_PORT");
292 }
293
294 #[test]
295 fn env_var_names_are_matched_case_insensitively() {
296 let env = EnvSnapshot::from_vars([
297 ("dd_dogstatsd_port".to_string(), "9125".to_string()),
298 ("Dd_Api_Key".to_string(), "00000".to_string()),
299 ]);
300
301 assert_eq!(env.lookup(&["DD_DOGSTATSD_PORT"]).as_deref(), Some("9125"));
302 assert_eq!(env.lookup(&["DD_API_KEY"]).as_deref(), Some("00000"));
303 }
304
305 #[test]
306 fn canonical_proxy_is_a_fallback_below_dd_form() {
307 let _guard = ENV_MUTEX.lock().unwrap();
308
309 std::env::set_var("HTTP_PROXY", "http://canonical:3128");
311 let mut base = json!({});
312 apply_datadog_env(&mut base, true).unwrap();
313 assert_eq!(at(&base, &["proxy", "http"]), Some(&json!("http://canonical:3128")));
314
315 std::env::set_var("DD_PROXY_HTTP", "http://prefixed:3128");
317 let mut base = json!({});
318 apply_datadog_env(&mut base, true).unwrap();
319 assert_eq!(at(&base, &["proxy", "http"]), Some(&json!("http://prefixed:3128")));
320
321 std::env::remove_var("HTTP_PROXY");
322 std::env::remove_var("DD_PROXY_HTTP");
323 }
324
325 #[test]
326 fn lowercase_proxy_env_is_honored() {
327 let env = EnvSnapshot::from_vars([("http_proxy".to_string(), "http://lower:3128".to_string())]);
328 let mut base = json!({});
329 apply_proxy_env(base.as_object_mut().unwrap(), &env, true);
330
331 assert_eq!(at(&base, &["proxy", "http"]), Some(&json!("http://lower:3128")));
332 }
333
334 #[test]
335 fn malformed_value_propagates() {
336 let _guard = ENV_MUTEX.lock().unwrap();
337 std::env::set_var("DD_DOGSTATSD_PORT", "not-a-number");
338 let mut base = json!({});
339 let result = apply_datadog_env(&mut base, true);
340 std::env::remove_var("DD_DOGSTATSD_PORT");
341 assert!(result.is_err());
342 }
343}