use crate::messages::Message;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthStatus {
pub timestamp: u64,
pub level: HealthLevel,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub diagnostics: Option<HashMap<String, String>>,
}
impl HealthStatus {
pub fn healthy() -> Self {
Self::at_level(HealthLevel::Ok, "Operating normally")
}
pub fn degraded(message: impl Into<String>) -> Self {
Self::at_level(HealthLevel::Degraded, message)
}
pub fn unhealthy(message: impl Into<String>) -> Self {
Self::at_level(HealthLevel::Error, message)
}
fn at_level(level: HealthLevel, message: impl Into<String>) -> Self {
Self {
timestamp: crate::prelude::now_micros(),
level,
message: message.into(),
diagnostics: None,
}
}
pub fn with_diagnostic(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.diagnostics
.get_or_insert_with(HashMap::new)
.insert(key.into(), value.into());
self
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = message.into();
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HealthLevel {
Ok,
Degraded,
Error,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeStatus {
pub timestamp: u64,
pub name: String,
pub state: NodeState,
pub uptime: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub cpu_usage: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_usage: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_rate: Option<f32>,
pub health: HealthStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NodeState {
Starting,
Running,
Paused,
Stopping,
Stopped,
Crashed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemCommand {
#[serde(default)]
pub timestamp: u64,
pub target: String,
pub command: Command,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Command {
Start,
Stop,
Pause,
Resume,
Restart,
Trigger,
HealthCheck,
ReloadConfig,
Shutdown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl From<tracing::Level> for LogLevel {
fn from(level: tracing::Level) -> Self {
match level {
tracing::Level::TRACE => LogLevel::Trace,
tracing::Level::DEBUG => LogLevel::Debug,
tracing::Level::INFO => LogLevel::Info,
tracing::Level::WARN => LogLevel::Warn,
tracing::Level::ERROR => LogLevel::Error,
}
}
}
impl std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogLevel::Trace => write!(f, "trace"),
LogLevel::Debug => write!(f, "debug"),
LogLevel::Info => write!(f, "info"),
LogLevel::Warn => write!(f, "warn"),
LogLevel::Error => write!(f, "error"),
}
}
}
impl From<&str> for LogLevel {
fn from(s: &str) -> Self {
match s.to_lowercase().as_str() {
"trace" => LogLevel::Trace,
"debug" => LogLevel::Debug,
"info" => LogLevel::Info,
"warn" | "warning" => LogLevel::Warn,
"error" | "err" => LogLevel::Error,
_ => LogLevel::Info,
}
}
}
impl LogLevel {
pub fn severity(self) -> u8 {
match self {
LogLevel::Trace => 0,
LogLevel::Debug => 1,
LogLevel::Info => 2,
LogLevel::Warn => 3,
LogLevel::Error => 4,
}
}
pub fn at_least(self, min_level: LogLevel) -> bool {
self.severity() >= min_level.severity()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogRecord {
pub node_id: String,
pub level: LogLevel,
pub target: String,
pub message: String,
pub timestamp: u64,
}
impl Message for LogRecord {}
macro_rules! system_command_ctor {
($(#[$meta:meta])* $name:ident, $variant:expr) => {
$(#[$meta])*
pub fn $name(target: impl Into<String>) -> Self {
Self {
timestamp: crate::prelude::now_micros(),
target: target.into(),
command: $variant,
}
}
};
}
impl SystemCommand {
system_command_ctor!(
start,
Command::Start
);
system_command_ctor!(
stop,
Command::Stop
);
system_command_ctor!(
restart,
Command::Restart
);
system_command_ctor!(
trigger,
Command::Trigger
);
system_command_ctor!(
health_check,
Command::HealthCheck
);
}
#[cfg(test)]
mod log_level_tests {
use super::*;
#[test]
fn from_str_parses_known_names_case_insensitively_and_defaults_to_info() {
assert_eq!(LogLevel::from("TRACE"), LogLevel::Trace);
assert_eq!(LogLevel::from("Debug"), LogLevel::Debug);
assert_eq!(LogLevel::from("info"), LogLevel::Info);
assert_eq!(LogLevel::from("warn"), LogLevel::Warn);
assert_eq!(LogLevel::from("warning"), LogLevel::Warn);
assert_eq!(LogLevel::from("error"), LogLevel::Error);
assert_eq!(LogLevel::from("err"), LogLevel::Error);
assert_eq!(LogLevel::from("nonsense"), LogLevel::Info);
}
#[test]
fn severity_increases_from_trace_to_error() {
assert!(LogLevel::Trace.severity() < LogLevel::Debug.severity());
assert!(LogLevel::Debug.severity() < LogLevel::Info.severity());
assert!(LogLevel::Info.severity() < LogLevel::Warn.severity());
assert!(LogLevel::Warn.severity() < LogLevel::Error.severity());
}
#[test]
fn at_least_compares_by_severity() {
assert!(LogLevel::Error.at_least(LogLevel::Warn));
assert!(LogLevel::Warn.at_least(LogLevel::Warn));
assert!(!LogLevel::Debug.at_least(LogLevel::Warn));
}
}
#[cfg(test)]
mod system_command_tests {
use super::*;
#[test]
fn start_targets_the_given_node_and_tags_the_start_command() {
let cmd = SystemCommand::start("robot-1");
assert_eq!(cmd.target, "robot-1");
assert_eq!(cmd.command, Command::Start);
}
#[test]
fn stop_targets_the_given_node_and_tags_the_stop_command() {
let cmd = SystemCommand::stop("robot-1");
assert_eq!(cmd.target, "robot-1");
assert_eq!(cmd.command, Command::Stop);
}
#[test]
fn restart_targets_the_given_node_and_tags_the_restart_command() {
let cmd = SystemCommand::restart("robot-1");
assert_eq!(cmd.target, "robot-1");
assert_eq!(cmd.command, Command::Restart);
}
#[test]
fn trigger_targets_the_given_node_and_tags_the_trigger_command() {
let cmd = SystemCommand::trigger("image-classifier");
assert_eq!(cmd.target, "image-classifier");
assert_eq!(cmd.command, Command::Trigger);
}
#[test]
fn health_check_targets_the_given_node_and_tags_the_health_check_command() {
let cmd = SystemCommand::health_check("robot-1");
assert_eq!(cmd.target, "robot-1");
assert_eq!(cmd.command, Command::HealthCheck);
}
#[test]
fn each_constructor_accepts_any_impl_into_string_target() {
let owned = SystemCommand::start(String::from("robot-2"));
let borrowed = SystemCommand::start("robot-2");
assert_eq!(owned.target, borrowed.target);
}
}