mecha10-core 0.6.3

Mecha10 core runtime and foundational types
Documentation
// System Message Types

use crate::messages::Message;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Health status of a node or system
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthStatus {
    /// Timestamp in microseconds since epoch
    pub timestamp: u64,

    /// Overall health level
    pub level: HealthLevel,

    /// Human-readable status message
    pub message: String,

    /// Additional diagnostic data (key-value pairs)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub diagnostics: Option<HashMap<String, String>>,
}

impl HealthStatus {
    /// Create a healthy status
    pub fn healthy() -> Self {
        Self {
            timestamp: crate::prelude::now_micros(),
            level: HealthLevel::Ok,
            message: "Operating normally".to_string(),
            diagnostics: None,
        }
    }

    /// Create a degraded status with a message
    pub fn degraded(message: impl Into<String>) -> Self {
        Self {
            timestamp: crate::prelude::now_micros(),
            level: HealthLevel::Degraded,
            message: message.into(),
            diagnostics: None,
        }
    }

    /// Create an unhealthy status with a message
    pub fn unhealthy(message: impl Into<String>) -> Self {
        Self {
            timestamp: crate::prelude::now_micros(),
            level: HealthLevel::Error,
            message: message.into(),
            diagnostics: None,
        }
    }

    /// Add diagnostic data
    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
    }

    /// Set a custom message
    pub fn with_message(mut self, message: impl Into<String>) -> Self {
        self.message = message.into();
        self
    }
}

/// Health level
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HealthLevel {
    /// Everything is functioning normally
    Ok,

    /// Operating but with reduced performance
    Degraded,

    /// Not functioning properly
    Error,

    /// Cannot determine health status
    Unknown,
}

/// Node status information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeStatus {
    /// Timestamp in microseconds since epoch
    pub timestamp: u64,

    /// Node name
    pub name: String,

    /// Current state
    pub state: NodeState,

    /// Uptime in seconds
    pub uptime: f64,

    /// CPU usage (0.0 to 1.0)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cpu_usage: Option<f32>,

    /// Memory usage in bytes
    #[serde(skip_serializing_if = "Option::is_none")]
    pub memory_usage: Option<u64>,

    /// Message throughput (messages/second)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message_rate: Option<f32>,

    /// Health status
    pub health: HealthStatus,
}

/// Node execution state
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NodeState {
    /// Node is starting up
    Starting,

    /// Node is running normally
    Running,

    /// Node is paused
    Paused,

    /// Node is shutting down
    Stopping,

    /// Node has stopped
    Stopped,

    /// Node has crashed
    Crashed,
}

/// System command
///
/// Commands to control node execution
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemCommand {
    /// Timestamp in microseconds since epoch (optional, defaults to 0)
    #[serde(default)]
    pub timestamp: u64,

    /// Target node name (or "*" for all nodes)
    pub target: String,

    /// Command to execute
    pub command: Command,
}

/// Command types
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Command {
    /// Start the node
    Start,

    /// Stop the node gracefully
    Stop,

    /// Pause execution
    Pause,

    /// Resume execution
    Resume,

    /// Restart the node
    Restart,

    /// Fire a node-specific single-shot action (e.g. image-classifier's one-shot
    /// classify trigger) without changing the node's overall running state.
    ///
    /// Unlike `Start`/`Stop`/`Restart`, which [`crate::command_relay::CommandRelay`]
    /// dispatches to lifecycle-management methods on its `NodeController`, `Trigger`
    /// is relayed as a fire-and-forget event on `system/node/{target}/trigger` (see
    /// [`crate::lifecycle::topic_node_trigger`]) - any node may opt in to that topic
    /// to receive dashboard-initiated one-shot triggers alongside its own
    /// node-specific control topic. Added for LAB-2082, where the dashboard's
    /// single-shot "c" trigger had no verb in this enum and was silently dropped by
    /// `CommandRelay`'s `_ => debug!` fallback.
    Trigger,

    /// Request health check
    HealthCheck,

    /// Reload configuration
    ReloadConfig,

    /// Shutdown the runtime
    Shutdown,
}

/// Severity of a [`LogRecord`], mirroring `tracing::Level`.
#[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"),
        }
    }
}

/// A single log event, published to `/system/log` so the platform's log
/// aggregator can stream it to `/logs/stream` subscribers (the dashboard's
/// fleet-level logs view), filterable by robot, node, level, and a search
/// pattern. See `mecha10_core::log_publisher`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogRecord {
    /// The node that emitted this log line.
    pub node_id: String,

    /// Severity of the log event.
    pub level: LogLevel,

    /// `tracing` target (module path) the event was emitted from.
    pub target: String,

    /// Rendered message text.
    pub message: String,

    /// Timestamp in microseconds since epoch.
    pub timestamp: u64,
}

// Implement Message trait so LogRecord can be published via pub/sub, the
// same way HealthReport implements it for `/system/health`.
impl Message for LogRecord {}

/// Generates a `SystemCommand` associated constructor that tags a specific
/// `Command` variant.
///
/// Every `SystemCommand` constructor has the same shape — stamp the current
/// timestamp, store the target, and set a fixed `Command` variant — so this
/// avoids repeating that boilerplate for each command kind.
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!(
        /// Create a start command
        start,
        Command::Start
    );

    system_command_ctor!(
        /// Create a stop command
        stop,
        Command::Stop
    );

    system_command_ctor!(
        /// Create a restart command
        restart,
        Command::Restart
    );

    system_command_ctor!(
        /// Create a trigger command (fire a node-specific single-shot action)
        trigger,
        Command::Trigger
    );

    system_command_ctor!(
        /// Create a health check request
        health_check,
        Command::HealthCheck
    );
}

#[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);
    }
}