use byte_unit::Byte;
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize, ValueEnum)]
#[repr(u8)]
pub enum LogLevel {
Error = 1,
Warn = 2,
Info = 3,
Debug = 4,
}
impl LogLevel {
pub fn from_u8(v: u8) -> Self {
match v {
1 => LogLevel::Error,
2 => LogLevel::Warn,
3 => LogLevel::Info,
4 => LogLevel::Debug,
_ => LogLevel::Info, }
}
pub fn as_str(&self) -> &'static str {
match self {
LogLevel::Error => "ERROR",
LogLevel::Warn => "WARN",
LogLevel::Info => "INFO",
LogLevel::Debug => "DEBUG",
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, ValueEnum)]
#[repr(u8)]
pub enum LogMode {
Compact = 0,
Json = 1,
}
impl LogMode {
pub fn from_u8(v: u8) -> Self {
match v {
1 => LogMode::Json,
_ => LogMode::Compact,
}
}
}
fn format_bytes(bytes: u64) -> String {
format!(
"{:.2}",
Byte::from_u64(bytes).get_appropriate_unit(byte_unit::UnitType::Decimal)
)
}
#[derive(Serialize, Clone)]
#[serde(tag = "message", rename_all = "snake_case")]
pub enum SentinelEvent<'a> {
Message {
#[serde(skip)] level: LogLevel,
text: &'a str,
},
Startup {
interval_ms: u64,
},
Monitor {
memory_available_bytes: Option<u64>,
memory_available_percent: Option<f64>,
swap_free_bytes: Option<u64>,
swap_free_percent: Option<f64>,
zram_free_bytes: Option<u64>,
zram_free_percent: Option<f64>,
psi_pressure: Option<f64>,
},
LowMemoryWarn {
available_bytes: u64,
available_percent: f64,
threshold_type: &'a str,
threshold_value: f64,
},
LowSwapWarn {
free_bytes: u64,
free_percent: f64,
threshold_type: &'a str,
threshold_value: f64,
},
LowZramWarn {
free_bytes: u64,
free_percent: f64,
threshold_type: &'a str,
threshold_value: f64,
},
PsiPressureWarn {
pressure_curr: f64,
threshold: f64,
},
KillTriggered {
trigger: &'a str,
observed_value: f64,
threshold_value: f64,
threshold_type: &'a str,
amount_needed: Option<u64>,
},
KillCandidateSelected {
pid: u32,
process_name: &'a str,
score: u64,
rss: u64,
match_index: usize,
},
KillExecuted {
pid: u32,
process_name: &'a str,
strategy: &'a str,
rss_freed: u64,
},
KillSequenceFinished {
reason: &'a str,
},
KillSequenceAborted {
reason: &'a str,
},
KillCandidateIgnored {
pid: u32,
reason: &'a str,
},
}
impl fmt::Display for SentinelEvent<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SentinelEvent::Message { text, .. } => write!(f, "{}", text),
SentinelEvent::Startup { interval_ms } => {
write!(f, "ram-sentinel started. Interval: {}ms", interval_ms)
}
SentinelEvent::Monitor {
memory_available_bytes,
memory_available_percent: _,
swap_free_bytes,
swap_free_percent: _,
zram_free_bytes,
zram_free_percent: _,
psi_pressure,
} => {
let avail_str = match memory_available_bytes {
Some(b) => format_bytes(*b),
None => "N/A".to_string(),
};
let swap_str = match swap_free_bytes {
Some(b) => format_bytes(*b),
None => "N/A".to_string(),
};
let zram_str = match zram_free_bytes {
Some(b) => format_bytes(*b),
None => "N/A".to_string(),
};
let psi_str = match psi_pressure {
Some(p) => format!("{:.2}", p),
None => "N/A".to_string(),
};
write!(
f,
"Memory: {} available, Swap: {} available, ZRAM: {} available, PSI: {}",
avail_str, swap_str, zram_str, psi_str
)
}
SentinelEvent::LowMemoryWarn {
available_bytes,
available_percent,
threshold_type,
threshold_value,
} => {
let avail_str = format_bytes(*available_bytes);
if *threshold_type == "bytes" {
let thresh_str = format_bytes(*threshold_value as u64);
write!(
f,
"Low RAM: {} available (Limit: {})",
avail_str, thresh_str
)
} else {
write!(
f,
"Low RAM: {} ({:.2}%) available (Limit: {:.2}%)",
avail_str, available_percent, threshold_value
)
}
}
SentinelEvent::LowSwapWarn {
free_bytes,
free_percent,
threshold_type,
threshold_value,
} => {
let free_str = format_bytes(*free_bytes);
if *threshold_type == "bytes" {
let thresh_str = format_bytes(*threshold_value as u64);
write!(
f,
"Low Swap: {} available (Limit: {})",
free_str, thresh_str
)
} else {
write!(
f,
"Low Swap: {} ({:.2}%) available (Limit: {:.2}%)",
free_str, free_percent, threshold_value
)
}
}
SentinelEvent::LowZramWarn {
free_bytes,
free_percent,
threshold_type,
threshold_value,
} => {
let free_str = format_bytes(*free_bytes);
if *threshold_type == "bytes" {
let thresh_str = format_bytes(*threshold_value as u64);
write!(
f,
"Low ZRAM: {} available (Limit: {})",
free_str, thresh_str
)
} else {
write!(
f,
"Low ZRAM: {} ({:.2}%) available (Limit: {:.2}%)",
free_str, free_percent, threshold_value
)
}
}
SentinelEvent::PsiPressureWarn {
pressure_curr,
threshold,
} => {
write!(
f,
"Memory Pressure: {:.2}% (Limit: {:.2}%)",
pressure_curr, threshold
)
}
SentinelEvent::KillTriggered {
trigger,
observed_value,
threshold_value,
threshold_type,
..
} => {
let observed_str = if *threshold_type == "bytes" {
format_bytes(*observed_value as u64)
} else {
format!("{:.2}%", observed_value)
};
let limit_str = if *threshold_type == "bytes" {
format_bytes(*threshold_value as u64)
} else {
format!("{:.2}%", threshold_value)
};
write!(
f,
"Kill Triggered: {} - Observed {} < Limit {}",
trigger, observed_str, limit_str
)
}
SentinelEvent::KillCandidateSelected {
process_name,
pid,
score,
rss,
..
} => {
let rss_str = format_bytes(*rss);
write!(
f,
"Selected Target: {} (PID {}). Score: {}, RSS: {}",
process_name, pid, score, rss_str
)
}
SentinelEvent::KillExecuted {
process_name,
pid,
strategy,
rss_freed,
} => {
let rss_str = format_bytes(*rss_freed);
write!(
f,
"{} {} (PID {}). Freed: {}",
strategy, process_name, pid, rss_str
)
}
SentinelEvent::KillSequenceFinished { reason } => {
write!(f, "Kill Sequence Finished: {}", reason)
}
SentinelEvent::KillSequenceAborted { reason } => {
write!(f, "Kill Sequence Aborted: {}", reason)
}
SentinelEvent::KillCandidateIgnored { pid, reason } => {
write!(f, "Ignored Candidate PID {}: {}", pid, reason)
}
}
}
}
impl SentinelEvent<'_> {
pub fn severity(&self) -> LogLevel {
match self {
SentinelEvent::Message { level, .. } => *level,
SentinelEvent::Monitor { .. } => LogLevel::Debug,
SentinelEvent::Startup { .. }
| SentinelEvent::KillCandidateSelected { .. }
| SentinelEvent::KillExecuted { .. }
| SentinelEvent::KillSequenceFinished { .. }
| SentinelEvent::KillSequenceAborted { .. }
| SentinelEvent::KillCandidateIgnored { .. } => LogLevel::Info,
SentinelEvent::LowMemoryWarn { .. }
| SentinelEvent::LowSwapWarn { .. }
| SentinelEvent::LowZramWarn { .. }
| SentinelEvent::PsiPressureWarn { .. } => LogLevel::Warn,
SentinelEvent::KillTriggered { .. } => LogLevel::Error,
}
}
}