use std::cmp::Ordering;
use std::fmt;
use std::fmt::Formatter;
pub use runner::*;
use crate::ServiceState::{Critical, Warning};
use std::str::FromStr;
mod runner;
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum ServiceState {
Ok,
Warning,
Critical,
#[default]
Unknown,
}
impl ServiceState {
pub fn exit_code(&self) -> i32 {
match self {
ServiceState::Ok => 0,
ServiceState::Warning => 1,
ServiceState::Critical => 2,
ServiceState::Unknown => 3,
}
}
fn order_number(&self) -> u8 {
match self {
ServiceState::Ok => 0,
ServiceState::Unknown => 1,
ServiceState::Warning => 2,
ServiceState::Critical => 3,
}
}
}
impl PartialOrd for ServiceState {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.order_number().partial_cmp(&other.order_number())
}
}
impl Ord for ServiceState {
fn cmp(&self, other: &Self) -> Ordering {
self.order_number().cmp(&other.order_number())
}
}
impl fmt::Display for ServiceState {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let s = match self {
ServiceState::Ok => "OK",
ServiceState::Warning => "WARNING",
ServiceState::Critical => "CRITICAL",
ServiceState::Unknown => "UNKNOWN",
};
f.write_str(s)
}
}
#[derive(Debug, thiserror::Error)]
#[error("expected one of: ok, warning, critical, unknown")]
pub struct ServiceStateFromStrError;
impl FromStr for ServiceState {
type Err = ServiceStateFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"ok" => Ok(ServiceState::Ok),
"warning" => Ok(ServiceState::Warning),
"critical" => Ok(ServiceState::Critical),
"unknown" => Ok(ServiceState::Unknown),
_ => Err(ServiceStateFromStrError),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Unit {
#[default]
None,
Seconds,
Milliseconds,
Microseconds,
Percentage,
Bytes,
Kilobytes,
Megabytes,
Gigabytes,
Terabytes,
Counter,
Other(UnitString),
}
impl Unit {
fn as_str(&self) -> &str {
match self {
Unit::None => "",
Unit::Seconds => "s",
Unit::Milliseconds => "ms",
Unit::Microseconds => "us",
Unit::Percentage => "%",
Unit::Bytes => "B",
Unit::Kilobytes => "KB",
Unit::Megabytes => "MB",
Unit::Gigabytes => "GB",
Unit::Terabytes => "TB",
Unit::Counter => "c",
Unit::Other(s) => &s.0,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum UnitStringCreateError {
#[error("expected string to not include numbers, semicolons or quotes")]
InvalidCharacters,
}
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
pub struct UnitString(String);
impl UnitString {
pub fn new(s: impl Into<String>) -> Result<Self, UnitStringCreateError> {
let s = s.into();
if ('0'..='9').chain(['"', ';']).any(|c| s.contains(c)) {
Err(UnitStringCreateError::InvalidCharacters)
} else {
Ok(UnitString::new_unchecked(s))
}
}
pub fn new_unchecked(s: impl Into<String>) -> Self {
UnitString(s.into())
}
}
impl FromStr for UnitString {
type Err = UnitStringCreateError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
UnitString::new(s)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum TriggerIfValue {
Greater,
Less,
}
impl From<&TriggerIfValue> for Ordering {
fn from(v: &TriggerIfValue) -> Self {
match v {
TriggerIfValue::Greater => Ordering::Greater,
TriggerIfValue::Less => Ordering::Less,
}
}
}
#[derive(Debug, Clone)]
pub struct Metric<T> {
name: String,
value: T,
unit: Unit,
thresholds: Option<(Option<T>, Option<T>, TriggerIfValue)>,
min: Option<T>,
max: Option<T>,
fixed_state: Option<ServiceState>,
}
impl<T> Metric<T> {
pub fn new(name: impl Into<String>, value: T) -> Self {
Self {
name: name.into(),
value,
unit: Default::default(),
thresholds: Default::default(),
min: Default::default(),
max: Default::default(),
fixed_state: Default::default(),
}
}
pub fn with_thresholds(
mut self,
warning: impl Into<Option<T>>,
critical: impl Into<Option<T>>,
trigger_if_value: TriggerIfValue,
) -> Self {
self.thresholds = Some((warning.into(), critical.into(), trigger_if_value));
self
}
pub fn with_minimum(mut self, minimum: T) -> Self {
self.min = Some(minimum);
self
}
pub fn with_maximum(mut self, maximum: T) -> Self {
self.max = Some(maximum);
self
}
pub fn with_fixed_state(mut self, state: ServiceState) -> Self {
self.fixed_state = Some(state);
self
}
pub fn with_unit(mut self, unit: Unit) -> Self {
self.unit = unit;
self
}
}
#[derive(Debug, Clone)]
pub struct PerfData<T> {
name: String,
value: T,
unit: Unit,
warning: Option<T>,
critical: Option<T>,
minimum: Option<T>,
maximum: Option<T>,
}
impl<T: ToPerfString> PerfData<T> {
pub fn new(name: impl Into<String>, value: T) -> Self {
Self {
name: name.into(),
value,
unit: Default::default(),
warning: Default::default(),
critical: Default::default(),
minimum: Default::default(),
maximum: Default::default(),
}
}
pub fn with_thresholds(mut self, warning: Option<T>, critical: Option<T>) -> Self {
self.warning = warning;
self.critical = critical;
self
}
pub fn with_minimum(mut self, minimum: T) -> Self {
self.minimum = Some(minimum);
self
}
pub fn with_maximum(mut self, maximum: T) -> Self {
self.maximum = Some(maximum);
self
}
pub fn with_unit(mut self, unit: Unit) -> Self {
self.unit = unit;
self
}
}
impl<T: ToPerfString> From<PerfData<T>> for PerfString {
fn from(perf_data: PerfData<T>) -> Self {
let s = PerfString::new(
&perf_data.name,
&perf_data.value,
perf_data.unit,
perf_data.warning.as_ref(),
perf_data.critical.as_ref(),
perf_data.minimum.as_ref(),
perf_data.maximum.as_ref(),
);
s
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct PerfString(String);
impl PerfString {
pub fn new<T>(
name: &str,
value: &T,
unit: Unit,
warning: Option<&T>,
critical: Option<&T>,
minimum: Option<&T>,
maximum: Option<&T>,
) -> Self
where
T: ToPerfString,
{
let value = value.to_perf_string();
let warning = warning.map_or_else(|| "".to_owned(), |v| v.to_perf_string());
let critical = critical.map_or_else(|| "".to_owned(), |v| v.to_perf_string());
let minimum = minimum.map_or_else(|| "".to_owned(), |v| v.to_perf_string());
let maximum = maximum.map_or_else(|| "".to_owned(), |v| v.to_perf_string());
PerfString(format!(
"'{}'={}{};{};{};{};{}",
name,
value,
unit.as_str(),
warning,
critical,
minimum,
maximum
))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckResult {
state: Option<ServiceState>,
message: Option<String>,
perf_string: Option<PerfString>,
}
impl CheckResult {
pub fn new() -> Self {
Self {
state: Default::default(),
message: Default::default(),
perf_string: Default::default(),
}
}
pub fn with_state(mut self, state: ServiceState) -> Self {
self.state = Some(state);
self
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
pub fn with_perf_data(mut self, perf_data: impl Into<PerfString>) -> Self {
self.perf_string = Some(perf_data.into());
self
}
}
impl Default for CheckResult {
fn default() -> Self {
Self::new()
}
}
impl<T: PartialOrd + ToPerfString> From<Metric<T>> for CheckResult {
fn from(metric: Metric<T>) -> Self {
let state = if let Some(state) = metric.fixed_state {
Some(state)
} else if let Some((warning, critical, trigger)) = &metric.thresholds {
let ord: Ordering = trigger.into();
let warning_cmp = warning.as_ref().and_then(|w| metric.value.partial_cmp(w));
let critical_cmp = critical.as_ref().and_then(|w| metric.value.partial_cmp(w));
[(critical_cmp, Critical), (warning_cmp, Warning)]
.iter()
.filter_map(|(cmp, state)| cmp.as_ref().map(|cmp| (cmp, state)))
.filter_map(|(&cmp, &state)| {
if cmp == ord || cmp == Ordering::Equal {
Some(state)
} else {
None
}
})
.next()
} else {
None
};
let message = match state {
Some(state) if state != ServiceState::Ok => {
let (warning, critical, _) = metric.thresholds.as_ref().unwrap();
let threshold = match state {
ServiceState::Warning => warning.as_ref().unwrap(),
ServiceState::Critical => critical.as_ref().unwrap(),
_ => unreachable!(),
};
Some(format!(
"metric '{}' is {}: value '{}' has exceeded threshold of '{}'",
&metric.name,
state,
metric.value.to_perf_string(),
threshold.to_perf_string(),
))
}
_ => None,
};
let perf_string = {
let (warning, critical) = if let Some((warning, critical, _)) = &metric.thresholds {
(warning.as_ref(), critical.as_ref())
} else {
(None, None)
};
PerfString::new(
&metric.name,
&metric.value,
metric.unit,
warning,
critical,
metric.min.as_ref(),
metric.max.as_ref(),
)
};
CheckResult {
state,
message,
perf_string: Some(perf_string),
}
}
}
pub trait ToPerfString {
fn to_perf_string(&self) -> String;
}
macro_rules! impl_to_perf_string {
($t:ty) => {
impl ToPerfString for $t {
fn to_perf_string(&self) -> String {
self.to_string()
}
}
};
}
impl_to_perf_string!(usize);
impl_to_perf_string!(isize);
impl_to_perf_string!(u8);
impl_to_perf_string!(u16);
impl_to_perf_string!(u32);
impl_to_perf_string!(u64);
impl_to_perf_string!(u128);
impl_to_perf_string!(i8);
impl_to_perf_string!(i16);
impl_to_perf_string!(i32);
impl_to_perf_string!(i64);
impl_to_perf_string!(i128);
impl_to_perf_string!(f32);
impl_to_perf_string!(f64);
#[derive(Debug, PartialEq, Eq)]
pub struct Resource {
name: String,
results: Vec<CheckResult>,
fixed_state: Option<ServiceState>,
description: Option<String>,
}
impl Resource {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
results: Default::default(),
fixed_state: Default::default(),
description: Default::default(),
}
}
pub fn with_fixed_state(mut self, state: ServiceState) -> Self {
self.fixed_state = Some(state);
self
}
pub fn with_result(mut self, result: impl Into<CheckResult>) -> Self {
self.push_result(result);
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.set_description(description);
self
}
pub fn set_description(&mut self, description: impl Into<String>) {
self.description = Some(description.into());
}
pub fn push_result(&mut self, result: impl Into<CheckResult>) {
self.results.push(result.into());
}
pub fn nagios_result(self) -> (ServiceState, String) {
let (state, messages, perf_string) = {
let mut final_state = ServiceState::Ok;
let mut messages = String::new();
let mut perf_string = String::new();
for result in self.results {
if let Some(state) = result.state {
if final_state < state {
final_state = state;
}
}
if let Some(message) = result.message {
messages.push_str(message.trim());
messages.push('\n');
}
if let Some(s) = result.perf_string {
perf_string.push(' ');
perf_string.push_str(s.0.trim());
}
}
if let Some(state) = self.fixed_state {
final_state = state;
}
(final_state, messages, perf_string)
};
let description = {
let mut s = String::new();
s.push_str(&self.name);
s.push_str(" is ");
s.push_str(&state.to_string());
if let Some(description) = self.description {
s.push_str(": ");
s.push_str(description.trim());
}
s
};
let pad = if messages.is_empty() { "" } else { "\n\n" };
(
state,
format!("{}{}{}|{}", description, pad, messages.trim(), perf_string),
)
}
fn print_and_exit(self) -> ! {
let (state, s) = self.nagios_result();
println!("{}", &s);
std::process::exit(state.exit_code());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resource_nagios_result() {
let (state, s) = Resource::new("foo")
.with_description("i am bar")
.with_result(
CheckResult::new()
.with_state(ServiceState::Warning)
.with_message("flubblebar"),
)
.with_result(CheckResult::new().with_state(ServiceState::Critical))
.nagios_result();
assert_eq!(state, ServiceState::Critical);
assert!(s.contains("i am bar"));
assert!(s.contains("flubblebar"));
assert!(s.contains(&ServiceState::Critical.to_string()));
}
#[test]
fn test_resource_with_fixed_state() {
let (state, _) = Resource::new("foo")
.with_fixed_state(ServiceState::Critical)
.nagios_result();
assert_eq!(state, ServiceState::Critical);
}
#[test]
fn test_resource_with_ok_result() {
let (state, msg) = Resource::new("foo")
.with_result(
CheckResult::new()
.with_message("test")
.with_state(ServiceState::Ok),
)
.nagios_result();
assert_eq!(ServiceState::Ok, state);
assert!(msg.contains("test"));
}
#[test]
fn test_perf_string_new() {
let s = PerfString::new("foo", &12, Unit::None, Some(&42), None, None, Some(&60));
assert_eq!(&s.0, "'foo'=12;42;;;60")
}
#[test]
fn test_metric_into_check_result_complete() {
let metric = Metric::new("test", 42)
.with_minimum(0)
.with_maximum(100)
.with_thresholds(40, 50, TriggerIfValue::Greater);
let result: CheckResult = metric.into();
assert_eq!(result.state, Some(ServiceState::Warning));
let message = result.message.expect("no message set");
assert!(message.contains(&ServiceState::Warning.to_string()));
assert!(message.contains("test"));
assert!(message.contains("threshold"));
}
#[test]
fn test_metric_into_check_result_threshold_less() {
let result: CheckResult = Metric::new("test", 40)
.with_thresholds(50, 30, TriggerIfValue::Less)
.into();
assert_eq!(result.state, Some(ServiceState::Warning));
}
#[test]
fn test_metric_into_check_result_threshold_greater() {
let result: CheckResult = Metric::new("test", 40)
.with_thresholds(30, 50, TriggerIfValue::Greater)
.into();
assert_eq!(result.state, Some(ServiceState::Warning));
}
#[test]
fn test_metric_into_check_result_threshold_equal_to_val() {
let result: CheckResult = Metric::new("foo", 30)
.with_thresholds(30, 40, TriggerIfValue::Greater)
.into();
assert_eq!(result.state, Some(ServiceState::Warning));
}
#[test]
fn test_metric_into_check_result_threshold_only_warning() {
let result: CheckResult = Metric::new("foo", 30)
.with_thresholds(25, None, TriggerIfValue::Greater)
.into();
assert_eq!(result.state, Some(ServiceState::Warning));
let result: CheckResult = Metric::new("foo", 30)
.with_thresholds(35, None, TriggerIfValue::Greater)
.into();
assert_eq!(result.state, None);
}
#[test]
fn test_metric_into_check_result_with_unit() {
let result: CheckResult = Metric::new("foo", 20)
.with_thresholds(25, None, TriggerIfValue::Greater)
.with_unit(Unit::Megabytes)
.into();
result.perf_string.unwrap().0.contains("MB");
assert_eq!(result.state, None);
}
}