saluki_config/dynamic/
event.rs

1//! Defines the event type for configuration changes.
2
3use serde_json::Value as JsonValue;
4
5use crate::upsert;
6
7/// An event that occurs when the configuration changes.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct ConfigChangeEvent {
10    /// The key that changed.
11    pub key: String,
12    /// The previous value, if any.
13    pub old_value: Option<JsonValue>,
14    /// The new value.
15    pub new_value: Option<JsonValue>,
16}
17
18/// Whether a configuration setting was set explicitly, or merely defaulted.
19///
20/// A configuration producer generally publishes every setting it knows about, including settings
21/// nobody configured, so a value on its own cannot say whether anything set it.
22/// Consumers that treat a defaulted setting differently from a configured one need this distinction;
23/// consumers that only want effective values can ignore it.
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum Provenance {
26    /// The value came from a real input: a configuration file, an environment variable, remote
27    /// configuration, and so on.
28    Explicit,
29    /// The value is the producer's own default, standing in for a setting nobody configured.
30    Default,
31}
32
33/// A single configuration setting, as published by a configuration producer.
34///
35/// The key is in the producer's own flat, possibly dotted form, and the value is that key's entire
36/// value: a map-valued setting arrives as one setting, not one setting per entry.
37#[derive(Clone, Debug, PartialEq)]
38pub struct ConfigSetting {
39    /// The key being set.
40    pub key: String,
41    /// The value of the key.
42    pub value: JsonValue,
43    /// Where the value came from.
44    pub provenance: Provenance,
45}
46
47impl ConfigSetting {
48    /// Creates a new `ConfigSetting`.
49    pub fn new(key: impl Into<String>, value: JsonValue, provenance: Provenance) -> Self {
50        Self {
51            key: key.into(),
52            value,
53            provenance,
54        }
55    }
56
57    /// Creates a new `ConfigSetting` that an input set explicitly.
58    pub fn explicit(key: impl Into<String>, value: JsonValue) -> Self {
59        Self::new(key, value, Provenance::Explicit)
60    }
61}
62
63/// An update message for the dynamic configuration state, sent from the config stream to the updater task.
64#[derive(Clone, Debug)]
65pub enum ConfigUpdate {
66    /// A complete snapshot of the configuration.
67    ///
68    /// The existing state should be replaced.
69    Snapshot(Vec<ConfigSetting>),
70    /// A partial update for a single setting.
71    ///
72    /// This should be merged into the existing state.
73    Partial(ConfigSetting),
74}
75
76impl ConfigUpdate {
77    /// Creates a snapshot update from the given settings.
78    pub fn snapshot(settings: impl IntoIterator<Item = ConfigSetting>) -> Self {
79        Self::Snapshot(settings.into_iter().collect())
80    }
81}
82
83/// Builds the nested state tree described by `settings`.
84///
85/// Dotted keys expand into nested objects, and each value is inserted whole, so an object-valued
86/// setting keeps entry keys that themselves contain dots (intake URLs, for example) intact.
87pub fn settings_to_state(settings: &[ConfigSetting]) -> JsonValue {
88    let mut state = JsonValue::Object(serde_json::Map::new());
89    for setting in settings {
90        upsert(&mut state, &setting.key, setting.value.clone());
91    }
92
93    state
94}
95
96#[cfg(test)]
97mod tests {
98    use serde_json::json;
99
100    use super::{settings_to_state, ConfigSetting, Provenance};
101
102    #[test]
103    fn settings_expand_dotted_keys_into_nested_objects() {
104        let settings = [
105            ConfigSetting::explicit("dogstatsd_port", json!(8125)),
106            ConfigSetting::explicit("otlp_config.traces.enabled", json!(true)),
107        ];
108
109        assert_eq!(
110            settings_to_state(&settings),
111            json!({ "dogstatsd_port": 8125, "otlp_config": { "traces": { "enabled": true } } })
112        );
113    }
114
115    #[test]
116    fn object_valued_settings_keep_dotted_entry_keys_intact() {
117        let settings = [ConfigSetting::explicit(
118            "additional_endpoints",
119            json!({ "https://app.datadoghq.eu": ["deadbeef"] }),
120        )];
121
122        assert_eq!(
123            settings_to_state(&settings),
124            json!({ "additional_endpoints": { "https://app.datadoghq.eu": ["deadbeef"] } })
125        );
126    }
127
128    #[test]
129    fn later_settings_win_over_earlier_ones() {
130        let settings = [
131            ConfigSetting::new("dd_url", json!("https://app.datadoghq.com"), Provenance::Default),
132            ConfigSetting::explicit("dd_url", json!("https://app.datadoghq.eu")),
133        ];
134
135        assert_eq!(
136            settings_to_state(&settings),
137            json!({ "dd_url": "https://app.datadoghq.eu" })
138        );
139    }
140}