use core::time::Duration;
pub const DEFAULT_CPU_WATCH_PERCENT: f32 = 80.0;
pub const DEFAULT_CPU_CRITICAL_PERCENT: f32 = 95.0;
pub const DEFAULT_MEMORY_WATCH_AVAILABLE_PERCENT: f32 = 15.0;
pub const DEFAULT_MEMORY_CRITICAL_AVAILABLE_PERCENT: f32 = 5.0;
pub const DEFAULT_SUSTAINED_SAMPLES: usize = 10;
pub const DEFAULT_SUSTAINED_WINDOW: usize = 15;
pub const MAX_SUSTAINED_WINDOW: usize = 600;
pub const MIN_INTERVAL_MULTIPLE: f32 = 1.0;
pub const MAX_INTERVAL_MULTIPLE: f32 = 1_000.0;
const MIB: u64 = 1024 * 1024;
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Thresholds {
pub enabled: bool,
pub cpu_watch_percent: f32,
pub cpu_critical_percent: f32,
pub memory_watch_available_percent: f32,
pub memory_critical_available_percent: f32,
pub sustained_samples: usize,
pub sustained_window: usize,
pub swap_watch_bytes_per_second: f64,
pub swap_critical_bytes_per_second: f64,
pub psi_watch_percent: f32,
pub psi_critical_percent: f32,
pub disk_busy_watch_percent: f32,
pub disk_busy_critical_percent: f32,
pub network_watch_percent: f32,
pub network_critical_percent: f32,
pub load_watch_per_cpu: f32,
pub load_critical_per_cpu: f32,
pub process_cpu_spike_percent: f32,
pub process_cpu_spike_points: f32,
pub process_rss_minimum_bytes: u64,
pub process_rss_growth_bytes_per_minute: u64,
pub zombie_watch_count: usize,
pub collector_lag_watch_intervals: f32,
pub collector_lag_critical_intervals: f32,
pub stale_watch_intervals: f32,
pub stale_critical_intervals: f32,
pub discontinuity_intervals: f32,
pub self_cpu_budget_percent: f32,
pub self_rss_budget_bytes: u64,
pub self_sample_budget_millis: u64,
}
impl Default for Thresholds {
fn default() -> Self {
Self {
enabled: true,
cpu_watch_percent: DEFAULT_CPU_WATCH_PERCENT,
cpu_critical_percent: DEFAULT_CPU_CRITICAL_PERCENT,
memory_watch_available_percent: DEFAULT_MEMORY_WATCH_AVAILABLE_PERCENT,
memory_critical_available_percent: DEFAULT_MEMORY_CRITICAL_AVAILABLE_PERCENT,
sustained_samples: DEFAULT_SUSTAINED_SAMPLES,
sustained_window: DEFAULT_SUSTAINED_WINDOW,
swap_watch_bytes_per_second: MIB as f64,
swap_critical_bytes_per_second: 16.0 * MIB as f64,
psi_watch_percent: 10.0,
psi_critical_percent: 40.0,
disk_busy_watch_percent: 80.0,
disk_busy_critical_percent: 95.0,
network_watch_percent: 70.0,
network_critical_percent: 90.0,
load_watch_per_cpu: 1.0,
load_critical_per_cpu: 2.0,
process_cpu_spike_percent: 100.0,
process_cpu_spike_points: 50.0,
process_rss_minimum_bytes: 128 * MIB,
process_rss_growth_bytes_per_minute: 64 * MIB,
zombie_watch_count: 8,
collector_lag_watch_intervals: 2.0,
collector_lag_critical_intervals: 5.0,
stale_watch_intervals: 3.0,
stale_critical_intervals: 10.0,
discontinuity_intervals: 10.0,
self_cpu_budget_percent: 2.0,
self_rss_budget_bytes: 50 * MIB,
self_sample_budget_millis: 200,
}
}
}
impl Thresholds {
#[must_use]
pub fn sanitized(self) -> Self {
let defaults = Self::default();
let mut out = Self {
enabled: self.enabled,
cpu_watch_percent: non_negative(self.cpu_watch_percent, defaults.cpu_watch_percent),
cpu_critical_percent: non_negative(
self.cpu_critical_percent,
defaults.cpu_critical_percent,
),
memory_watch_available_percent: non_negative(
self.memory_watch_available_percent,
defaults.memory_watch_available_percent,
),
memory_critical_available_percent: non_negative(
self.memory_critical_available_percent,
defaults.memory_critical_available_percent,
),
sustained_samples: self.sustained_samples.clamp(1, MAX_SUSTAINED_WINDOW),
sustained_window: self.sustained_window.clamp(1, MAX_SUSTAINED_WINDOW),
swap_watch_bytes_per_second: non_negative_f64(
self.swap_watch_bytes_per_second,
defaults.swap_watch_bytes_per_second,
),
swap_critical_bytes_per_second: non_negative_f64(
self.swap_critical_bytes_per_second,
defaults.swap_critical_bytes_per_second,
),
psi_watch_percent: non_negative(self.psi_watch_percent, defaults.psi_watch_percent),
psi_critical_percent: non_negative(
self.psi_critical_percent,
defaults.psi_critical_percent,
),
disk_busy_watch_percent: non_negative(
self.disk_busy_watch_percent,
defaults.disk_busy_watch_percent,
),
disk_busy_critical_percent: non_negative(
self.disk_busy_critical_percent,
defaults.disk_busy_critical_percent,
),
network_watch_percent: non_negative(
self.network_watch_percent,
defaults.network_watch_percent,
),
network_critical_percent: non_negative(
self.network_critical_percent,
defaults.network_critical_percent,
),
load_watch_per_cpu: non_negative(self.load_watch_per_cpu, defaults.load_watch_per_cpu),
load_critical_per_cpu: non_negative(
self.load_critical_per_cpu,
defaults.load_critical_per_cpu,
),
process_cpu_spike_percent: non_negative(
self.process_cpu_spike_percent,
defaults.process_cpu_spike_percent,
),
process_cpu_spike_points: non_negative(
self.process_cpu_spike_points,
defaults.process_cpu_spike_points,
),
process_rss_minimum_bytes: self.process_rss_minimum_bytes,
process_rss_growth_bytes_per_minute: self.process_rss_growth_bytes_per_minute.max(1),
zombie_watch_count: self.zombie_watch_count.max(1),
collector_lag_watch_intervals: multiple(
self.collector_lag_watch_intervals,
defaults.collector_lag_watch_intervals,
),
collector_lag_critical_intervals: multiple(
self.collector_lag_critical_intervals,
defaults.collector_lag_critical_intervals,
),
stale_watch_intervals: multiple(
self.stale_watch_intervals,
defaults.stale_watch_intervals,
),
stale_critical_intervals: multiple(
self.stale_critical_intervals,
defaults.stale_critical_intervals,
),
discontinuity_intervals: multiple(
self.discontinuity_intervals,
defaults.discontinuity_intervals,
),
self_cpu_budget_percent: non_negative(
self.self_cpu_budget_percent,
defaults.self_cpu_budget_percent,
),
self_rss_budget_bytes: self.self_rss_budget_bytes.max(1),
self_sample_budget_millis: self.self_sample_budget_millis.max(1),
};
out.cpu_critical_percent = out.cpu_critical_percent.max(out.cpu_watch_percent);
out.psi_critical_percent = out.psi_critical_percent.max(out.psi_watch_percent);
out.disk_busy_critical_percent = out
.disk_busy_critical_percent
.max(out.disk_busy_watch_percent);
out.network_critical_percent = out.network_critical_percent.max(out.network_watch_percent);
out.load_critical_per_cpu = out.load_critical_per_cpu.max(out.load_watch_per_cpu);
out.swap_critical_bytes_per_second = out
.swap_critical_bytes_per_second
.max(out.swap_watch_bytes_per_second);
out.collector_lag_critical_intervals = out
.collector_lag_critical_intervals
.max(out.collector_lag_watch_intervals);
out.stale_critical_intervals = out.stale_critical_intervals.max(out.stale_watch_intervals);
out.memory_critical_available_percent = out
.memory_critical_available_percent
.min(out.memory_watch_available_percent);
out.sustained_window = out.sustained_window.max(out.sustained_samples);
out
}
#[must_use]
pub const fn minimum_samples(&self) -> usize {
self.sustained_samples
}
#[must_use]
pub const fn self_sample_budget(&self) -> Duration {
Duration::from_millis(self.self_sample_budget_millis)
}
#[must_use]
pub fn memory_watch_used_percent(&self) -> f32 {
(100.0 - self.memory_watch_available_percent).max(0.0)
}
#[must_use]
pub fn memory_critical_used_percent(&self) -> f32 {
(100.0 - self.memory_critical_available_percent).max(0.0)
}
#[must_use]
pub fn intervals_as_seconds(interval: Duration, multiple: f32) -> f64 {
interval.as_secs_f64()
* f64::from(multiple.clamp(MIN_INTERVAL_MULTIPLE, MAX_INTERVAL_MULTIPLE))
}
}
fn non_negative(value: f32, fallback: f32) -> f32 {
if value.is_finite() && value >= 0.0 {
value
} else {
fallback
}
}
fn non_negative_f64(value: f64, fallback: f64) -> f64 {
if value.is_finite() && value >= 0.0 {
value
} else {
fallback
}
}
fn multiple(value: f32, fallback: f32) -> f32 {
if value.is_finite() {
value.clamp(MIN_INTERVAL_MULTIPLE, MAX_INTERVAL_MULTIPLE)
} else {
fallback
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_defaults_are_the_numbers_section_twelve_documents() {
let thresholds = Thresholds::default();
assert!(thresholds.enabled);
assert!((thresholds.cpu_watch_percent - 80.0).abs() < f32::EPSILON);
assert!((thresholds.cpu_critical_percent - 95.0).abs() < f32::EPSILON);
assert!((thresholds.memory_watch_available_percent - 15.0).abs() < f32::EPSILON);
assert!((thresholds.memory_critical_available_percent - 5.0).abs() < f32::EPSILON);
assert_eq!(thresholds.sustained_samples, 10);
}
#[test]
fn the_defaults_are_already_sanitary() {
assert_eq!(Thresholds::default().sanitized(), Thresholds::default());
}
#[test]
fn a_window_narrower_than_the_sample_count_is_widened_not_ignored() {
let thresholds = Thresholds {
sustained_samples: 10,
sustained_window: 5,
..Thresholds::default()
}
.sanitized();
assert_eq!(thresholds.sustained_window, 10);
assert_eq!(thresholds.sustained_samples, 10);
}
#[test]
fn zero_samples_becomes_one_so_a_condition_still_has_to_be_observed() {
let thresholds = Thresholds {
sustained_samples: 0,
sustained_window: 0,
..Thresholds::default()
}
.sanitized();
assert_eq!(thresholds.sustained_samples, 1);
assert_eq!(thresholds.sustained_window, 1);
assert_eq!(thresholds.minimum_samples(), 1);
}
#[test]
fn an_inverted_pair_of_thresholds_is_ordered_by_severity() {
let thresholds = Thresholds {
cpu_watch_percent: 95.0,
cpu_critical_percent: 40.0,
..Thresholds::default()
}
.sanitized();
assert!(thresholds.cpu_critical_percent >= thresholds.cpu_watch_percent);
}
#[test]
fn memory_thresholds_are_inverted_so_critical_is_the_lower_share() {
let thresholds = Thresholds {
memory_watch_available_percent: 5.0,
memory_critical_available_percent: 15.0,
..Thresholds::default()
}
.sanitized();
assert!(
thresholds.memory_critical_available_percent
<= thresholds.memory_watch_available_percent,
"less available memory must be the more severe state"
);
}
#[test]
fn non_finite_percentages_fall_back_to_the_documented_default() {
let thresholds = Thresholds {
cpu_watch_percent: f32::NAN,
psi_watch_percent: f32::INFINITY,
load_watch_per_cpu: -3.0,
swap_watch_bytes_per_second: f64::NAN,
..Thresholds::default()
}
.sanitized();
assert!((thresholds.cpu_watch_percent - DEFAULT_CPU_WATCH_PERCENT).abs() < f32::EPSILON);
assert!(thresholds.psi_watch_percent.is_finite());
assert!(thresholds.load_watch_per_cpu >= 0.0);
assert!(thresholds.swap_watch_bytes_per_second.is_finite());
}
#[test]
fn interval_multiples_are_bounded_so_duration_arithmetic_cannot_overflow() {
let thresholds = Thresholds {
stale_watch_intervals: f32::MAX,
discontinuity_intervals: 0.0,
..Thresholds::default()
}
.sanitized();
assert!(thresholds.stale_watch_intervals <= MAX_INTERVAL_MULTIPLE);
assert!(thresholds.discontinuity_intervals >= MIN_INTERVAL_MULTIPLE);
let seconds = Thresholds::intervals_as_seconds(Duration::from_secs(1), f32::MAX);
assert!(seconds.is_finite());
}
#[test]
fn memory_shares_convert_between_available_and_used() {
let thresholds = Thresholds::default();
assert!((thresholds.memory_watch_used_percent() - 85.0).abs() < f32::EPSILON);
assert!((thresholds.memory_critical_used_percent() - 95.0).abs() < f32::EPSILON);
}
#[test]
fn an_available_share_above_one_hundred_does_not_produce_a_negative_used_share() {
let thresholds = Thresholds {
memory_watch_available_percent: 400.0,
..Thresholds::default()
};
assert!(thresholds.memory_watch_used_percent() >= 0.0);
}
#[test]
fn the_self_overhead_budgets_match_section_sixteen() {
let thresholds = Thresholds::default();
assert_eq!(thresholds.self_sample_budget(), Duration::from_millis(200));
assert_eq!(thresholds.self_rss_budget_bytes, 50 * 1024 * 1024);
assert!((thresholds.self_cpu_budget_percent - 2.0).abs() < f32::EPSILON);
}
}