1use core::time::Duration;
10
11pub const DEFAULT_CPU_WATCH_PERCENT: f32 = 80.0;
13pub const DEFAULT_CPU_CRITICAL_PERCENT: f32 = 95.0;
15pub const DEFAULT_MEMORY_WATCH_AVAILABLE_PERCENT: f32 = 15.0;
17pub const DEFAULT_MEMORY_CRITICAL_AVAILABLE_PERCENT: f32 = 5.0;
19pub const DEFAULT_SUSTAINED_SAMPLES: usize = 10;
21
22pub const DEFAULT_SUSTAINED_WINDOW: usize = 15;
29
30pub const MAX_SUSTAINED_WINDOW: usize = 600;
36
37pub const MIN_INTERVAL_MULTIPLE: f32 = 1.0;
39pub const MAX_INTERVAL_MULTIPLE: f32 = 1_000.0;
44
45const MIB: u64 = 1024 * 1024;
47
48#[derive(Clone, Copy, Debug, PartialEq)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60#[cfg_attr(feature = "serde", serde(default))]
61pub struct Thresholds {
62 pub enabled: bool,
69
70 pub cpu_watch_percent: f32,
72 pub cpu_critical_percent: f32,
74
75 pub memory_watch_available_percent: f32,
81 pub memory_critical_available_percent: f32,
84
85 pub sustained_samples: usize,
92 pub sustained_window: usize,
94
95 pub swap_watch_bytes_per_second: f64,
101 pub swap_critical_bytes_per_second: f64,
107
108 pub psi_watch_percent: f32,
113 pub psi_critical_percent: f32,
118
119 pub disk_busy_watch_percent: f32,
124 pub disk_busy_critical_percent: f32,
126
127 pub network_watch_percent: f32,
131 pub network_critical_percent: f32,
133
134 pub load_watch_per_cpu: f32,
139 pub load_critical_per_cpu: f32,
142
143 pub process_cpu_spike_percent: f32,
146 pub process_cpu_spike_points: f32,
149
150 pub process_rss_minimum_bytes: u64,
155 pub process_rss_growth_bytes_per_minute: u64,
161
162 pub zombie_watch_count: usize,
168
169 pub collector_lag_watch_intervals: f32,
171 pub collector_lag_critical_intervals: f32,
173
174 pub stale_watch_intervals: f32,
176 pub stale_critical_intervals: f32,
178
179 pub discontinuity_intervals: f32,
187
188 pub self_cpu_budget_percent: f32,
190 pub self_rss_budget_bytes: u64,
192 pub self_sample_budget_millis: u64,
199}
200
201impl Default for Thresholds {
202 fn default() -> Self {
204 Self {
205 enabled: true,
206 cpu_watch_percent: DEFAULT_CPU_WATCH_PERCENT,
207 cpu_critical_percent: DEFAULT_CPU_CRITICAL_PERCENT,
208 memory_watch_available_percent: DEFAULT_MEMORY_WATCH_AVAILABLE_PERCENT,
209 memory_critical_available_percent: DEFAULT_MEMORY_CRITICAL_AVAILABLE_PERCENT,
210 sustained_samples: DEFAULT_SUSTAINED_SAMPLES,
211 sustained_window: DEFAULT_SUSTAINED_WINDOW,
212 swap_watch_bytes_per_second: MIB as f64,
213 swap_critical_bytes_per_second: 16.0 * MIB as f64,
214 psi_watch_percent: 10.0,
215 psi_critical_percent: 40.0,
216 disk_busy_watch_percent: 80.0,
217 disk_busy_critical_percent: 95.0,
218 network_watch_percent: 70.0,
219 network_critical_percent: 90.0,
220 load_watch_per_cpu: 1.0,
221 load_critical_per_cpu: 2.0,
222 process_cpu_spike_percent: 100.0,
223 process_cpu_spike_points: 50.0,
224 process_rss_minimum_bytes: 128 * MIB,
225 process_rss_growth_bytes_per_minute: 64 * MIB,
226 zombie_watch_count: 8,
227 collector_lag_watch_intervals: 2.0,
228 collector_lag_critical_intervals: 5.0,
229 stale_watch_intervals: 3.0,
230 stale_critical_intervals: 10.0,
231 discontinuity_intervals: 10.0,
232 self_cpu_budget_percent: 2.0,
233 self_rss_budget_bytes: 50 * MIB,
234 self_sample_budget_millis: 200,
235 }
236 }
237}
238
239impl Thresholds {
240 #[must_use]
248 pub fn sanitized(self) -> Self {
249 let defaults = Self::default();
250 let mut out = Self {
251 enabled: self.enabled,
252 cpu_watch_percent: non_negative(self.cpu_watch_percent, defaults.cpu_watch_percent),
253 cpu_critical_percent: non_negative(
254 self.cpu_critical_percent,
255 defaults.cpu_critical_percent,
256 ),
257 memory_watch_available_percent: non_negative(
258 self.memory_watch_available_percent,
259 defaults.memory_watch_available_percent,
260 ),
261 memory_critical_available_percent: non_negative(
262 self.memory_critical_available_percent,
263 defaults.memory_critical_available_percent,
264 ),
265 sustained_samples: self.sustained_samples.clamp(1, MAX_SUSTAINED_WINDOW),
266 sustained_window: self.sustained_window.clamp(1, MAX_SUSTAINED_WINDOW),
267 swap_watch_bytes_per_second: non_negative_f64(
268 self.swap_watch_bytes_per_second,
269 defaults.swap_watch_bytes_per_second,
270 ),
271 swap_critical_bytes_per_second: non_negative_f64(
272 self.swap_critical_bytes_per_second,
273 defaults.swap_critical_bytes_per_second,
274 ),
275 psi_watch_percent: non_negative(self.psi_watch_percent, defaults.psi_watch_percent),
276 psi_critical_percent: non_negative(
277 self.psi_critical_percent,
278 defaults.psi_critical_percent,
279 ),
280 disk_busy_watch_percent: non_negative(
281 self.disk_busy_watch_percent,
282 defaults.disk_busy_watch_percent,
283 ),
284 disk_busy_critical_percent: non_negative(
285 self.disk_busy_critical_percent,
286 defaults.disk_busy_critical_percent,
287 ),
288 network_watch_percent: non_negative(
289 self.network_watch_percent,
290 defaults.network_watch_percent,
291 ),
292 network_critical_percent: non_negative(
293 self.network_critical_percent,
294 defaults.network_critical_percent,
295 ),
296 load_watch_per_cpu: non_negative(self.load_watch_per_cpu, defaults.load_watch_per_cpu),
297 load_critical_per_cpu: non_negative(
298 self.load_critical_per_cpu,
299 defaults.load_critical_per_cpu,
300 ),
301 process_cpu_spike_percent: non_negative(
302 self.process_cpu_spike_percent,
303 defaults.process_cpu_spike_percent,
304 ),
305 process_cpu_spike_points: non_negative(
306 self.process_cpu_spike_points,
307 defaults.process_cpu_spike_points,
308 ),
309 process_rss_minimum_bytes: self.process_rss_minimum_bytes,
310 process_rss_growth_bytes_per_minute: self.process_rss_growth_bytes_per_minute.max(1),
311 zombie_watch_count: self.zombie_watch_count.max(1),
312 collector_lag_watch_intervals: multiple(
313 self.collector_lag_watch_intervals,
314 defaults.collector_lag_watch_intervals,
315 ),
316 collector_lag_critical_intervals: multiple(
317 self.collector_lag_critical_intervals,
318 defaults.collector_lag_critical_intervals,
319 ),
320 stale_watch_intervals: multiple(
321 self.stale_watch_intervals,
322 defaults.stale_watch_intervals,
323 ),
324 stale_critical_intervals: multiple(
325 self.stale_critical_intervals,
326 defaults.stale_critical_intervals,
327 ),
328 discontinuity_intervals: multiple(
329 self.discontinuity_intervals,
330 defaults.discontinuity_intervals,
331 ),
332 self_cpu_budget_percent: non_negative(
333 self.self_cpu_budget_percent,
334 defaults.self_cpu_budget_percent,
335 ),
336 self_rss_budget_bytes: self.self_rss_budget_bytes.max(1),
337 self_sample_budget_millis: self.self_sample_budget_millis.max(1),
338 };
339
340 out.cpu_critical_percent = out.cpu_critical_percent.max(out.cpu_watch_percent);
343 out.psi_critical_percent = out.psi_critical_percent.max(out.psi_watch_percent);
344 out.disk_busy_critical_percent = out
345 .disk_busy_critical_percent
346 .max(out.disk_busy_watch_percent);
347 out.network_critical_percent = out.network_critical_percent.max(out.network_watch_percent);
348 out.load_critical_per_cpu = out.load_critical_per_cpu.max(out.load_watch_per_cpu);
349 out.swap_critical_bytes_per_second = out
350 .swap_critical_bytes_per_second
351 .max(out.swap_watch_bytes_per_second);
352 out.collector_lag_critical_intervals = out
353 .collector_lag_critical_intervals
354 .max(out.collector_lag_watch_intervals);
355 out.stale_critical_intervals = out.stale_critical_intervals.max(out.stale_watch_intervals);
356 out.memory_critical_available_percent = out
359 .memory_critical_available_percent
360 .min(out.memory_watch_available_percent);
361 out.sustained_window = out.sustained_window.max(out.sustained_samples);
364 out
365 }
366
367 #[must_use]
373 pub const fn minimum_samples(&self) -> usize {
374 self.sustained_samples
375 }
376
377 #[must_use]
379 pub const fn self_sample_budget(&self) -> Duration {
380 Duration::from_millis(self.self_sample_budget_millis)
381 }
382
383 #[must_use]
388 pub fn memory_watch_used_percent(&self) -> f32 {
389 (100.0 - self.memory_watch_available_percent).max(0.0)
390 }
391
392 #[must_use]
395 pub fn memory_critical_used_percent(&self) -> f32 {
396 (100.0 - self.memory_critical_available_percent).max(0.0)
397 }
398
399 #[must_use]
404 pub fn intervals_as_seconds(interval: Duration, multiple: f32) -> f64 {
405 interval.as_secs_f64()
406 * f64::from(multiple.clamp(MIN_INTERVAL_MULTIPLE, MAX_INTERVAL_MULTIPLE))
407 }
408}
409
410fn non_negative(value: f32, fallback: f32) -> f32 {
412 if value.is_finite() && value >= 0.0 {
413 value
414 } else {
415 fallback
416 }
417}
418
419fn non_negative_f64(value: f64, fallback: f64) -> f64 {
421 if value.is_finite() && value >= 0.0 {
422 value
423 } else {
424 fallback
425 }
426}
427
428fn multiple(value: f32, fallback: f32) -> f32 {
430 if value.is_finite() {
431 value.clamp(MIN_INTERVAL_MULTIPLE, MAX_INTERVAL_MULTIPLE)
432 } else {
433 fallback
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn the_defaults_are_the_numbers_section_twelve_documents() {
443 let thresholds = Thresholds::default();
444 assert!(thresholds.enabled);
445 assert!((thresholds.cpu_watch_percent - 80.0).abs() < f32::EPSILON);
446 assert!((thresholds.cpu_critical_percent - 95.0).abs() < f32::EPSILON);
447 assert!((thresholds.memory_watch_available_percent - 15.0).abs() < f32::EPSILON);
448 assert!((thresholds.memory_critical_available_percent - 5.0).abs() < f32::EPSILON);
449 assert_eq!(thresholds.sustained_samples, 10);
450 }
451
452 #[test]
453 fn the_defaults_are_already_sanitary() {
454 assert_eq!(Thresholds::default().sanitized(), Thresholds::default());
455 }
456
457 #[test]
458 fn a_window_narrower_than_the_sample_count_is_widened_not_ignored() {
459 let thresholds = Thresholds {
462 sustained_samples: 10,
463 sustained_window: 5,
464 ..Thresholds::default()
465 }
466 .sanitized();
467 assert_eq!(thresholds.sustained_window, 10);
468 assert_eq!(thresholds.sustained_samples, 10);
469 }
470
471 #[test]
472 fn zero_samples_becomes_one_so_a_condition_still_has_to_be_observed() {
473 let thresholds = Thresholds {
474 sustained_samples: 0,
475 sustained_window: 0,
476 ..Thresholds::default()
477 }
478 .sanitized();
479 assert_eq!(thresholds.sustained_samples, 1);
480 assert_eq!(thresholds.sustained_window, 1);
481 assert_eq!(thresholds.minimum_samples(), 1);
482 }
483
484 #[test]
485 fn an_inverted_pair_of_thresholds_is_ordered_by_severity() {
486 let thresholds = Thresholds {
487 cpu_watch_percent: 95.0,
488 cpu_critical_percent: 40.0,
489 ..Thresholds::default()
490 }
491 .sanitized();
492 assert!(thresholds.cpu_critical_percent >= thresholds.cpu_watch_percent);
493 }
494
495 #[test]
496 fn memory_thresholds_are_inverted_so_critical_is_the_lower_share() {
497 let thresholds = Thresholds {
498 memory_watch_available_percent: 5.0,
499 memory_critical_available_percent: 15.0,
500 ..Thresholds::default()
501 }
502 .sanitized();
503 assert!(
504 thresholds.memory_critical_available_percent
505 <= thresholds.memory_watch_available_percent,
506 "less available memory must be the more severe state"
507 );
508 }
509
510 #[test]
511 fn non_finite_percentages_fall_back_to_the_documented_default() {
512 let thresholds = Thresholds {
513 cpu_watch_percent: f32::NAN,
514 psi_watch_percent: f32::INFINITY,
515 load_watch_per_cpu: -3.0,
516 swap_watch_bytes_per_second: f64::NAN,
517 ..Thresholds::default()
518 }
519 .sanitized();
520 assert!((thresholds.cpu_watch_percent - DEFAULT_CPU_WATCH_PERCENT).abs() < f32::EPSILON);
521 assert!(thresholds.psi_watch_percent.is_finite());
522 assert!(thresholds.load_watch_per_cpu >= 0.0);
523 assert!(thresholds.swap_watch_bytes_per_second.is_finite());
524 }
525
526 #[test]
527 fn interval_multiples_are_bounded_so_duration_arithmetic_cannot_overflow() {
528 let thresholds = Thresholds {
529 stale_watch_intervals: f32::MAX,
530 discontinuity_intervals: 0.0,
531 ..Thresholds::default()
532 }
533 .sanitized();
534 assert!(thresholds.stale_watch_intervals <= MAX_INTERVAL_MULTIPLE);
535 assert!(thresholds.discontinuity_intervals >= MIN_INTERVAL_MULTIPLE);
536
537 let seconds = Thresholds::intervals_as_seconds(Duration::from_secs(1), f32::MAX);
538 assert!(seconds.is_finite());
539 }
540
541 #[test]
542 fn memory_shares_convert_between_available_and_used() {
543 let thresholds = Thresholds::default();
544 assert!((thresholds.memory_watch_used_percent() - 85.0).abs() < f32::EPSILON);
545 assert!((thresholds.memory_critical_used_percent() - 95.0).abs() < f32::EPSILON);
546 }
547
548 #[test]
549 fn an_available_share_above_one_hundred_does_not_produce_a_negative_used_share() {
550 let thresholds = Thresholds {
551 memory_watch_available_percent: 400.0,
552 ..Thresholds::default()
553 };
554 assert!(thresholds.memory_watch_used_percent() >= 0.0);
555 }
556
557 #[test]
558 fn the_self_overhead_budgets_match_section_sixteen() {
559 let thresholds = Thresholds::default();
560 assert_eq!(thresholds.self_sample_budget(), Duration::from_millis(200));
561 assert_eq!(thresholds.self_rss_budget_bytes, 50 * 1024 * 1024);
562 assert!((thresholds.self_cpu_budget_percent - 2.0).abs() < f32::EPSILON);
563 }
564}