1pub const MIN_SAMPLES_FOR_LEARNING: usize = 10;
18
19pub const DEFAULT_CONFIDENCE_LEVEL: f64 = 0.95;
21
22pub const DEFAULT_OUTLIER_THRESHOLD: f64 = 3.0;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ThresholdDirection {
28 Upper,
30 Lower,
32 Both,
34}
35
36impl ThresholdDirection {
37 pub fn name(&self) -> &'static str {
39 match self {
40 Self::Upper => "upper",
41 Self::Lower => "lower",
42 Self::Both => "both",
43 }
44 }
45}
46
47#[derive(Debug, Clone)]
49pub struct LearnedThreshold {
50 pub metric: String,
52 pub mean: f64,
54 pub std_dev: f64,
56 pub sample_count: usize,
58 pub lower_bound: f64,
60 pub upper_bound: f64,
62 pub lower_critical: f64,
64 pub upper_critical: f64,
66 pub cv: f64,
68 pub confidence_level: f64,
70 pub direction: ThresholdDirection,
72}
73
74impl LearnedThreshold {
75 pub fn is_warning(&self, value: f64) -> bool {
77 match self.direction {
78 ThresholdDirection::Upper => value > self.upper_bound,
79 ThresholdDirection::Lower => value < self.lower_bound,
80 ThresholdDirection::Both => value < self.lower_bound || value > self.upper_bound,
81 }
82 }
83
84 pub fn is_critical(&self, value: f64) -> bool {
86 match self.direction {
87 ThresholdDirection::Upper => value > self.upper_critical,
88 ThresholdDirection::Lower => value < self.lower_critical,
89 ThresholdDirection::Both => value < self.lower_critical || value > self.upper_critical,
90 }
91 }
92
93 pub fn is_normal(&self, value: f64) -> bool {
95 !self.is_warning(value)
96 }
97
98 pub fn to_json(&self) -> String {
100 format!(
101 r#"{{"metric":"{}","mean":{},"std_dev":{},"cv":{},"lower_bound":{},"upper_bound":{},"sample_count":{}}}"#,
102 self.metric,
103 self.mean,
104 self.std_dev,
105 self.cv,
106 self.lower_bound,
107 self.upper_bound,
108 self.sample_count
109 )
110 }
111}
112
113#[derive(Debug)]
115pub struct ThresholdLearner {
116 metric: String,
118 samples: Vec<f64>,
120 max_samples: usize,
122 outlier_threshold: f64,
124 confidence_level: f64,
126 warning_multiplier: f64,
128 critical_multiplier: f64,
130 direction: ThresholdDirection,
132 override_value: Option<f64>,
134}
135
136impl ThresholdLearner {
137 pub fn new(metric: &str) -> Self {
139 Self {
140 metric: metric.to_string(),
141 samples: Vec::new(),
142 max_samples: 1000,
143 outlier_threshold: DEFAULT_OUTLIER_THRESHOLD,
144 confidence_level: DEFAULT_CONFIDENCE_LEVEL,
145 warning_multiplier: 2.0,
146 critical_multiplier: 3.0,
147 direction: ThresholdDirection::Upper,
148 override_value: None,
149 }
150 }
151
152 pub fn with_direction(mut self, direction: ThresholdDirection) -> Self {
154 self.direction = direction;
155 self
156 }
157
158 pub fn with_warning_multiplier(mut self, multiplier: f64) -> Self {
160 self.warning_multiplier = multiplier.max(0.5);
161 self
162 }
163
164 pub fn with_critical_multiplier(mut self, multiplier: f64) -> Self {
166 self.critical_multiplier = multiplier.max(1.0);
167 self
168 }
169
170 pub fn with_outlier_threshold(mut self, threshold: f64) -> Self {
172 self.outlier_threshold = threshold.max(2.0);
173 self
174 }
175
176 pub fn with_max_samples(mut self, max: usize) -> Self {
178 self.max_samples = max.max(10);
179 self
180 }
181
182 pub fn with_override(mut self, value: f64) -> Self {
184 self.override_value = Some(value);
185 self
186 }
187
188 pub fn clear_override(&mut self) {
190 self.override_value = None;
191 }
192
193 pub fn add_sample(&mut self, value: f64) {
195 self.samples.push(value);
196
197 if self.samples.len() > self.max_samples {
199 self.samples.remove(0);
200 }
201 }
202
203 pub fn add_samples(&mut self, values: &[f64]) {
205 for &v in values {
206 self.add_sample(v);
207 }
208 }
209
210 pub fn sample_count(&self) -> usize {
212 self.samples.len()
213 }
214
215 pub fn has_sufficient_samples(&self) -> bool {
217 self.samples.len() >= MIN_SAMPLES_FOR_LEARNING
218 }
219
220 fn mean(data: &[f64]) -> f64 {
222 if data.is_empty() {
223 return 0.0;
224 }
225 data.iter().sum::<f64>() / data.len() as f64
226 }
227
228 fn std_dev(data: &[f64], mean: f64) -> f64 {
230 if data.len() < 2 {
231 return 0.0;
232 }
233 let variance =
234 data.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (data.len() - 1) as f64;
235 variance.sqrt()
236 }
237
238 pub fn filter_outliers(&self) -> Vec<f64> {
240 if self.samples.len() < 3 {
241 return self.samples.clone();
242 }
243
244 let mean = Self::mean(&self.samples);
245 let std_dev = Self::std_dev(&self.samples, mean);
246
247 if std_dev < 1e-10 {
248 return self.samples.clone();
249 }
250
251 self.samples
252 .iter()
253 .filter(|&&x| ((x - mean) / std_dev).abs() <= self.outlier_threshold)
254 .copied()
255 .collect()
256 }
257
258 pub fn learn_baseline(&self) -> Option<LearnedThreshold> {
260 if !self.has_sufficient_samples() {
261 return None;
262 }
263
264 let filtered = self.filter_outliers();
266 if filtered.len() < MIN_SAMPLES_FOR_LEARNING {
267 return None;
268 }
269
270 let mean = Self::mean(&filtered);
271 let std_dev = Self::std_dev(&filtered, mean);
272 let cv = if mean.abs() > 1e-10 {
273 (std_dev / mean.abs()) * 100.0
274 } else {
275 0.0
276 };
277
278 let (lower_bound, upper_bound) = match self.direction {
280 ThresholdDirection::Upper => {
281 (f64::NEG_INFINITY, mean + self.warning_multiplier * std_dev)
282 }
283 ThresholdDirection::Lower => (mean - self.warning_multiplier * std_dev, f64::INFINITY),
284 ThresholdDirection::Both => (
285 mean - self.warning_multiplier * std_dev,
286 mean + self.warning_multiplier * std_dev,
287 ),
288 };
289
290 let (lower_critical, upper_critical) = match self.direction {
291 ThresholdDirection::Upper => {
292 (f64::NEG_INFINITY, mean + self.critical_multiplier * std_dev)
293 }
294 ThresholdDirection::Lower => (mean - self.critical_multiplier * std_dev, f64::INFINITY),
295 ThresholdDirection::Both => (
296 mean - self.critical_multiplier * std_dev,
297 mean + self.critical_multiplier * std_dev,
298 ),
299 };
300
301 Some(LearnedThreshold {
302 metric: self.metric.clone(),
303 mean,
304 std_dev,
305 sample_count: filtered.len(),
306 lower_bound,
307 upper_bound,
308 lower_critical,
309 upper_critical,
310 cv,
311 confidence_level: self.confidence_level,
312 direction: self.direction,
313 })
314 }
315
316 pub fn percentile_threshold(&self, percentile: f64) -> Option<f64> {
318 if self.samples.is_empty() {
319 return None;
320 }
321
322 let mut sorted = self.samples.clone();
323 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
324
325 let p = percentile.clamp(0.0, 100.0);
326 let idx = ((p / 100.0) * (sorted.len() - 1) as f64).round() as usize;
327 Some(sorted[idx.min(sorted.len() - 1)])
328 }
329
330 pub fn get_effective_threshold(&self) -> Option<f64> {
332 if let Some(override_val) = self.override_value {
333 return Some(override_val);
334 }
335
336 self.learn_baseline().map(|t| t.upper_bound)
337 }
338
339 pub fn check(&self, value: f64) -> ThresholdCheck {
341 if let Some(override_val) = self.override_value {
343 let is_exceeded = match self.direction {
344 ThresholdDirection::Upper => value > override_val,
345 ThresholdDirection::Lower => value < override_val,
346 ThresholdDirection::Both => false, };
348 return ThresholdCheck {
349 value,
350 threshold: override_val,
351 is_warning: is_exceeded,
352 is_critical: false,
353 is_override: true,
354 };
355 }
356
357 if let Some(learned) = self.learn_baseline() {
359 ThresholdCheck {
360 value,
361 threshold: learned.upper_bound,
362 is_warning: learned.is_warning(value),
363 is_critical: learned.is_critical(value),
364 is_override: false,
365 }
366 } else {
367 ThresholdCheck {
369 value,
370 threshold: 0.0,
371 is_warning: false,
372 is_critical: false,
373 is_override: false,
374 }
375 }
376 }
377
378 pub fn clear(&mut self) {
380 self.samples.clear();
381 }
382}
383
384#[derive(Debug, Clone)]
386pub struct ThresholdCheck {
387 pub value: f64,
389 pub threshold: f64,
391 pub is_warning: bool,
393 pub is_critical: bool,
395 pub is_override: bool,
397}
398
399impl ThresholdCheck {
400 pub fn passed(&self) -> bool {
402 !self.is_warning && !self.is_critical
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 #[test]
411 fn test_threshold_direction() {
412 assert_eq!(ThresholdDirection::Upper.name(), "upper");
413 assert_eq!(ThresholdDirection::Lower.name(), "lower");
414 assert_eq!(ThresholdDirection::Both.name(), "both");
415 }
416
417 #[test]
418 fn test_learner_creation() {
419 let learner = ThresholdLearner::new("cpu_temp");
420 assert_eq!(learner.sample_count(), 0);
421 assert!(!learner.has_sufficient_samples());
422 }
423
424 #[test]
425 fn test_add_samples() {
426 let mut learner = ThresholdLearner::new("test");
427 learner.add_sample(10.0);
428 learner.add_sample(11.0);
429 assert_eq!(learner.sample_count(), 2);
430 }
431
432 #[test]
433 fn test_learn_baseline() {
434 let mut learner = ThresholdLearner::new("test");
435
436 for i in 0..20 {
438 learner.add_sample(100.0 + (i % 3) as f64);
439 }
440
441 let threshold = learner.learn_baseline().unwrap();
442 assert!(threshold.mean > 99.0 && threshold.mean < 103.0);
443 assert!(threshold.std_dev > 0.0);
444 }
445
446 #[test]
447 fn test_override() {
448 let mut learner = ThresholdLearner::new("test").with_override(50.0);
449
450 for i in 0..20 {
451 learner.add_sample(100.0 + i as f64);
452 }
453
454 let effective = learner.get_effective_threshold().unwrap();
455 assert_eq!(effective, 50.0);
456 }
457
458 #[test]
459 fn test_percentile() {
460 let mut learner = ThresholdLearner::new("test");
461
462 for i in 0..100 {
463 learner.add_sample(i as f64);
464 }
465
466 let p50 = learner.percentile_threshold(50.0).unwrap();
467 assert!((p50 - 50.0).abs() < 1.0);
468 }
469}