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::config_from;
99    use serde_json::json;
100
101    use super::*;
102
103    async fn mrf_config_from(value: serde_json::Value) -> MrfConfiguration {
104        MrfConfiguration::from_configuration(&config_from(value).await).expect("MRF configuration should deserialize")
105    }
106
107    #[tokio::test]
108    async fn parses_mrf_configuration_keys() {
109        let config = mrf_config_from(json!({
110            "multi_region_failover": {
111                "enabled": true,
112                "failover_metrics": true,
113                "metric_allowlist": ["first.metric", "second.metric"],
114                "api_key": "mrf-api-key",
115                "site": "datadoghq.eu"
116            }
117        }))
118        .await;
119
120        assert!(config.is_metrics_forwarding_requested());
121        assert_eq!(config.metric_allowlist(), ["first.metric", "second.metric"]);
122        assert_eq!(config.api_key(), Some("mrf-api-key"));
123        assert_eq!(
124            config.metrics_endpoint_url().as_deref(),
125            Some("https://app.mrf.datadoghq.eu")
126        );
127    }
128
129    #[tokio::test]
130    async fn metrics_endpoint_override_requires_api_key_and_endpoint() {
131        let missing_api_key = mrf_config_from(json!({
132            "multi_region_failover": {
133                "enabled": true,
134                "failover_metrics": true,
135                "site": "datadoghq.eu"
136            }
137        }))
138        .await;
139        assert_eq!(missing_api_key.metrics_endpoint_override(), None);
140
141        let missing_endpoint = mrf_config_from(json!({
142            "multi_region_failover": {
143                "enabled": true,
144                "failover_metrics": true,
145                "api_key": "mrf-api-key"
146            }
147        }))
148        .await;
149        assert_eq!(missing_endpoint.metrics_endpoint_override(), None);
150
151        let ready = mrf_config_from(json!({
152            "multi_region_failover": {
153                "enabled": true,
154                "failover_metrics": true,
155                "api_key": "mrf-api-key",
156                "dd_url": "https://mrf.example.com"
157            }
158        }))
159        .await;
160        assert_eq!(
161            ready.metrics_endpoint_override(),
162            Some(("https://mrf.example.com".to_string(), "mrf-api-key".to_string()))
163        );
164    }
165
166    #[tokio::test]
167    async fn metrics_endpoint_override_does_not_require_failover_metrics() {
168        let config = mrf_config_from(json!({
169            "multi_region_failover": {
170                "enabled": true,
171                "failover_metrics": false,
172                "api_key": "mrf-api-key",
173                "dd_url": "https://mrf.example.com"
174            }
175        }))
176        .await;
177
178        assert!(!config.is_metrics_forwarding_requested());
179        assert_eq!(
180            config.metrics_endpoint_override(),
181            Some(("https://mrf.example.com".to_string(), "mrf-api-key".to_string()))
182        );
183    }
184
185    #[tokio::test]
186    async fn metrics_endpoint_override_is_none_when_disabled() {
187        // Even with a fully-populated endpoint and API key, `metrics_endpoint_override` short-circuits to `None`
188        // when multi-region failover is disabled (the `if !self.enabled` guard at the top of the method).
189        let config = mrf_config_from(json!({
190            "multi_region_failover": {
191                "enabled": false,
192                "failover_metrics": true,
193                "api_key": "mrf-api-key",
194                "dd_url": "https://mrf.example.com"
195            }
196        }))
197        .await;
198
199        assert!(!config.is_enabled());
200        assert_eq!(config.metrics_endpoint_override(), None);
201
202        // The endpoint URL itself still resolves from configuration; only the override is gated on `enabled`, which
203        // confirms the `None` above comes from the disabled short-circuit rather than a missing endpoint/API key.
204        assert_eq!(
205            config.metrics_endpoint_url().as_deref(),
206            Some("https://mrf.example.com")
207        );
208        assert_eq!(config.api_key(), Some("mrf-api-key"));
209    }
210
211    #[tokio::test]
212    async fn dd_url_takes_precedence_over_site() {
213        let config = mrf_config_from(json!({
214            "multi_region_failover": {
215                "site": "datadoghq.eu",
216                "dd_url": "https://custom-mrf.example.com"
217            }
218        }))
219        .await;
220
221        assert_eq!(
222            config.metrics_endpoint_url().as_deref(),
223            Some("https://custom-mrf.example.com")
224        );
225    }
226}