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