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    // Obfuscation keys live at `apm_config.obfuscation.*` in YAML but the Agent's env vars use
27    // `DD_APM_OBFUSCATION_*` (no `_CONFIG_` segment), producing flat keys. These aliases emit the
28    // flat key when the nested YAML path is present so that both sources land on the same Figment
29    // key and env var precedence over file config works correctly.
30    (
31        "apm_config.obfuscation.credit_cards.enabled",
32        "apm_obfuscation_credit_cards_enabled",
33    ),
34    (
35        "apm_config.obfuscation.credit_cards.keep_values",
36        "apm_obfuscation_credit_cards_keep_values",
37    ),
38    (
39        "apm_config.obfuscation.credit_cards.luhn",
40        "apm_obfuscation_credit_cards_luhn",
41    ),
42    (
43        "apm_config.obfuscation.elasticsearch.enabled",
44        "apm_obfuscation_elasticsearch_enabled",
45    ),
46    (
47        "apm_config.obfuscation.elasticsearch.keep_values",
48        "apm_obfuscation_elasticsearch_keep_values",
49    ),
50    (
51        "apm_config.obfuscation.elasticsearch.obfuscate_sql_values",
52        "apm_obfuscation_elasticsearch_obfuscate_sql_values",
53    ),
54    (
55        "apm_config.obfuscation.http.remove_paths_with_digits",
56        "apm_obfuscation_http_remove_paths_with_digits",
57    ),
58    (
59        "apm_config.obfuscation.http.remove_query_string",
60        "apm_obfuscation_http_remove_query_string",
61    ),
62    (
63        "apm_config.obfuscation.memcached.enabled",
64        "apm_obfuscation_memcached_enabled",
65    ),
66    (
67        "apm_config.obfuscation.memcached.keep_command",
68        "apm_obfuscation_memcached_keep_command",
69    ),
70    (
71        "apm_config.obfuscation.mongodb.enabled",
72        "apm_obfuscation_mongodb_enabled",
73    ),
74    (
75        "apm_config.obfuscation.mongodb.keep_values",
76        "apm_obfuscation_mongodb_keep_values",
77    ),
78    (
79        "apm_config.obfuscation.mongodb.obfuscate_sql_values",
80        "apm_obfuscation_mongodb_obfuscate_sql_values",
81    ),
82    (
83        "apm_config.obfuscation.opensearch.enabled",
84        "apm_obfuscation_opensearch_enabled",
85    ),
86    (
87        "apm_config.obfuscation.opensearch.keep_values",
88        "apm_obfuscation_opensearch_keep_values",
89    ),
90    (
91        "apm_config.obfuscation.opensearch.obfuscate_sql_values",
92        "apm_obfuscation_opensearch_obfuscate_sql_values",
93    ),
94    ("apm_config.obfuscation.redis.enabled", "apm_obfuscation_redis_enabled"),
95    (
96        "apm_config.obfuscation.redis.remove_all_args",
97        "apm_obfuscation_redis_remove_all_args",
98    ),
99    (
100        "apm_config.obfuscation.valkey.enabled",
101        "apm_obfuscation_valkey_enabled",
102    ),
103    (
104        "apm_config.obfuscation.valkey.remove_all_args",
105        "apm_obfuscation_valkey_remove_all_args",
106    ),
107    ("apm_config.obfuscation.sql.dbms", "apm_obfuscation_sql_dbms"),
108    (
109        "apm_config.obfuscation.sql.dollar_quoted_func",
110        "apm_obfuscation_sql_dollar_quoted_func",
111    ),
112    (
113        "apm_config.obfuscation.sql.keep_sql_alias",
114        "apm_obfuscation_sql_keep_sql_alias",
115    ),
116    (
117        "apm_config.obfuscation.sql.replace_digits",
118        "apm_obfuscation_sql_replace_digits",
119    ),
120    (
121        "apm_config.obfuscation.sql.table_names",
122        "apm_obfuscation_sql_table_names",
123    ),
124    // `otlp_config.traces.probabilistic_sampler.sampling_percentage` lives at a deeply nested YAML
125    // path but the Agent's env var uses `DD_OTLP_CONFIG_TRACES_PROBABILISTIC_SAMPLER_SAMPLING_PERCENTAGE`,
126    // which strips to a flat key. This alias bridges the two so env var precedence over file config
127    // works correctly.
128    (
129        "otlp_config.traces.probabilistic_sampler.sampling_percentage",
130        "otlp_config_traces_probabilistic_sampler_sampling_percentage",
131    ),
132    // Agent IPC relates to some of the Agent's IPC configuration options.
133    //
134    // We don't use them in this crate, but we still depend on them for stuff like the environment provider, and this is
135    // the only set of key aliases we use, so I'm adding it here _for now_ until we have a better way to unify these
136    // sorts of things.
137    ("agent_ipc.grpc_max_message_size", "agent_ipc_grpc_max_message_size"),
138];
139
140/// Remappings from environment variable names to canonical config keys.
141///
142/// Matching is case-insensitive.
143const ENV_REMAPPINGS: &[(&str, &str)] = &[("http_proxy", "proxy_http"), ("https_proxy", "proxy_https")];
144
145/// A Figment provider that remaps canonical environment variable names to our desired config keys.
146///
147/// Reads environment variables case-insensitively and maps them to config keys (e.g. `HTTP_PROXY` →
148/// `proxy_http`). Values are snapshotted at construction time.
149///
150/// Add this provider to a [`ConfigurationLoader`][saluki_config::ConfigurationLoader] *after* file-based
151/// providers and *before* vendor-prefixed env providers (e.g. `DD_`) to achieve the correct precedence:
152/// file < remapped env vars < `DD_`-prefixed.
153///
154/// For YAML key aliasing (e.g. `proxy.http` → `proxy_http`), pass [`KEY_ALIASES`] to
155/// [`ConfigurationLoader::with_key_aliases`][saluki_config::ConfigurationLoader::with_key_aliases] instead —
156/// that is handled at file-load time.
157pub struct DatadogRemapper {
158    values: serde_json::Map<String, serde_json::Value>,
159}
160
161impl DatadogRemapper {
162    /// Constructs a `DatadogRemapper` by eagerly snapshotting env var remappings.
163    pub fn new() -> Self {
164        let mut values = serde_json::Map::new();
165
166        for (env_key, env_value) in std::env::vars() {
167            let lower = env_key.to_lowercase();
168            for &(from, to) in ENV_REMAPPINGS {
169                if lower == from && !values.contains_key(to) {
170                    values.insert(to.to_string(), serde_json::Value::String(env_value.clone()));
171                }
172            }
173        }
174
175        Self { values }
176    }
177}
178
179impl Default for DatadogRemapper {
180    fn default() -> Self {
181        Self::new()
182    }
183}
184
185impl Provider for DatadogRemapper {
186    fn metadata(&self) -> Metadata {
187        Metadata::named("Datadog config remapper")
188    }
189
190    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
191        if self.values.is_empty() {
192            return Ok(Map::new());
193        }
194        Serialized::defaults(serde_json::Value::Object(self.values.clone())).data()
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
203
204    #[test]
205    fn env_var_remapped_case_insensitively() {
206        let _guard = ENV_MUTEX.lock().unwrap();
207
208        std::env::set_var("HTTP_PROXY", "http://proxy.example.com");
209        let remapper = DatadogRemapper::new();
210        std::env::remove_var("HTTP_PROXY");
211
212        assert_eq!(
213            remapper.values.get("proxy_http").and_then(|v| v.as_str()),
214            Some("http://proxy.example.com"),
215        );
216    }
217
218    #[test]
219    fn env_var_not_remapped_when_absent() {
220        let _guard = ENV_MUTEX.lock().unwrap();
221
222        std::env::remove_var("HTTP_PROXY");
223        std::env::remove_var("http_proxy");
224        std::env::remove_var("HTTPS_PROXY");
225        std::env::remove_var("https_proxy");
226
227        let remapper = DatadogRemapper::new();
228
229        assert!(remapper.values.get("proxy_http").is_none());
230        assert!(remapper.values.get("proxy_https").is_none());
231    }
232}