Skip to main content

saluki_components/config/
mod.rs

1//! Datadog-specific configuration providers and remappers.
2
3pub mod autoscaling_failover;
4pub mod cluster_agent;
5pub mod mrf;
6use figment::{
7    providers::Serialized,
8    value::{Dict, Map},
9    Error, Metadata, Profile, Provider,
10};
11
12pub use self::autoscaling_failover::AutoscalingFailoverConfiguration;
13pub use self::cluster_agent::ClusterAgentConfiguration;
14pub use self::mrf::MrfConfiguration;
15
16/// Key aliases to pass to [`ConfigurationLoader::with_key_aliases`][saluki_config::ConfigurationLoader::with_key_aliases].
17///
18/// Each entry maps a nested dot-separated path to a flat key name. When the nested path is found in a loaded
19/// config file, its value is also emitted under the flat key—but only if the flat key isn't already
20/// explicitly set. This ensures both YAML nested format and flat env var format produce the same Figment key,
21/// so source precedence (env vars > file) works correctly.
22pub const KEY_ALIASES: &[(&str, &str)] = &[
23    // The Datadog Agent config file uses `proxy: http:` and `proxy: https:` (nested), while env
24    // vars produce `proxy_http` and `proxy_https` (flat). Figment treats these as different keys,
25    // so without this alias env var precedence over YAML is silently broken for proxy config.
26    ("proxy.http", "proxy_http"),
27    ("proxy.https", "proxy_https"),
28    ("proxy.no_proxy", "proxy_no_proxy"),
29    ("apm_config.enable_rare_sampler", "apm_enable_rare_sampler"),
30    (
31        "apm_config.error_tracking_standalone.enabled",
32        "apm_error_tracking_standalone_enabled",
33    ),
34    // Obfuscation keys live at `apm_config.obfuscation.*` in YAML but the Agent's env vars use
35    // `DD_APM_OBFUSCATION_*` (no `_CONFIG_` segment), producing flat keys. These aliases emit the
36    // flat key when the nested YAML path is present so that both sources land on the same Figment
37    // key and env var precedence over file config works correctly.
38    (
39        "apm_config.obfuscation.credit_cards.enabled",
40        "apm_obfuscation_credit_cards_enabled",
41    ),
42    (
43        "apm_config.obfuscation.credit_cards.keep_values",
44        "apm_obfuscation_credit_cards_keep_values",
45    ),
46    (
47        "apm_config.obfuscation.credit_cards.luhn",
48        "apm_obfuscation_credit_cards_luhn",
49    ),
50    (
51        "apm_config.obfuscation.elasticsearch.enabled",
52        "apm_obfuscation_elasticsearch_enabled",
53    ),
54    (
55        "apm_config.obfuscation.elasticsearch.keep_values",
56        "apm_obfuscation_elasticsearch_keep_values",
57    ),
58    (
59        "apm_config.obfuscation.elasticsearch.obfuscate_sql_values",
60        "apm_obfuscation_elasticsearch_obfuscate_sql_values",
61    ),
62    (
63        "apm_config.obfuscation.http.remove_paths_with_digits",
64        "apm_obfuscation_http_remove_paths_with_digits",
65    ),
66    (
67        "apm_config.obfuscation.http.remove_query_string",
68        "apm_obfuscation_http_remove_query_string",
69    ),
70    (
71        "apm_config.obfuscation.memcached.enabled",
72        "apm_obfuscation_memcached_enabled",
73    ),
74    (
75        "apm_config.obfuscation.memcached.keep_command",
76        "apm_obfuscation_memcached_keep_command",
77    ),
78    (
79        "apm_config.obfuscation.mongodb.enabled",
80        "apm_obfuscation_mongodb_enabled",
81    ),
82    (
83        "apm_config.obfuscation.mongodb.keep_values",
84        "apm_obfuscation_mongodb_keep_values",
85    ),
86    (
87        "apm_config.obfuscation.mongodb.obfuscate_sql_values",
88        "apm_obfuscation_mongodb_obfuscate_sql_values",
89    ),
90    (
91        "apm_config.obfuscation.opensearch.enabled",
92        "apm_obfuscation_opensearch_enabled",
93    ),
94    (
95        "apm_config.obfuscation.opensearch.keep_values",
96        "apm_obfuscation_opensearch_keep_values",
97    ),
98    (
99        "apm_config.obfuscation.opensearch.obfuscate_sql_values",
100        "apm_obfuscation_opensearch_obfuscate_sql_values",
101    ),
102    ("apm_config.obfuscation.redis.enabled", "apm_obfuscation_redis_enabled"),
103    (
104        "apm_config.obfuscation.redis.remove_all_args",
105        "apm_obfuscation_redis_remove_all_args",
106    ),
107    (
108        "apm_config.obfuscation.valkey.enabled",
109        "apm_obfuscation_valkey_enabled",
110    ),
111    (
112        "apm_config.obfuscation.valkey.remove_all_args",
113        "apm_obfuscation_valkey_remove_all_args",
114    ),
115    ("apm_config.obfuscation.sql.dbms", "apm_obfuscation_sql_dbms"),
116    (
117        "apm_config.obfuscation.sql.dollar_quoted_func",
118        "apm_obfuscation_sql_dollar_quoted_func",
119    ),
120    (
121        "apm_config.obfuscation.sql.keep_sql_alias",
122        "apm_obfuscation_sql_keep_sql_alias",
123    ),
124    (
125        "apm_config.obfuscation.sql.replace_digits",
126        "apm_obfuscation_sql_replace_digits",
127    ),
128    (
129        "apm_config.obfuscation.sql.table_names",
130        "apm_obfuscation_sql_table_names",
131    ),
132    // `otlp_config.traces.probabilistic_sampler.sampling_percentage` lives at a deeply nested YAML
133    // path but the Agent's env var uses `DD_OTLP_CONFIG_TRACES_PROBABILISTIC_SAMPLER_SAMPLING_PERCENTAGE`,
134    // which strips to a flat key. This alias bridges the two so env var precedence over file config
135    // works correctly.
136    (
137        "otlp_config.traces.probabilistic_sampler.sampling_percentage",
138        "otlp_config_traces_probabilistic_sampler_sampling_percentage",
139    ),
140    // OPW metrics endpoint keys live in nested YAML sections, while env vars strip to flat keys. The flat fields are
141    // consumed by ForwarderConfiguration because this override is metrics-only and should not live in generic endpoint
142    // configuration.
143    (
144        "observability_pipelines_worker.metrics.enabled",
145        "observability_pipelines_worker_metrics_enabled",
146    ),
147    (
148        "observability_pipelines_worker.metrics.url",
149        "observability_pipelines_worker_metrics_url",
150    ),
151    ("vector.metrics.enabled", "vector_metrics_enabled"),
152    ("vector.metrics.url", "vector_metrics_url"),
153    // Agent IPC relates to some of the Agent's IPC configuration options.
154    //
155    // We don't use them in this crate, but we still depend on them for stuff like the environment provider, and this is
156    // 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
157    // sorts of things.
158    ("agent_ipc.grpc_max_message_size", "agent_ipc_grpc_max_message_size"),
159    // `use_v2_api.series` lives at a nested YAML path but the Agent's env var is `DD_USE_V2_API_SERIES` (flat). This
160    // alias bridges the two so file and env var sources land on the same Figment key.
161    ("use_v2_api.series", "use_v2_api_series"),
162];
163
164/// Remappings from environment variable names to canonical config keys.
165///
166/// Matching is case-insensitive.
167const ENV_REMAPPINGS: &[(&str, &str)] = &[("http_proxy", "proxy_http"), ("https_proxy", "proxy_https")];
168
169/// A Figment provider that remaps canonical environment variable names to our desired config keys.
170///
171/// Reads environment variables case-insensitively and maps them to config keys (for example, `HTTP_PROXY` →
172/// `proxy_http`). Values are snapshotted at construction time.
173///
174/// Add this provider to a [`ConfigurationLoader`][saluki_config::ConfigurationLoader] *after* file-based
175/// providers and *before* vendor-prefixed env providers (for example, `DD_`) to achieve the correct precedence:
176/// file < remapped env vars < `DD_`-prefixed.
177///
178/// For YAML key aliasing (for example, `proxy.http` → `proxy_http`), pass [`KEY_ALIASES`] to
179/// [`ConfigurationLoader::with_key_aliases`][saluki_config::ConfigurationLoader::with_key_aliases] instead—
180/// that's handled at file-load time.
181pub struct DatadogRemapper {
182    values: serde_json::Map<String, serde_json::Value>,
183}
184
185impl DatadogRemapper {
186    /// Constructs a `DatadogRemapper` by eagerly snapshotting env var remappings.
187    pub fn new() -> Self {
188        let mut values = serde_json::Map::new();
189
190        for (env_key, env_value) in std::env::vars() {
191            let lower = env_key.to_lowercase();
192            for &(from, to) in ENV_REMAPPINGS {
193                if lower == from && !values.contains_key(to) {
194                    values.insert(to.to_string(), serde_json::Value::String(env_value.clone()));
195                }
196            }
197        }
198
199        Self { values }
200    }
201}
202
203impl Default for DatadogRemapper {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209impl Provider for DatadogRemapper {
210    fn metadata(&self) -> Metadata {
211        Metadata::named("Datadog config remapper")
212    }
213
214    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
215        if self.values.is_empty() {
216            return Ok(Map::new());
217        }
218        Serialized::defaults(serde_json::Value::Object(self.values.clone())).data()
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
227
228    #[test]
229    fn env_var_remapped_case_insensitively() {
230        let _guard = ENV_MUTEX.lock().unwrap();
231
232        std::env::set_var("HTTP_PROXY", "http://proxy.example.com");
233        let remapper = DatadogRemapper::new();
234        std::env::remove_var("HTTP_PROXY");
235
236        assert_eq!(
237            remapper.values.get("proxy_http").and_then(|v| v.as_str()),
238            Some("http://proxy.example.com"),
239        );
240    }
241
242    #[test]
243    fn env_var_not_remapped_when_absent() {
244        let _guard = ENV_MUTEX.lock().unwrap();
245
246        std::env::remove_var("HTTP_PROXY");
247        std::env::remove_var("http_proxy");
248        std::env::remove_var("HTTPS_PROXY");
249        std::env::remove_var("https_proxy");
250
251        let remapper = DatadogRemapper::new();
252
253        assert!(remapper.values.get("proxy_http").is_none());
254        assert!(remapper.values.get("proxy_https").is_none());
255    }
256}