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
119 .classify("dogstatsd_stats_buffer", &Value::Number(999.into()))
120 .unwrap();
121 assert!(matches!(result.support_level, SupportLevel::Incompatible(_)));
122 assert!(!result.is_default);
123 }
124
125 #[test]
126 fn incompatible_default() {
127 let c = classifier();
128 let result = c.classify("config_id", &Value::String("".into())).unwrap();
130 assert!(matches!(result.support_level, SupportLevel::Incompatible(_)));
131 assert!(result.is_default);
132 }
133
134 #[test]
135 fn not_in_registry_returns_none() {
136 let c = classifier();
137 assert!(c.classify("totally_made_up_key", &Value::Bool(true)).is_none());
138 assert!(c.classify("GUI_host", &Value::String("localhost".into())).is_none());
139 }
140
141 #[test]
142 fn duration_default_null_is_not_default() {
143 let c = classifier();
144 let result = c
146 .classify("dogstatsd_packet_buffer_flush_timeout", &Value::Null)
147 .unwrap();
148 assert!(!result.is_default);
149 }
150
151 #[test]
152 fn duration_default_non_duration_string_is_not_default() {
153 let c = classifier();
154 assert!(
156 !c.classify("dogstatsd_packet_buffer_flush_timeout", &Value::String("".into()))
157 .unwrap()
158 .is_default
159 );
160 assert!(
161 !c.classify(
162 "dogstatsd_packet_buffer_flush_timeout",
163 &Value::String("something".into())
164 )
165 .unwrap()
166 .is_default
167 );
168 }
169
170 #[test]
171 fn duration_default_matches_go_duration_string() {
172 let c = classifier();
173 let result = c
175 .classify("dogstatsd_packet_buffer_flush_timeout", &Value::String("100ms".into()))
176 .unwrap();
177 assert!(result.is_default);
178 }
179
180 #[test]
181 fn incompatible_severity_levels() {
182 let c = classifier();
183 let result = c.classify("dogstatsd_stats_buffer", &Value::Number(30.into())).unwrap();
184 assert!(matches!(
185 result.support_level,
186 SupportLevel::Incompatible(Severity::Medium)
187 ));
188 }
189
190 #[test]
191 fn duration_default_as_nanoseconds_is_default() {
192 let c = classifier();
193 let result = c
196 .classify(
197 "dogstatsd_packet_buffer_flush_timeout",
198 &Value::Number(100_000_000i64.into()),
199 )
200 .unwrap();
201 assert!(result.is_default);
202 }
203
204 #[test]
205 fn duration_non_default_nanoseconds_is_not_default() {
206 let c = classifier();
207 let result = c
209 .classify(
210 "dogstatsd_packet_buffer_flush_timeout",
211 &Value::Number(5_000_000i64.into()),
212 )
213 .unwrap();
214 assert!(!result.is_default);
215 }
216
217 #[test]
218 fn is_default_value_by_variant() {
219 assert!(is_default_value(
221 DefaultValue::Json("\"tlsv1.2\""),
222 &Value::String("tlsv1.2".into())
223 ));
224 assert!(!is_default_value(
225 DefaultValue::Json("\"tlsv1.2\""),
226 &Value::String("tlsv1.3".into())
227 ));
228 assert!(is_default_value(DefaultValue::Json("1"), &Value::Number(1.into())));
229
230 assert!(is_default_value(
232 DefaultValue::DurationNanos(10_000_000_000),
233 &Value::Number(10_000_000_000u64.into())
234 ));
235 assert!(is_default_value(
236 DefaultValue::DurationNanos(10_000_000_000),
237 &Value::String("10s".into())
238 ));
239 assert!(!is_default_value(
240 DefaultValue::DurationNanos(10_000_000_000),
241 &Value::Number(5_000_000_000u64.into())
242 ));
243 assert!(!is_default_value(
244 DefaultValue::DurationNanos(10_000_000_000),
245 &Value::String("nope".into())
246 ));
247
248 assert!(is_default_value(DefaultValue::Missing, &Value::Null));
250 assert!(is_default_value(DefaultValue::Missing, &Value::String("".into())));
251 assert!(!is_default_value(DefaultValue::Missing, &Value::String("x".into())));
252 }
253}