datadog_agent_config/classifier/
classifier.rs1use std::collections::HashMap;
2
3use serde_json::Value;
4
5use super::{ClassifierEntry, DefaultValue, PipelineAffinity, SupportLevel, CLASSIFIER_ENTRIES};
6
7pub struct Classification {
9 pub support_level: SupportLevel,
11 pub is_default: bool,
13 pub pipeline_affinity: PipelineAffinity,
15}
16
17pub struct ConfigClassifier {
23 lookup: HashMap<&'static str, &'static ClassifierEntry>,
24}
25
26impl ConfigClassifier {
27 pub fn new() -> Self {
29 let mut lookup = HashMap::new();
30
31 for entry in CLASSIFIER_ENTRIES {
32 lookup.insert(entry.yaml_path, entry);
33 for alias in entry.aliases {
34 lookup.insert(alias, entry);
35 }
36 }
37
38 Self { lookup }
39 }
40
41 pub fn classify(&self, key: &str, value: &Value) -> Option<Classification> {
45 let entry = self.lookup.get(key)?;
46 Some(Classification {
47 support_level: entry.support_level,
48 is_default: is_default_value(entry.default, value),
49 pipeline_affinity: entry.pipeline_affinity,
50 })
51 }
52}
53
54fn is_default_value(default: DefaultValue, value: &Value) -> bool {
55 match default {
56 DefaultValue::Json(default_str) => serde_json::from_str::<Value>(default_str)
57 .map(|default_value| *value == default_value)
58 .unwrap_or(false),
59
60 DefaultValue::DurationNanos(default_ns) => duration_value_as_nanos(value)
64 .map(|value_ns| value_ns == default_ns)
65 .unwrap_or(false),
66
67 DefaultValue::Missing => match value {
68 Value::Null => true,
69 Value::String(s) => s.is_empty(),
70 _ => false,
71 },
72 }
73}
74
75fn duration_value_as_nanos(value: &Value) -> Option<u64> {
81 match value {
82 Value::Number(n) => n.as_u64(),
83 Value::String(s) => go_duration::parse_duration(s).ok().map(|d| d.as_nanos() as u64),
84 _ => None,
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::classifier::Severity;
92
93 fn classifier() -> ConfigClassifier {
94 ConfigClassifier::new()
95 }
96
97 #[test]
98 fn full_support_keys_not_in_classifier() {
99 let c = classifier();
102 assert!(c.classify("dogstatsd_port", &Value::Number(9999.into())).is_none());
103 assert!(c.classify("log_payloads", &Value::Bool(true)).is_none());
104 }
105
106 #[test]
107 fn partial_non_default() {
108 let c = classifier();
109 let result = c
110 .classify("min_tls_version", &Value::String("non_default_value".into()))
111 .unwrap();
112 assert_eq!(result.support_level, SupportLevel::Partial);
113 }
114
115 #[test]
116 fn incompatible_non_default() {
117 let c = classifier();
118 let result = c.classify("tls_handshake_timeout", &Value::Number(999.into())).unwrap();
119 assert!(matches!(result.support_level, SupportLevel::Incompatible(_)));
120 assert!(!result.is_default);
121 }
122
123 #[test]
124 fn incompatible_default() {
125 let c = classifier();
126 let result = c.classify("config_id", &Value::String("".into())).unwrap();
128 assert!(matches!(result.support_level, SupportLevel::Incompatible(_)));
129 assert!(result.is_default);
130 }
131
132 #[test]
133 fn not_in_registry_returns_none() {
134 let c = classifier();
135 assert!(c.classify("totally_made_up_key", &Value::Bool(true)).is_none());
136 assert!(c.classify("GUI_host", &Value::String("localhost".into())).is_none());
137 }
138
139 #[test]
140 fn duration_default_null_is_not_default() {
141 let c = classifier();
142 let result = c.classify("tls_handshake_timeout", &Value::Null).unwrap();
144 assert!(!result.is_default);
145 }
146
147 #[test]
148 fn duration_default_non_duration_string_is_not_default() {
149 let c = classifier();
150 assert!(
152 !c.classify("tls_handshake_timeout", &Value::String("".into()))
153 .unwrap()
154 .is_default
155 );
156 assert!(
157 !c.classify("tls_handshake_timeout", &Value::String("something".into()))
158 .unwrap()
159 .is_default
160 );
161 }
162
163 #[test]
164 fn duration_default_matches_go_duration_string() {
165 let c = classifier();
166 let result = c
168 .classify("tls_handshake_timeout", &Value::String("10s".into()))
169 .unwrap();
170 assert!(result.is_default);
171 }
172
173 #[test]
174 fn incompatible_severity_levels() {
175 let c = classifier();
176 let result = c.classify("tls_handshake_timeout", &Value::Number(30.into())).unwrap();
177 assert!(matches!(
178 result.support_level,
179 SupportLevel::Incompatible(Severity::Medium)
180 ));
181 }
182
183 #[test]
184 fn duration_default_as_nanoseconds_is_default() {
185 let c = classifier();
186 let result = c
189 .classify("tls_handshake_timeout", &Value::Number(10_000_000_000i64.into()))
190 .unwrap();
191 assert!(result.is_default);
192 }
193
194 #[test]
195 fn duration_non_default_nanoseconds_is_not_default() {
196 let c = classifier();
197 let result = c
199 .classify("tls_handshake_timeout", &Value::Number(5_000_000_000i64.into()))
200 .unwrap();
201 assert!(!result.is_default);
202 }
203
204 #[test]
205 fn is_default_value_by_variant() {
206 assert!(is_default_value(
208 DefaultValue::Json("\"tlsv1.2\""),
209 &Value::String("tlsv1.2".into())
210 ));
211 assert!(!is_default_value(
212 DefaultValue::Json("\"tlsv1.2\""),
213 &Value::String("tlsv1.3".into())
214 ));
215 assert!(is_default_value(DefaultValue::Json("1"), &Value::Number(1.into())));
216
217 assert!(is_default_value(
219 DefaultValue::DurationNanos(10_000_000_000),
220 &Value::Number(10_000_000_000u64.into())
221 ));
222 assert!(is_default_value(
223 DefaultValue::DurationNanos(10_000_000_000),
224 &Value::String("10s".into())
225 ));
226 assert!(!is_default_value(
227 DefaultValue::DurationNanos(10_000_000_000),
228 &Value::Number(5_000_000_000u64.into())
229 ));
230 assert!(!is_default_value(
231 DefaultValue::DurationNanos(10_000_000_000),
232 &Value::String("nope".into())
233 ));
234
235 assert!(is_default_value(DefaultValue::Missing, &Value::Null));
237 assert!(is_default_value(DefaultValue::Missing, &Value::String("".into())));
238 assert!(!is_default_value(DefaultValue::Missing, &Value::String("x".into())));
239 }
240}