datadog_agent_config/
lib.rs1mod cast_de;
3
4pub mod classifier;
5
6mod duration_de;
7
8pub mod env_decode;
10mod list_de;
11
12pub mod env_provider;
14
15pub mod env_reader;
18
19mod generated;
21
22mod platform_default;
24
25mod schema_defaults;
27
28mod size_in_bytes;
30
31mod translate_error;
33
34pub use cast_de::cast_to_f64;
35pub use cast_de::cast_to_string;
36pub use env_decode::EnvDecode;
37pub use env_provider::DatadogEnvProvider;
38pub use env_reader::{apply_datadog_env, apply_datadog_env_vars, apply_env_at_path, datadog_leaf_paths, EnvKey};
39pub use generated::{drive, DatadogConfigWitness, DatadogConfiguration};
40pub use platform_default::is_containerized;
41pub use size_in_bytes::parse_size_in_bytes;
42pub use translate_error::{TranslateError, TranslateErrors};
43
44#[cfg(test)]
45mod string_list_shape_tests {
46 use super::DatadogConfiguration;
47
48 #[test]
55 fn string_list_leaf_accepts_a_space_separated_string() {
56 let config: DatadogConfiguration =
57 serde_json::from_value(serde_json::json!({ "dogstatsd_tags": "env:prod team:core" }))
58 .expect("space-separated string deserializes into the string-list leaf");
59 assert_eq!(config.dogstatsd_tags, vec!["env:prod", "team:core"]);
60 }
61
62 #[test]
63 fn string_list_leaf_accepts_a_json_encoded_list() {
64 let config: DatadogConfiguration = serde_json::from_value(serde_json::json!({
65 "apm_config": { "obfuscation": { "elasticsearch": { "keep_values": r#"["cr-a","cr-b"]"# } } }
66 }))
67 .expect("JSON-encoded list deserializes into the string-list leaf");
68 assert_eq!(
69 config.apm_config.obfuscation.elasticsearch.keep_values,
70 vec!["cr-a", "cr-b"]
71 );
72 }
73
74 #[test]
75 fn string_list_leaf_accepts_a_sequence() {
76 let config: DatadogConfiguration =
77 serde_json::from_value(serde_json::json!({ "dogstatsd_tags": ["env:prod", "team:core"] }))
78 .expect("sequence deserializes into the string-list leaf");
79 assert_eq!(config.dogstatsd_tags, vec!["env:prod", "team:core"]);
80 }
81}
82
83#[cfg(test)]
84mod string_map_list_shape_tests {
85 use super::DatadogConfiguration;
86
87 #[test]
88 fn additional_endpoints_accept_scalar_values() {
89 let config: DatadogConfiguration = serde_json::from_value(serde_json::json!({
90 "additional_endpoints": {
91 "https://agent.datadoghq.com.": "ENC[vault://api-key]"
92 }
93 }))
94 .expect("scalar additional endpoint API key deserializes");
95
96 assert_eq!(
97 config.additional_endpoints["https://agent.datadoghq.com."],
98 ["ENC[vault://api-key]"]
99 );
100 }
101
102 #[test]
103 fn additional_endpoints_accept_sequence_values() {
104 let config: DatadogConfiguration = serde_json::from_value(serde_json::json!({
105 "additional_endpoints": {
106 "https://agent.datadoghq.com.": ["first", "second"]
107 }
108 }))
109 .expect("additional endpoint API key sequence deserializes");
110
111 assert_eq!(
112 config.additional_endpoints["https://agent.datadoghq.com."],
113 ["first", "second"]
114 );
115 }
116}
117
118#[cfg(test)]
119mod scalar_shape_tests {
120 use serde_json::{json, Value};
121
122 use super::env_decode::EnvDecode;
123 use super::generated::env_keys::DATADOG_ENV_KEYS;
124 use super::DatadogConfiguration;
125
126 #[test]
132 fn boolean_leaf_accepts_a_boolean_string() {
133 let config: DatadogConfiguration = serde_json::from_value(json!({ "dogstatsd_non_local_traffic": "true" }))
134 .expect("boolean string deserializes");
135 assert!(config.dogstatsd_non_local_traffic);
136 }
137
138 #[test]
139 fn number_map_leaf_accepts_numeric_string_values() {
140 let config: DatadogConfiguration = serde_json::from_value(json!({
141 "apm_config": { "analyzed_rate_by_service": { "service": "0.8" } }
142 }))
143 .expect("numeric string map deserializes");
144 assert_eq!(config.apm_config.analyzed_rate_by_service["service"], 0.8);
145 }
146
147 #[test]
148 fn integer_leaf_accepts_a_numeric_string() {
149 let config: DatadogConfiguration =
150 serde_json::from_value(json!({ "dogstatsd_port": "8125" })).expect("numeric string deserializes");
151 assert_eq!(config.dogstatsd_port, 8125);
152 }
153
154 #[test]
155 fn integer_leaf_reads_a_quoted_zero_padded_number_as_octal() {
156 let config: DatadogConfiguration =
157 serde_json::from_value(json!({ "dogstatsd_buffer_size": "010" })).expect("numeric string deserializes");
158 assert_eq!(config.dogstatsd_buffer_size, 8);
159 }
160
161 #[test]
162 fn string_leaf_accepts_a_boolean() {
163 let config: DatadogConfiguration =
165 serde_json::from_value(json!({ "use_v3_api": { "series": { "enabled": true } } }))
166 .expect("boolean V3 series mode deserializes");
167 assert_eq!(config.use_v3_api.series.enabled, "true");
168 }
169
170 #[test]
171 fn string_leaf_accepts_a_numeric_byte_count() {
172 let config: DatadogConfiguration = serde_json::from_value(json!({ "dogstatsd_log_file_max_size": 10485760 }))
174 .expect("byte count deserializes");
175 assert_eq!(config.dogstatsd_log_file_max_size, "10485760");
176 }
177
178 #[test]
179 fn every_scalar_leaf_accepts_the_agent_castable_form_of_its_type() {
180 let defaults = serde_json::to_value(DatadogConfiguration::default()).expect("defaults serialize");
183
184 for key in DATADOG_ENV_KEYS {
185 let pointer = format!("/{}", key.path.join("/"));
186 let current = defaults.pointer(&pointer);
187
188 let cases = match key.decode {
194 EnvDecode::Bool => {
195 let flipped = !current.and_then(Value::as_bool).unwrap_or(false);
196 vec![(json!(flipped.to_string()), json!(flipped))]
197 }
198 EnvDecode::Integer => {
199 let bumped = current.and_then(Value::as_i64).unwrap_or(0) + 1;
200 let sign = if bumped < 0 { "-" } else { "" };
201 let hex = format!("{sign}{:#x}", bumped.unsigned_abs());
202 vec![(json!(bumped.to_string()), json!(bumped)), (json!(hex), json!(bumped))]
203 }
204 EnvDecode::Float => {
205 let bumped = current.and_then(Value::as_f64).unwrap_or(0.0) + 1.5;
206 vec![(json!(bumped.to_string()), json!(bumped))]
207 }
208 EnvDecode::RawString => {
209 let flag = current.and_then(Value::as_str) != Some("true");
210 vec![(json!(flag), json!(flag.to_string()))]
211 }
212 _ => continue,
213 };
214
215 for (written, expected) in cases {
216 let mut tree = written.clone();
217 for segment in key.path.iter().rev() {
218 tree = json!({ *segment: tree });
219 }
220
221 let leaf = key.path.join(".");
222 let config: DatadogConfiguration =
223 serde_json::from_value(tree).unwrap_or_else(|e| panic!("leaf `{leaf}` rejected {written}: {e}"));
224 let coerced = serde_json::to_value(config).expect("the configuration serializes");
225 assert_eq!(
226 coerced.pointer(&pointer),
227 Some(&expected),
228 "leaf `{leaf}` did not coerce {written}"
229 );
230 }
231 }
232 }
233
234 #[test]
235 fn a_malformed_scalar_is_still_rejected() {
236 for malformed in [
239 json!({ "dogstatsd_non_local_traffic": "yes" }),
240 json!({ "dogstatsd_port": "8125ms" }),
241 json!({ "dogstatsd_log_file_max_size": ["10MB"] }),
242 ] {
243 let result: Result<DatadogConfiguration, _> = serde_json::from_value(malformed.clone());
244 assert!(result.is_err(), "{malformed} should be rejected");
245 }
246 }
247
248 #[test]
249 fn a_null_scalar_reads_as_the_type_zero_value() {
250 let config: DatadogConfiguration =
251 serde_json::from_value(json!({ "api_key": Value::Null })).expect("an explicitly null leaf deserializes");
252 assert_eq!(config.api_key, "");
253 }
254}
255
256#[cfg(test)]
257mod null_collection_tests {
258 use serde_json::{json, Value};
259
260 use super::env_decode::EnvDecode;
261 use super::generated::env_keys::DATADOG_ENV_KEYS;
262 use super::DatadogConfiguration;
263
264 #[test]
268 fn every_collection_leaf_accepts_null() {
269 let collections = DATADOG_ENV_KEYS.iter().filter(|key| {
271 !matches!(
272 key.decode,
273 EnvDecode::Bool
274 | EnvDecode::Integer
275 | EnvDecode::Float
276 | EnvDecode::RawString
277 | EnvDecode::DurationString
278 )
279 });
280
281 for key in collections {
282 let mut tree = Value::Null;
283 for segment in key.path.iter().rev() {
284 tree = json!({ *segment: tree });
285 }
286
287 let leaf = key.path.join(".");
288 serde_json::from_value::<DatadogConfiguration>(tree)
289 .unwrap_or_else(|e| panic!("leaf `{leaf}` rejected null: {e}"));
290 }
291 }
292
293 #[test]
294 fn a_null_string_list_is_empty_while_an_absent_one_keeps_its_default() {
295 let absent: DatadogConfiguration = serde_json::from_value(json!({})).expect("empty configuration deserializes");
296 assert!(
297 !absent.histogram_aggregates.is_empty(),
298 "the schema default is not empty"
299 );
300
301 let null: DatadogConfiguration =
302 serde_json::from_value(json!({ "histogram_aggregates": null, "proxy": { "no_proxy": null } }))
303 .expect("null string lists deserialize");
304 assert!(null.histogram_aggregates.is_empty());
305 assert!(null.proxy.no_proxy.is_empty());
306 }
307
308 #[test]
309 fn null_maps_and_object_arrays_are_empty() {
310 let config: DatadogConfiguration = serde_json::from_value(json!({
311 "additional_endpoints": null,
312 "dogstatsd_mapper_profiles": null,
313 "apm_config": { "replace_tags": null },
314 "use_v3_api": { "series": { "endpoints": null } }
315 }))
316 .expect("null collections deserialize");
317
318 assert!(config.additional_endpoints.is_empty());
319 assert!(config.dogstatsd_mapper_profiles.is_empty());
320 assert!(config.apm_config.replace_tags.is_empty());
321 assert!(config.use_v3_api.series.endpoints.is_empty());
322 }
323
324 #[test]
325 fn a_null_replace_tags_rule_is_an_empty_map() {
326 let config: DatadogConfiguration =
327 serde_json::from_value(json!({ "apm_config": { "replace_tags": [null, { "name": "*" }] } }))
328 .expect("a null rule deserializes");
329
330 assert_eq!(config.apm_config.replace_tags.len(), 2);
331 assert!(config.apm_config.replace_tags[0].is_empty());
332 assert_eq!(config.apm_config.replace_tags[1]["name"], "*");
333 }
334}