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 isn't 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    // OPW metrics endpoint keys live in nested YAML sections, while env vars strip to flat keys. The flat fields are
133    // consumed by ForwarderConfiguration because this override is metrics-only and should not live in generic endpoint
134    // configuration.
135    (
136        "observability_pipelines_worker.metrics.enabled",
137        "observability_pipelines_worker_metrics_enabled",
138    ),
139    (
140        "observability_pipelines_worker.metrics.url",
141        "observability_pipelines_worker_metrics_url",
142    ),
143    ("vector.metrics.enabled", "vector_metrics_enabled"),
144    ("vector.metrics.url", "vector_metrics_url"),
145    // Agent IPC relates to some of the Agent's IPC configuration options.
146    //
147    // We don't use them in this crate, but we still depend on them for stuff like the environment provider, and this is
148    // 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
149    // sorts of things.
150    ("agent_ipc.grpc_max_message_size", "agent_ipc_grpc_max_message_size"),
151    // `use_v2_api.series` lives at a nested YAML path but the Agent's env var is `DD_USE_V2_API_SERIES` (flat). This
152    // alias bridges the two so file and env var sources land on the same Figment key.
153    ("use_v2_api.series", "use_v2_api_series"),
154];
155
156/// Remappings from environment variable names to canonical config keys.
157///
158/// Matching is case-insensitive.
159const ENV_REMAPPINGS: &[(&str, &str)] = &[("http_proxy", "proxy_http"), ("https_proxy", "proxy_https")];
160
161/// A Figment provider that remaps canonical environment variable names to our desired config keys.
162///
163/// Reads environment variables case-insensitively and maps them to config keys (for example, `HTTP_PROXY` →
164/// `proxy_http`). Values are snapshotted at construction time.
165///
166/// Add this provider to a [`ConfigurationLoader`][saluki_config::ConfigurationLoader] *after* file-based
167/// providers and *before* vendor-prefixed env providers (for example, `DD_`) to achieve the correct precedence:
168/// file < remapped env vars < `DD_`-prefixed.
169///
170/// For YAML key aliasing (for example, `proxy.http` → `proxy_http`), pass [`KEY_ALIASES`] to
171/// [`ConfigurationLoader::with_key_aliases`][saluki_config::ConfigurationLoader::with_key_aliases] instead—
172/// that's handled at file-load time.
173pub struct DatadogRemapper {
174    values: serde_json::Map<String, serde_json::Value>,
175}
176
177impl DatadogRemapper {
178    /// Constructs a `DatadogRemapper` by eagerly snapshotting env var remappings.
179    pub fn new() -> Self {
180        let mut values = serde_json::Map::new();
181
182        for (env_key, env_value) in std::env::vars() {
183            let lower = env_key.to_lowercase();
184            for &(from, to) in ENV_REMAPPINGS {
185                if lower == from && !values.contains_key(to) {
186                    values.insert(to.to_string(), serde_json::Value::String(env_value.clone()));
187                }
188            }
189        }
190
191        Self { values }
192    }
193}
194
195impl Default for DatadogRemapper {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201impl Provider for DatadogRemapper {
202    fn metadata(&self) -> Metadata {
203        Metadata::named("Datadog config remapper")
204    }
205
206    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
207        if self.values.is_empty() {
208            return Ok(Map::new());
209        }
210        Serialized::defaults(serde_json::Value::Object(self.values.clone())).data()
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
219
220    #[test]
221    fn env_var_remapped_case_insensitively() {
222        let _guard = ENV_MUTEX.lock().unwrap();
223
224        std::env::set_var("HTTP_PROXY", "http://proxy.example.com");
225        let remapper = DatadogRemapper::new();
226        std::env::remove_var("HTTP_PROXY");
227
228        assert_eq!(
229            remapper.values.get("proxy_http").and_then(|v| v.as_str()),
230            Some("http://proxy.example.com"),
231        );
232    }
233
234    #[test]
235    fn env_var_not_remapped_when_absent() {
236        let _guard = ENV_MUTEX.lock().unwrap();
237
238        std::env::remove_var("HTTP_PROXY");
239        std::env::remove_var("http_proxy");
240        std::env::remove_var("HTTPS_PROXY");
241        std::env::remove_var("https_proxy");
242
243        let remapper = DatadogRemapper::new();
244
245        assert!(remapper.values.get("proxy_http").is_none());
246        assert!(remapper.values.get("proxy_https").is_none());
247    }
248}