datadog_agent_config/
env_reader.rs

1//! Builds the typed configuration base from environment variables.
2//!
3//! The Datadog Agent reads configuration by iterating its table of known keys
4//! and, for each, looking up that key's exact environment variable names, never by scanning the
5//! environment or splitting on a separator. This module mirrors that: [`DATADOG_ENV_KEYS`] is
6//! generated from the vendored schema, and [`apply_datadog_env`] looks up each key's real names,
7//! decodes the first non-empty value into the JSON shape the schema declares (see
8//! [`crate::env_decode`]), and writes it at the key's nested path.
9//!
10//! Each value is decoded and written directly to the nested path the typed deserializer reads.
11//! Precedence relative to the config file is controlled by the caller through `overwrite`.
12
13use 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/// One modeled configuration key and how to read it from the environment.
21#[derive(Clone, Copy, Debug)]
22pub struct EnvKey {
23    /// The environment variable names bound to this key, highest priority first. The first one
24    /// set to a non-empty value supplies the key's value, matching the Agent's per-key lookup.
25    pub env_vars: &'static [&'static str],
26    /// The nested path the typed deserializer reads (`["dogstatsd", "port"]`).
27    pub path: &'static [&'static str],
28    /// How to decode the raw string into the schema-declared JSON shape.
29    pub decode: EnvDecode,
30}
31
32/// Reads every modeled Datadog key from the environment and writes decoded values into `base`.
33///
34/// Applies the canonical proxy variables (`HTTP_PROXY`/`HTTPS_PROXY`) as well. When `overwrite` is
35/// true an environment value replaces whatever is already at its path (environment wins over the
36/// file); when false it fills only an absent path (the file wins).
37///
38/// # Errors
39///
40/// Returns a message naming the environment variable when its value is malformed for the key's
41/// declared shape. At startup this aborts the boot rather than silently dropping the value.
42pub 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
54/// The nested path of every modeled Datadog key.
55///
56/// The merge that layers the Agent config stream over the base uses these to tell a schema section
57/// (an intermediate object it descends into) from a leaf (a value it replaces wholesale), so a
58/// map-typed leaf is never key-unioned across sources.
59pub fn datadog_leaf_paths() -> impl Iterator<Item = &'static [&'static str]> {
60    DATADOG_ENV_KEYS.iter().map(|key| key.path)
61}
62
63/// Reads one key from the environment and writes it into `root`.
64///
65/// The public entry for callers with a runtime-built key set (the Saluki-only model), which cannot
66/// use the `'static` [`EnvKey`] table. Same semantics as [`apply_datadog_env`] for a single key.
67///
68/// # Errors
69///
70/// Returns a message when the environment value is malformed for `decode`.
71pub 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
81/// The canonical proxy variables that carry no `DD_` prefix.
82///
83/// `HTTP_PROXY` and `HTTPS_PROXY` are honored by the Datadog Agent but are not declared by the
84/// vendored schema (they are wired only by the Agent's bespoke proxy handling), so they are not in
85/// [`DATADOG_ENV_KEYS`] and are injected here by hand. They are a fallback below the schema's
86/// `DD_PROXY_HTTP`/`DD_PROXY_HTTPS`: when the `DD_`-prefixed form is set, the canonical form is
87/// ignored, matching the Agent's ordering. Canonical `NO_PROXY` is intentionally unsupported.
88///
89/// The canonical names are matched case-insensitively through [`EnvSnapshot`], so the common
90/// lowercase Unix convention (`http_proxy`/`https_proxy`) is honored alongside the uppercase form.
91fn 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        // The DD_-prefixed form is handled by the generated table and wins; only fill from the
98        // canonical form when the DD_ form is unset.
99        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
108/// Reads, decodes, and writes one key.
109fn 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
123/// A case-insensitive snapshot of the process environment.
124///
125/// The legacy figment loader reads environment variables case-insensitively (via `uncased`), and
126/// the compatibility `DatadogRemapper` converts names to lowercase before matching. Because
127/// `std::env::var` is case-sensitive, the typed reader captures the environment with lowercase
128/// names to preserve parity. `DD_DOGSTATSD_PORT`, `dd_dogstatsd_port`, and `Dd_Dogstatsd_Port` all
129/// resolve to the same key.
130struct EnvSnapshot {
131    /// Environment values keyed by lowercase variable name. Only non-empty values are kept.
132    vars: HashMap<String, String>,
133}
134
135impl EnvSnapshot {
136    /// Captures the environment, converting names to lowercase and dropping empty values.
137    fn capture() -> Self {
138        Self::from_vars(std::env::vars())
139    }
140
141    /// Builds a snapshot from environment variable pairs.
142    ///
143    /// An empty string counts as unset, matching the Agent (`os.LookupEnv` plus an emptiness
144    /// check). If several case variants of one name are set, the first seen wins, mirroring the
145    /// remapper.
146    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    /// Returns the value of the first name in `names` (highest priority first) that is set to a
158    /// non-empty value, matched case-insensitively.
159    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    /// Whether any case variant of `name` is set to a non-empty value.
166    fn contains(&self, name: &str) -> bool {
167        self.vars.contains_key(&name.to_lowercase())
168    }
169}
170
171/// Writes `value` at `path`, creating intermediate objects. When `overwrite` is false, an existing
172/// value at the full path is left in place (the file wins).
173fn 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
193/// Returns whether a leaf already exists at `path`, walking object nodes from `root`.
194fn 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    // Process environment is global; serialize the tests that mutate it. Each test removes the
218    // variables it sets so they do not leak into sibling tests.
219    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        // Integer decodes to a JSON number; a numeric-looking API key stays a string (the schema
244        // says `api_key` is a string); a string list splits into a real array.
245        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        // Canonical form alone fills the slot.
294        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        // The DD_ form wins when both are set.
300        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}