datadog_agent_config/
remapper.rs

1//! Compatibility support for the by-key configuration path: nested-to-flat key aliases and an
2//! environment-variable remapper.
3//!
4//! These make nested YAML keys and underscore-joined environment names address the same flat
5//! configuration keys, so callers using the by-key view observe the same precedence as callers
6//! using the typed configuration. They live here so the configuration system does not depend on
7//! the component crate for this support.
8//
9// TODO: remove when every consumer reads the typed nested `DatadogConfiguration`; the typed path
10// gets the same environment reachability from the generated tables.
11
12use figment::{
13    providers::Serialized,
14    value::{Dict, Map},
15    Error, Metadata, Profile, Provider,
16};
17
18/// Key aliases to pass to `ConfigurationLoader::with_key_aliases`.
19///
20/// Each entry maps a nested dot-separated path to a flat key name. When the nested path is found in a loaded
21/// config file, its value is also emitted under the flat key—but only if the flat key isn't already
22/// explicitly set. This ensures both YAML nested format and flat env var format produce the same Figment key,
23/// so source precedence (env vars > file) works correctly.
24pub const KEY_ALIASES: &[(&str, &str)] = &[
25    // The Datadog Agent config file uses `proxy: http:` and `proxy: https:` (nested), while env
26    // vars produce `proxy_http` and `proxy_https` (flat). Figment treats these as different keys,
27    // so without this alias env var precedence over YAML is silently broken for proxy config.
28    ("proxy.http", "proxy_http"),
29    ("proxy.https", "proxy_https"),
30    ("proxy.no_proxy", "proxy_no_proxy"),
31    ("apm_config.enable_rare_sampler", "apm_enable_rare_sampler"),
32    (
33        "apm_config.error_tracking_standalone.enabled",
34        "apm_error_tracking_standalone_enabled",
35    ),
36    // Obfuscation keys live at `apm_config.obfuscation.*` in YAML but the Agent's env vars use
37    // `DD_APM_OBFUSCATION_*` (no `_CONFIG_` segment), producing flat keys. These aliases emit the
38    // flat key when the nested YAML path is present so that both sources land on the same Figment
39    // key and env var precedence over file config works correctly.
40    (
41        "apm_config.obfuscation.credit_cards.enabled",
42        "apm_obfuscation_credit_cards_enabled",
43    ),
44    (
45        "apm_config.obfuscation.credit_cards.keep_values",
46        "apm_obfuscation_credit_cards_keep_values",
47    ),
48    (
49        "apm_config.obfuscation.credit_cards.luhn",
50        "apm_obfuscation_credit_cards_luhn",
51    ),
52    (
53        "apm_config.obfuscation.elasticsearch.enabled",
54        "apm_obfuscation_elasticsearch_enabled",
55    ),
56    (
57        "apm_config.obfuscation.elasticsearch.keep_values",
58        "apm_obfuscation_elasticsearch_keep_values",
59    ),
60    (
61        "apm_config.obfuscation.elasticsearch.obfuscate_sql_values",
62        "apm_obfuscation_elasticsearch_obfuscate_sql_values",
63    ),
64    (
65        "apm_config.obfuscation.http.remove_paths_with_digits",
66        "apm_obfuscation_http_remove_paths_with_digits",
67    ),
68    (
69        "apm_config.obfuscation.http.remove_query_string",
70        "apm_obfuscation_http_remove_query_string",
71    ),
72    (
73        "apm_config.obfuscation.memcached.enabled",
74        "apm_obfuscation_memcached_enabled",
75    ),
76    (
77        "apm_config.obfuscation.memcached.keep_command",
78        "apm_obfuscation_memcached_keep_command",
79    ),
80    (
81        "apm_config.obfuscation.mongodb.enabled",
82        "apm_obfuscation_mongodb_enabled",
83    ),
84    (
85        "apm_config.obfuscation.mongodb.keep_values",
86        "apm_obfuscation_mongodb_keep_values",
87    ),
88    (
89        "apm_config.obfuscation.mongodb.obfuscate_sql_values",
90        "apm_obfuscation_mongodb_obfuscate_sql_values",
91    ),
92    (
93        "apm_config.obfuscation.opensearch.enabled",
94        "apm_obfuscation_opensearch_enabled",
95    ),
96    (
97        "apm_config.obfuscation.opensearch.keep_values",
98        "apm_obfuscation_opensearch_keep_values",
99    ),
100    (
101        "apm_config.obfuscation.opensearch.obfuscate_sql_values",
102        "apm_obfuscation_opensearch_obfuscate_sql_values",
103    ),
104    ("apm_config.obfuscation.redis.enabled", "apm_obfuscation_redis_enabled"),
105    (
106        "apm_config.obfuscation.redis.remove_all_args",
107        "apm_obfuscation_redis_remove_all_args",
108    ),
109    (
110        "apm_config.obfuscation.valkey.enabled",
111        "apm_obfuscation_valkey_enabled",
112    ),
113    (
114        "apm_config.obfuscation.valkey.remove_all_args",
115        "apm_obfuscation_valkey_remove_all_args",
116    ),
117    ("apm_config.obfuscation.sql.dbms", "apm_obfuscation_sql_dbms"),
118    (
119        "apm_config.obfuscation.sql.dollar_quoted_func",
120        "apm_obfuscation_sql_dollar_quoted_func",
121    ),
122    (
123        "apm_config.obfuscation.sql.keep_sql_alias",
124        "apm_obfuscation_sql_keep_sql_alias",
125    ),
126    (
127        "apm_config.obfuscation.sql.replace_digits",
128        "apm_obfuscation_sql_replace_digits",
129    ),
130    (
131        "apm_config.obfuscation.sql.table_names",
132        "apm_obfuscation_sql_table_names",
133    ),
134    // `otlp_config.traces.probabilistic_sampler.sampling_percentage` lives at a deeply nested YAML
135    // path but the Agent's env var uses `DD_OTLP_CONFIG_TRACES_PROBABILISTIC_SAMPLER_SAMPLING_PERCENTAGE`,
136    // which strips to a flat key. This alias bridges the two so env var precedence over file config
137    // works correctly.
138    (
139        "otlp_config.traces.probabilistic_sampler.sampling_percentage",
140        "otlp_config_traces_probabilistic_sampler_sampling_percentage",
141    ),
142    // OPW metrics endpoint keys live in nested YAML sections, while env vars strip to flat keys. The flat fields are
143    // consumed by ForwarderConfiguration because this override is metrics-only and should not live in generic endpoint
144    // configuration.
145    (
146        "observability_pipelines_worker.metrics.enabled",
147        "observability_pipelines_worker_metrics_enabled",
148    ),
149    (
150        "observability_pipelines_worker.metrics.url",
151        "observability_pipelines_worker_metrics_url",
152    ),
153    (
154        "observability_pipelines_worker.metrics.use_v3_api.series",
155        "observability_pipelines_worker_metrics_use_v3_api_series",
156    ),
157    ("vector.metrics.enabled", "vector_metrics_enabled"),
158    ("vector.metrics.url", "vector_metrics_url"),
159    ("vector.metrics.use_v3_api.series", "vector_metrics_use_v3_api_series"),
160    // Agent IPC relates to some of the Agent's IPC configuration options.
161    //
162    // We don't use them in this crate, but we still depend on them for stuff like the environment provider, and this is
163    // 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
164    // sorts of things.
165    ("agent_ipc.grpc_max_message_size", "agent_ipc_grpc_max_message_size"),
166    // `use_v2_api.series` lives at a nested YAML path but the Agent's env var is `DD_USE_V2_API_SERIES` (flat). This
167    // alias bridges the two so file and env var sources land on the same Figment key.
168    ("use_v2_api.series", "use_v2_api_series"),
169    // `use_v3_api.series` follows the Agent's nested config shape. Flatten it so both YAML and env-var sources feed the
170    // shared ADP config structs.
171    ("use_v3_api.series.enabled", "use_v3_api_series_enabled"),
172    ("use_v3_api.series.endpoints", "use_v3_api_series_endpoints"),
173    // ADP-specific zstd compression level: YAML `data_plane.serializer_zstd_compressor_level` →
174    // flat key `data_plane_serializer_zstd_compressor_level`. This lets both YAML and env-var
175    // sources feed the shared encoder config structs with correct precedence.
176    (
177        "data_plane.serializer_zstd_compressor_level",
178        "data_plane_serializer_zstd_compressor_level",
179    ),
180];
181
182/// Remappings from environment variable names to canonical config keys.
183///
184/// Matching is case-insensitive.
185const ENV_REMAPPINGS: &[(&str, &str)] = &[("http_proxy", "proxy_http"), ("https_proxy", "proxy_https")];
186
187/// A Figment provider that remaps canonical environment variable names to our desired config keys.
188///
189/// Reads environment variables case-insensitively and maps them to config keys (for example, `HTTP_PROXY` →
190/// `proxy_http`). Values are snapshotted at construction time.
191///
192/// Add this provider to a `ConfigurationLoader` *after* file-based providers and *before*
193/// vendor-prefixed env providers (for example, `DD_`) to achieve the correct precedence:
194/// file < remapped env vars < `DD_`-prefixed.
195///
196/// For YAML key aliasing (for example, `proxy.http` → `proxy_http`), pass [`KEY_ALIASES`] to
197/// `ConfigurationLoader::with_key_aliases` instead—that's handled at file-load time.
198pub struct DatadogRemapper {
199    values: serde_json::Map<String, serde_json::Value>,
200}
201
202impl DatadogRemapper {
203    /// Constructs a `DatadogRemapper` by eagerly snapshotting env var remappings from the current process
204    /// environment.
205    pub fn new() -> Self {
206        Self::from_env_iter(std::env::vars())
207    }
208
209    /// Constructs a `DatadogRemapper` from explicitly provided environment variable name/value pairs.
210    ///
211    /// Names are matched case-insensitively against `ENV_REMAPPINGS`. When more than one name maps to the same
212    /// canonical key (for example, both `HTTP_PROXY` and `http_proxy` map to `proxy_http`), the first matching name
213    /// encountered wins and later matches are ignored.
214    pub fn from_env_vars(env_vars: Vec<(String, String)>) -> Self {
215        Self::from_env_iter(env_vars)
216    }
217
218    fn from_env_iter<I>(env_vars: I) -> Self
219    where
220        I: IntoIterator<Item = (String, String)>,
221    {
222        let mut values = serde_json::Map::new();
223
224        for (env_key, env_value) in env_vars {
225            let lower = env_key.to_lowercase();
226            for &(from, to) in ENV_REMAPPINGS {
227                if lower == from && !values.contains_key(to) {
228                    values.insert(to.to_string(), serde_json::Value::String(env_value.clone()));
229                }
230            }
231        }
232
233        Self { values }
234    }
235}
236
237impl Default for DatadogRemapper {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243impl Provider for DatadogRemapper {
244    fn metadata(&self) -> Metadata {
245        Metadata::named("Datadog config remapper")
246    }
247
248    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
249        if self.values.is_empty() {
250            return Ok(Map::new());
251        }
252        Serialized::defaults(serde_json::Value::Object(self.values.clone())).data()
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
261
262    #[test]
263    fn env_var_remapped_case_insensitively() {
264        let _guard = ENV_MUTEX.lock().unwrap();
265
266        std::env::set_var("HTTP_PROXY", "http://proxy.example.com");
267        let remapper = DatadogRemapper::new();
268        std::env::remove_var("HTTP_PROXY");
269
270        assert_eq!(
271            remapper.values.get("proxy_http").and_then(|v| v.as_str()),
272            Some("http://proxy.example.com"),
273        );
274    }
275
276    #[test]
277    fn env_var_not_remapped_when_absent() {
278        let _guard = ENV_MUTEX.lock().unwrap();
279
280        std::env::remove_var("HTTP_PROXY");
281        std::env::remove_var("http_proxy");
282        std::env::remove_var("HTTPS_PROXY");
283        std::env::remove_var("https_proxy");
284
285        let remapper = DatadogRemapper::new();
286
287        assert!(remapper.values.get("proxy_http").is_none());
288        assert!(remapper.values.get("proxy_https").is_none());
289    }
290
291    #[test]
292    fn first_matching_env_var_wins_for_remapped_key() {
293        // `HTTP_PROXY` and `http_proxy` both remap to the canonical `proxy_http` key. The `!values.contains_key`
294        // guard means the first matching name encountered wins and any later match is ignored. Feeding an
295        // explicitly-ordered iterator makes this deterministic, independent of `std::env::vars` ordering (and so
296        // this case needs no process-env mutation or lock).
297        let remapper = DatadogRemapper::from_env_vars(vec![
298            ("HTTP_PROXY".to_string(), "http://first.example.com".to_string()),
299            ("http_proxy".to_string(), "http://second.example.com".to_string()),
300        ]);
301
302        assert_eq!(
303            remapper.values.get("proxy_http").and_then(|v| v.as_str()),
304            Some("http://first.example.com"),
305        );
306    }
307}