mecha10-core 0.6.5

Mecha10 core runtime and foundational types
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
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::at_level(HealthLevel::Ok, "Operating normally")
    }

    // duplicate: false positive — `degraded`/`unhealthy` already delegate their shared shape
    // to `at_level`; only the `HealthLevel` literal differs.
    /// Create a degraded status with a message
    pub fn degraded(message: impl Into<String>) -> Self {
        Self::at_level(HealthLevel::Degraded, message)
    }

    /// Create an unhealthy status with a message
    pub fn unhealthy(message: impl Into<String>) -> Self {
        Self::at_level(HealthLevel::Error, message)
    }

    /// Shared constructor behind `healthy`/`degraded`/`unhealthy`, which only differ in
    /// the health level and message.
    fn at_level(level: HealthLevel, message: impl Into<String>) -> Self {
        Self {
            timestamp: crate::prelude::now_micros(),
            level,
            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"),
        }
    }
}

impl From<&str> for LogLevel {
    /// Case-insensitively parse a level name (as found in a free-text log
    /// line or a config value), defaulting to [`LogLevel::Info`] for
    /// anything unrecognized rather than failing - this is used for
    /// best-effort classification, not strict validation.
    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 {
    /// Ordinal severity, low to high (`Trace` = 0 .. `Error` = 4). Used to
    /// compare levels, e.g. "is this record at least as severe as the
    /// configured minimum" (see [`LogLevel::at_least`]).
    pub fn severity(self) -> u8 {
        match self {
            LogLevel::Trace => 0,
            LogLevel::Debug => 1,
            LogLevel::Info => 2,
            LogLevel::Warn => 3,
            LogLevel::Error => 4,
        }
    }

    /// Whether this level is at least as severe as `min_level` - the
    /// standard "should this be forwarded/displayed given a minimum level
    /// filter" check.
    pub fn at_least(self, min_level: LogLevel) -> bool {
        self.severity() >= min_level.severity()
    }
}

/// 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,
}

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