Skip to main content

saluki_components/config/
mrf.rs

1//! Multi-region failover configuration.
2
3use saluki_config::GenericConfiguration;
4use saluki_error::GenericError;
5
6const MRF_METRICS_ENDPOINT_PREFIX: &str = "https://app.mrf.";
7
8/// Multi-region failover configuration shared by signal-specific pipelines.
9#[derive(Clone, Debug, Eq, PartialEq)]
10pub struct MrfConfiguration {
11    enabled: bool,
12    failover_metrics: bool,
13    metric_allowlist: Vec<String>,
14    api_key: Option<String>,
15    site: Option<String>,
16    dd_url: Option<String>,
17}
18
19impl MrfConfiguration {
20    /// Creates a new `MrfConfiguration` from the given configuration.
21    pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
22        Ok(Self {
23            enabled: config.try_get_typed("multi_region_failover.enabled")?.unwrap_or(false),
24            failover_metrics: config
25                .try_get_typed("multi_region_failover.failover_metrics")?
26                .unwrap_or(false),
27            metric_allowlist: config
28                .try_get_typed("multi_region_failover.metric_allowlist")?
29                .unwrap_or_default(),
30            api_key: get_non_empty_string(config, "multi_region_failover.api_key")?,
31            site: get_non_empty_string(config, "multi_region_failover.site")?,
32            dd_url: get_non_empty_string(config, "multi_region_failover.dd_url")?,
33        })
34    }
35
36    /// Returns whether multi-region failover is enabled for this process.
37    pub const fn is_enabled(&self) -> bool {
38        self.enabled
39    }
40
41    /// Returns whether metrics forwarding to the failover region is requested by configuration.
42    pub const fn is_metrics_forwarding_requested(&self) -> bool {
43        self.enabled && self.failover_metrics
44    }
45
46    /// Updates whether metrics forwarding to the failover region is enabled.
47    pub(crate) const fn set_failover_metrics(&mut self, failover_metrics: bool) {
48        self.failover_metrics = failover_metrics;
49    }
50
51    /// Updates the metric allowlist.
52    pub(crate) fn set_metric_allowlist(&mut self, metric_allowlist: Vec<String>) {
53        self.metric_allowlist = metric_allowlist;
54    }
55
56    /// Returns the metric allowlist.
57    pub fn metric_allowlist(&self) -> &[String] {
58        &self.metric_allowlist
59    }
60
61    /// Returns the failover-region API key.
62    pub fn api_key(&self) -> Option<&str> {
63        self.api_key.as_deref()
64    }
65
66    /// Returns the failover-region metrics endpoint URL.
67    ///
68    /// `multi_region_failover.dd_url` takes precedence and is used as provided. When only
69    /// `multi_region_failover.site` is configured, the Datadog MRF metrics endpoint is derived from
70    /// that site.
71    pub fn metrics_endpoint_url(&self) -> Option<String> {
72        self.dd_url.clone().or_else(|| {
73            self.site
74                .as_deref()
75                .map(|site| format!("{MRF_METRICS_ENDPOINT_PREFIX}{site}"))
76        })
77    }
78
79    /// Returns the endpoint and API key override for the failover-region metrics forwarder.
80    pub fn metrics_endpoint_override(&self) -> Option<(String, String)> {
81        if !self.enabled {
82            return None;
83        }
84
85        Some((self.metrics_endpoint_url()?, self.api_key.clone()?))
86    }
87}
88
89fn get_non_empty_string(config: &GenericConfiguration, key: &str) -> Result<Option<String>, GenericError> {
90    Ok(config
91        .try_get_typed::<String>(key)?
92        .map(|value| value.trim().to_string())
93        .filter(|value| !value.is_empty()))
94}
95
96#[cfg(test)]
97mod tests {
98    use saluki_config::ConfigurationLoader;
99    use serde_json::json;
100
101    use super::*;
102
103    async fn mrf_config_from(value: serde_json::Value) -> MrfConfiguration {
104        let (config, _) = ConfigurationLoader::for_tests(Some(value), None, false).await;
105        MrfConfiguration::from_configuration(&config).expect("MRF configuration should deserialize")
106    }
107
108    #[tokio::test]
109    async fn parses_mrf_configuration_keys() {
110        let config = mrf_config_from(json!({
111            "multi_region_failover": {
112                "enabled": true,
113                "failover_metrics": true,
114                "metric_allowlist": ["first.metric", "second.metric"],
115                "api_key": "mrf-api-key",
116                "site": "datadoghq.eu"
117            }
118        }))
119        .await;
120
121        assert!(config.is_metrics_forwarding_requested());
122        assert_eq!(config.metric_allowlist(), ["first.metric", "second.metric"]);
123        assert_eq!(config.api_key(), Some("mrf-api-key"));
124        assert_eq!(
125            config.metrics_endpoint_url().as_deref(),
126            Some("https://app.mrf.datadoghq.eu")
127        );
128    }
129
130    #[tokio::test]
131    async fn metrics_endpoint_override_requires_api_key_and_endpoint() {
132        let missing_api_key = mrf_config_from(json!({
133            "multi_region_failover": {
134                "enabled": true,
135                "failover_metrics": true,
136                "site": "datadoghq.eu"
137            }
138        }))
139        .await;
140        assert_eq!(missing_api_key.metrics_endpoint_override(), None);
141
142        let missing_endpoint = mrf_config_from(json!({
143            "multi_region_failover": {
144                "enabled": true,
145                "failover_metrics": true,
146                "api_key": "mrf-api-key"
147            }
148        }))
149        .await;
150        assert_eq!(missing_endpoint.metrics_endpoint_override(), None);
151
152        let ready = mrf_config_from(json!({
153            "multi_region_failover": {
154                "enabled": true,
155                "failover_metrics": true,
156                "api_key": "mrf-api-key",
157                "dd_url": "https://mrf.example.com"
158            }
159        }))
160        .await;
161        assert_eq!(
162            ready.metrics_endpoint_override(),
163            Some(("https://mrf.example.com".to_string(), "mrf-api-key".to_string()))
164        );
165    }
166
167    #[tokio::test]
168    async fn metrics_endpoint_override_does_not_require_failover_metrics() {
169        let config = mrf_config_from(json!({
170            "multi_region_failover": {
171                "enabled": true,
172                "failover_metrics": false,
173                "api_key": "mrf-api-key",
174                "dd_url": "https://mrf.example.com"
175            }
176        }))
177        .await;
178
179        assert!(!config.is_metrics_forwarding_requested());
180        assert_eq!(
181            config.metrics_endpoint_override(),
182            Some(("https://mrf.example.com".to_string(), "mrf-api-key".to_string()))
183        );
184    }
185
186    #[tokio::test]
187    async fn dd_url_takes_precedence_over_site() {
188        let config = mrf_config_from(json!({
189            "multi_region_failover": {
190                "site": "datadoghq.eu",
191                "dd_url": "https://custom-mrf.example.com"
192            }
193        }))
194        .await;
195
196        assert_eq!(
197            config.metrics_endpoint_url().as_deref(),
198            Some("https://custom-mrf.example.com")
199        );
200    }
201}