Skip to main content

saluki_components/
config.rs

1//! Datadog-specific configuration providers and remappers.
2use figment::{
3    providers::Serialized,
4    value::{Dict, Map},
5    Error, Metadata, Profile, Provider,
6};
7
8/// Key aliases to pass to [`ConfigurationLoader::with_key_aliases`][saluki_config::ConfigurationLoader::with_key_aliases].
9///
10/// Each entry maps a nested dot-separated path to a flat key name. When the nested path is found in a loaded
11/// config file, its value is also emitted under the flat key — but only if the flat key is not already
12/// explicitly set. This ensures both YAML nested format and flat env var format produce the same Figment key,
13/// so source precedence (env vars > file) works correctly.
14pub const KEY_ALIASES: &[(&str, &str)] = &[
15    // The Datadog Agent config file uses `proxy: http:` and `proxy: https:` (nested), while env
16    // vars produce `proxy_http` and `proxy_https` (flat). Figment treats these as different keys,
17    // so without this alias env var precedence over YAML is silently broken for proxy config.
18    ("proxy.http", "proxy_http"),
19    ("proxy.https", "proxy_https"),
20    ("proxy.no_proxy", "proxy_no_proxy"),
21    ("apm_config.enable_rare_sampler", "apm_enable_rare_sampler"),
22    (
23        "apm_config.error_tracking_standalone.enabled",
24        "apm_error_tracking_standalone_enabled",
25    ),
26];
27
28/// Remappings from environment variable names to canonical config keys.
29///
30/// Matching is case-insensitive.
31const ENV_REMAPPINGS: &[(&str, &str)] = &[("http_proxy", "proxy_http"), ("https_proxy", "proxy_https")];
32
33/// A Figment provider that remaps canonical environment variable names to our desired config keys.
34///
35/// Reads environment variables case-insensitively and maps them to config keys (e.g. `HTTP_PROXY` →
36/// `proxy_http`). Values are snapshotted at construction time.
37///
38/// Add this provider to a [`ConfigurationLoader`][saluki_config::ConfigurationLoader] *after* file-based
39/// providers and *before* vendor-prefixed env providers (e.g. `DD_`) to achieve the correct precedence:
40/// file < remapped env vars < `DD_`-prefixed.
41///
42/// For YAML key aliasing (e.g. `proxy.http` → `proxy_http`), pass [`KEY_ALIASES`] to
43/// [`ConfigurationLoader::with_key_aliases`][saluki_config::ConfigurationLoader::with_key_aliases] instead —
44/// that is handled at file-load time.
45pub struct DatadogRemapper {
46    values: serde_json::Map<String, serde_json::Value>,
47}
48
49impl DatadogRemapper {
50    /// Constructs a `DatadogRemapper` by eagerly snapshotting env var remappings.
51    pub fn new() -> Self {
52        let mut values = serde_json::Map::new();
53
54        for (env_key, env_value) in std::env::vars() {
55            let lower = env_key.to_lowercase();
56            for &(from, to) in ENV_REMAPPINGS {
57                if lower == from && !values.contains_key(to) {
58                    values.insert(to.to_string(), serde_json::Value::String(env_value.clone()));
59                }
60            }
61        }
62
63        Self { values }
64    }
65}
66
67impl Default for DatadogRemapper {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl Provider for DatadogRemapper {
74    fn metadata(&self) -> Metadata {
75        Metadata::named("Datadog config remapper")
76    }
77
78    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
79        if self.values.is_empty() {
80            return Ok(Map::new());
81        }
82        Serialized::defaults(serde_json::Value::Object(self.values.clone())).data()
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
91
92    #[test]
93    fn env_var_remapped_case_insensitively() {
94        let _guard = ENV_MUTEX.lock().unwrap();
95
96        std::env::set_var("HTTP_PROXY", "http://proxy.example.com");
97        let remapper = DatadogRemapper::new();
98        std::env::remove_var("HTTP_PROXY");
99
100        assert_eq!(
101            remapper.values.get("proxy_http").and_then(|v| v.as_str()),
102            Some("http://proxy.example.com"),
103        );
104    }
105
106    #[test]
107    fn env_var_not_remapped_when_absent() {
108        let _guard = ENV_MUTEX.lock().unwrap();
109
110        std::env::remove_var("HTTP_PROXY");
111        std::env::remove_var("http_proxy");
112        std::env::remove_var("HTTPS_PROXY");
113        std::env::remove_var("https_proxy");
114
115        let remapper = DatadogRemapper::new();
116
117        assert!(remapper.values.get("proxy_http").is_none());
118        assert!(remapper.values.get("proxy_https").is_none());
119    }
120}