Skip to main content

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/// Runtime selection of a schema `platform_default` value, called by generated default functions.
23mod platform_default;
24
25/// A shared, immutable view of the vendored schema's defaults.
26mod schema_defaults;
27
28/// Parses a byte-size setting the way the Agent's `GetSizeInBytes` does.
29mod size_in_bytes;
30
31/// The translation error type recorded by the translator and surfaced by the witness driver.
32mod 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    // A string-list leaf must accept both shapes the config sources produce: a real sequence (from a
49    // file or the remote Agent stream) and a single space-separated string (from an environment
50    // variable, e.g. `DD_DOGSTATSD_TAGS="env:prod team:core"`). The generated deserializer wires the
51    // shape-tolerant reader onto every `Vec<String>` leaf; these assertions guard that wiring so a
52    // regenerate that drops it fails loudly instead of crashing config load on the string form.
53
54    #[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    // The Agent reads a setting by casting whatever is stored to the accessor's type, so a leaf must
127    // accept more than the JSON type its schema declares. These assertions cover the wiring generated
128    // for that (`crate::cast_de`), so a regenerate that drops it fails here instead of rejecting a
129    // configuration the Agent accepts — which, at the strict startup gate, means ADP fails to boot.
130
131    #[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        // The Agent reads this leaf with `GetString`, so a YAML boolean reaches it as `"true"`.
164        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        // A byte size is schema-typed as a string but documented as a bare byte count as well.
173        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        // The environment table is the runtime inventory of leaves with their declared types, so this
181        // reaches every scalar leaf rather than the handful spelled out above.
182        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            // A boolean, integer, or float leaf must accept its string spelling (how it arrives from an
189            // environment variable, and how an operator may write it in YAML); a string leaf must accept
190            // a boolean. An integer leaf must also accept the base-prefixed spelling the Agent's integer
191            // cast reads. Each written value differs from the leaf's default, so a coercion that silently
192            // failed to land cannot be mistaken for one that worked.
193            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        // Permissive is not unconditional: a value the Agent's cast cannot convert must fail here
237        // rather than silently reading as the type's zero value, which is what the Agent does.
238        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    // The Agent's configuration stream carries `null` for an empty or cleared list or map, and the
265    // Agent's accessors read it as empty. A key that is absent is different: it keeps the schema default.
266
267    #[test]
268    fn every_collection_leaf_accepts_null() {
269        // Scalar leaves are covered by `scalar_shape_tests`; durations are not collections.
270        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}