datadog_agent_config/
lib.rs

1/// Deserializers that coerce a scalar leaf the way the Agent's own type cast does.
2mod cast_de;
3
4pub mod classifier;
5
6mod duration_de;
7
8/// Decoders that turn a raw environment-variable string into the JSON shape a schema leaf declares.
9pub mod env_decode;
10mod list_de;
11
12/// A Figment provider that reads the schema's environment variables into their canonical shape.
13pub mod env_provider;
14
15/// Builds the typed configuration base by reading environment variables directly and decoding them
16/// into the nested configuration shape.
17pub mod env_reader;
18
19/// Build-time generated code, produced from `core_schema.yaml` plus `schema_overlay.yaml`.
20mod generated;
21
22/// A shared, immutable view of the vendored schema's defaults.
23mod schema_defaults;
24
25/// The translation error type recorded by the translator and surfaced by the witness driver.
26mod translate_error;
27
28pub use cast_de::cast_to_string;
29pub use env_decode::EnvDecode;
30pub use env_provider::DatadogEnvProvider;
31pub use env_reader::{apply_datadog_env, apply_datadog_env_vars, apply_env_at_path, datadog_leaf_paths, EnvKey};
32pub use generated::{drive, DatadogConfigWitness, DatadogConfiguration};
33pub use translate_error::{TranslateError, TranslateErrors};
34
35#[cfg(test)]
36mod string_list_shape_tests {
37    use super::DatadogConfiguration;
38
39    // A string-list leaf must accept both shapes the config sources produce: a real sequence (from a
40    // file or the remote Agent stream) and a single space-separated string (from an environment
41    // variable, e.g. `DD_DOGSTATSD_TAGS="env:prod team:core"`). The generated deserializer wires the
42    // shape-tolerant reader onto every `Vec<String>` leaf; these assertions guard that wiring so a
43    // regenerate that drops it fails loudly instead of crashing config load on the string form.
44
45    #[test]
46    fn string_list_leaf_accepts_a_space_separated_string() {
47        let config: DatadogConfiguration =
48            serde_json::from_value(serde_json::json!({ "dogstatsd_tags": "env:prod team:core" }))
49                .expect("space-separated string deserializes into the string-list leaf");
50        assert_eq!(config.dogstatsd_tags, vec!["env:prod", "team:core"]);
51    }
52
53    #[test]
54    fn string_list_leaf_accepts_a_sequence() {
55        let config: DatadogConfiguration =
56            serde_json::from_value(serde_json::json!({ "dogstatsd_tags": ["env:prod", "team:core"] }))
57                .expect("sequence deserializes into the string-list leaf");
58        assert_eq!(config.dogstatsd_tags, vec!["env:prod", "team:core"]);
59    }
60}
61
62#[cfg(test)]
63mod string_map_list_shape_tests {
64    use super::DatadogConfiguration;
65
66    #[test]
67    fn additional_endpoints_accept_scalar_values() {
68        let config: DatadogConfiguration = serde_json::from_value(serde_json::json!({
69            "additional_endpoints": {
70                "https://agent.datadoghq.com.": "ENC[vault://api-key]"
71            }
72        }))
73        .expect("scalar additional endpoint API key deserializes");
74
75        assert_eq!(
76            config.additional_endpoints["https://agent.datadoghq.com."],
77            ["ENC[vault://api-key]"]
78        );
79    }
80
81    #[test]
82    fn additional_endpoints_accept_sequence_values() {
83        let config: DatadogConfiguration = serde_json::from_value(serde_json::json!({
84            "additional_endpoints": {
85                "https://agent.datadoghq.com.": ["first", "second"]
86            }
87        }))
88        .expect("additional endpoint API key sequence deserializes");
89
90        assert_eq!(
91            config.additional_endpoints["https://agent.datadoghq.com."],
92            ["first", "second"]
93        );
94    }
95}
96
97#[cfg(test)]
98mod scalar_shape_tests {
99    use serde_json::{json, Value};
100
101    use super::env_decode::EnvDecode;
102    use super::generated::env_keys::DATADOG_ENV_KEYS;
103    use super::DatadogConfiguration;
104
105    // The Agent reads a setting by casting whatever is stored to the accessor's type, so a leaf must
106    // accept more than the JSON type its schema declares. These assertions cover the wiring generated
107    // for that (`crate::cast_de`), so a regenerate that drops it fails here instead of rejecting a
108    // configuration the Agent accepts — which, at the strict startup gate, means ADP fails to boot.
109
110    #[test]
111    fn boolean_leaf_accepts_a_boolean_string() {
112        let config: DatadogConfiguration = serde_json::from_value(json!({ "dogstatsd_non_local_traffic": "true" }))
113            .expect("boolean string deserializes");
114        assert!(config.dogstatsd_non_local_traffic);
115    }
116
117    #[test]
118    fn integer_leaf_accepts_a_numeric_string() {
119        let config: DatadogConfiguration =
120            serde_json::from_value(json!({ "dogstatsd_port": "8125" })).expect("numeric string deserializes");
121        assert_eq!(config.dogstatsd_port, 8125);
122    }
123
124    #[test]
125    fn string_leaf_accepts_a_boolean() {
126        // The Agent reads this leaf with `GetString`, so a YAML boolean reaches it as `"true"`.
127        let config: DatadogConfiguration =
128            serde_json::from_value(json!({ "use_v3_api": { "series": { "enabled": true } } }))
129                .expect("boolean V3 series mode deserializes");
130        assert_eq!(config.use_v3_api.series.enabled, "true");
131    }
132
133    #[test]
134    fn string_leaf_accepts_a_numeric_byte_count() {
135        // A byte size is schema-typed as a string but documented as a bare byte count as well.
136        let config: DatadogConfiguration = serde_json::from_value(json!({ "dogstatsd_log_file_max_size": 10485760 }))
137            .expect("byte count deserializes");
138        assert_eq!(config.dogstatsd_log_file_max_size, "10485760");
139    }
140
141    #[test]
142    fn every_scalar_leaf_accepts_the_agent_castable_form_of_its_type() {
143        // The environment table is the runtime inventory of leaves with their declared types, so this
144        // reaches every scalar leaf rather than the handful spelled out above.
145        let defaults = serde_json::to_value(DatadogConfiguration::default()).expect("defaults serialize");
146
147        for key in DATADOG_ENV_KEYS {
148            let pointer = format!("/{}", key.path.join("/"));
149            let current = defaults.pointer(&pointer);
150
151            // A boolean, integer, or float leaf must accept its string spelling (how it arrives from an
152            // environment variable, and how an operator may write it in YAML); a string leaf must accept
153            // a boolean. Each written value differs from the leaf's default, so a coercion that silently
154            // failed to land cannot be mistaken for one that worked.
155            let (written, expected) = match key.decode {
156                EnvDecode::Bool => {
157                    let flipped = !current.and_then(Value::as_bool).unwrap_or(false);
158                    (json!(flipped.to_string()), json!(flipped))
159                }
160                EnvDecode::Integer => {
161                    let bumped = current.and_then(Value::as_i64).unwrap_or(0) + 1;
162                    (json!(bumped.to_string()), json!(bumped))
163                }
164                EnvDecode::Float => {
165                    let bumped = current.and_then(Value::as_f64).unwrap_or(0.0) + 1.5;
166                    (json!(bumped.to_string()), json!(bumped))
167                }
168                EnvDecode::RawString => {
169                    let flag = current.and_then(Value::as_str) != Some("true");
170                    (json!(flag), json!(flag.to_string()))
171                }
172                _ => continue,
173            };
174
175            let mut tree = written.clone();
176            for segment in key.path.iter().rev() {
177                tree = json!({ *segment: tree });
178            }
179
180            let leaf = key.path.join(".");
181            let config: DatadogConfiguration =
182                serde_json::from_value(tree).unwrap_or_else(|e| panic!("leaf `{leaf}` rejected {written}: {e}"));
183            let coerced = serde_json::to_value(config).expect("the configuration serializes");
184            assert_eq!(
185                coerced.pointer(&pointer),
186                Some(&expected),
187                "leaf `{leaf}` did not coerce {written}"
188            );
189        }
190    }
191
192    #[test]
193    fn a_malformed_scalar_is_still_rejected() {
194        // Permissive is not unconditional: a value the Agent's cast cannot convert must fail here
195        // rather than silently reading as the type's zero value, which is what the Agent does.
196        for malformed in [
197            json!({ "dogstatsd_non_local_traffic": "yes" }),
198            json!({ "dogstatsd_port": "8125ms" }),
199            json!({ "dogstatsd_log_file_max_size": ["10MB"] }),
200        ] {
201            let result: Result<DatadogConfiguration, _> = serde_json::from_value(malformed.clone());
202            assert!(result.is_err(), "{malformed} should be rejected");
203        }
204    }
205
206    #[test]
207    fn a_null_scalar_reads_as_the_type_zero_value() {
208        let config: DatadogConfiguration =
209            serde_json::from_value(json!({ "api_key": Value::Null })).expect("an explicitly null leaf deserializes");
210        assert_eq!(config.api_key, "");
211    }
212}