datadog_agent_config/
env_provider.rs

1//! A Figment provider that reads the Datadog schema's environment variables into their canonical
2//! shape.
3//!
4//! Figment's own environment provider can only split a variable name on a fixed separator, but the
5//! Datadog Agent does not name its variables that way: it looks up each known key's declared
6//! variable names, so `DD_PROXY_HTTP` reaches `proxy.http` while `DD_DOGSTATSD_PORT` reaches the
7//! flat `dogstatsd_port`. Nothing about either name says where a nesting boundary falls.
8//!
9//! [`DatadogEnvProvider`] resolves that by reading the environment through
10//! [`apply_datadog_env`](crate::apply_datadog_env), the same schema-driven reader the typed
11//! configuration path uses, so every value lands at the path its key declares.
12
13use figment::providers::Serialized;
14use figment::value::{Dict, Map};
15use figment::{Error, Metadata, Profile, Provider};
16use serde_json::Value;
17
18use crate::env_reader::{apply_datadog_env, apply_datadog_env_vars};
19
20/// A Figment provider carrying every Datadog schema key set in the environment, at its canonical
21/// path.
22///
23/// Values are snapshotted at construction time.
24pub struct DatadogEnvProvider {
25    values: Value,
26}
27
28impl DatadogEnvProvider {
29    /// Reads the process environment for every modeled Datadog key.
30    ///
31    /// Only keys whose environment variables are set to a non-empty value appear in the provider; it
32    /// contributes no defaults, so it never masks a lower-precedence source.
33    ///
34    /// # Errors
35    ///
36    /// Returns a message naming the environment variable when its value is malformed for the shape
37    /// its key declares.
38    pub fn new() -> Result<Self, String> {
39        Self::build(|values| apply_datadog_env(values, true))
40    }
41
42    /// Reads explicitly provided environment variable name/value pairs instead of the process
43    /// environment.
44    ///
45    /// # Errors
46    ///
47    /// Returns a message naming the environment variable when its value is malformed for the shape
48    /// its key declares.
49    pub fn from_env_vars(vars: Vec<(String, String)>) -> Result<Self, String> {
50        Self::build(|values| apply_datadog_env_vars(values, vars, true))
51    }
52
53    fn build(read: impl FnOnce(&mut Value) -> Result<(), String>) -> Result<Self, String> {
54        // The base starts empty, so nothing can be overwritten and the readers' `overwrite` flag is
55        // immaterial.
56        let mut values = Value::Object(serde_json::Map::new());
57        read(&mut values)?;
58        Ok(Self { values })
59    }
60}
61
62impl Provider for DatadogEnvProvider {
63    fn metadata(&self) -> Metadata {
64        Metadata::named("Datadog schema environment variables")
65    }
66
67    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
68        Serialized::defaults(&self.values).data()
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn nested_key_lands_at_its_canonical_path() {
78        let provider = DatadogEnvProvider::from_env_vars(vec![(
79            "DD_PROXY_HTTP".to_string(),
80            "http://proxy.example.com".to_string(),
81        )])
82        .expect("environment reads");
83
84        assert_eq!(
85            provider.values.pointer("/proxy/http"),
86            Some(&Value::String("http://proxy.example.com".to_string()))
87        );
88    }
89
90    #[test]
91    fn canonical_proxy_variable_is_honored() {
92        // `HTTP_PROXY` carries no `DD_` prefix, so a prefix-scanning provider cannot see it at all.
93        let provider = DatadogEnvProvider::from_env_vars(vec![(
94            "HTTP_PROXY".to_string(),
95            "http://canonical.example.com".to_string(),
96        )])
97        .expect("environment reads");
98
99        assert_eq!(
100            provider.values.pointer("/proxy/http"),
101            Some(&Value::String("http://canonical.example.com".to_string()))
102        );
103    }
104
105    #[test]
106    fn a_value_is_decoded_into_the_shape_its_key_declares() {
107        let provider = DatadogEnvProvider::from_env_vars(vec![("DD_DOGSTATSD_PORT".to_string(), "9125".to_string())])
108            .expect("environment reads");
109
110        assert_eq!(provider.values.pointer("/dogstatsd_port"), Some(&Value::from(9125)));
111    }
112
113    #[test]
114    fn unset_keys_contribute_nothing() {
115        let provider = DatadogEnvProvider::from_env_vars(Vec::new()).expect("environment reads");
116        assert_eq!(provider.values, serde_json::json!({}));
117    }
118
119    #[test]
120    fn a_malformed_value_is_rejected() {
121        let result =
122            DatadogEnvProvider::from_env_vars(vec![("DD_DOGSTATSD_PORT".to_string(), "not-a-number".to_string())]);
123        assert!(result.is_err());
124    }
125}