Logging
HORUS provides structured, node-aware logging macros that write to both the console and a shared memory buffer (visible in horus monitor and horus log).
use horus::prelude::*;
hlog! — Standard Logging
Log a message with a level and the current node context:
hlog!(info, "Sensor initialized on port {}", port);
hlog!(warn, "Battery at {}% — consider charging", pct);
hlog!(error, "Failed to read IMU: {}", err);
hlog!(debug, "Raw accelerometer: {:?}", accel);
Log Levels
| Level | Color | Use For |
|---|---|---|
info | Blue | Normal operation events (startup, config loaded, calibration done) |
warn | Yellow | Abnormal but recoverable conditions (battery low, sensor noisy) |
error | Red | Failures that need attention (hardware disconnected, topic timeout) |
debug | Gray | Detailed information for development (raw values, timing) |
Output Format
Logs appear on stderr with color and node attribution:
[INFO] [SensorNode] Initialized on /dev/ttyUSB0
[WARN] [BatteryMonitor] Battery at 15% — consider charging
[ERROR] [MotorController] Failed to read encoder: timeout
[DEBUG] [Planner] Path computed in 2.3ms, 47 waypoints
The scheduler automatically sets the node context before each tick(), init(), and shutdown() call — you don't need to pass the node name manually.
Example in a Node
use horus::prelude::*;
struct SensorNode {
port: String,
readings: u64,
}
impl Node for SensorNode {
fn name(&self) -> &str { "SensorNode" }
fn init(&mut self) {
hlog!(info, "Starting sensor on {}", self.port);
}
fn tick(&mut self) {
self.readings += 1;
hlog!(debug, "Reading #{}", self.readings);
if self.readings % 1000 == 0 {
hlog!(info, "Processed {} readings", self.readings);
}
}
fn shutdown(&mut self) {
hlog!(info, "Sensor shutting down after {} readings", self.readings);
}
}
hlog_once! — Log Once Per Run
Log a message exactly once, regardless of how many times the callsite executes. Subsequent calls from the same location are silently ignored.
fn tick(&mut self) {
if let Some(frame) = self.camera.recv() {
hlog_once!(info, "First frame received: {}x{}", frame.width, frame.height);
// Only logs on the very first frame — silent after that
}
}
Equivalent to ROS2's RCLCPP_INFO_ONCE.
Use for:
- First-time events ("calibration complete", "first message received")
- One-time warnings ("running without GPU acceleration")
- Init messages inside
tick()that only matter once
hlog_every! — Rate-Limited Logging
Log at most once per N milliseconds. Prevents log flooding from high-frequency nodes.
fn tick(&mut self) {
// At most once per second, even if tick() runs at 1000 Hz
hlog_every!(1000, info, "Position: ({:.2}, {:.2})", self.x, self.y);
// At most once per 5 seconds
hlog_every!(5000, warn, "Battery: {}%", self.battery_pct);
// At most once per 200ms (5 Hz logging from a 100 Hz node)
hlog_every!(200, debug, "Encoder ticks: L={} R={}", self.left, self.right);
}
Syntax: hlog_every!(interval_ms, level, format, args...)
Equivalent to ROS2's RCLCPP_INFO_THROTTLE.
Use for:
- Status updates from high-frequency nodes (>10 Hz)
- Periodic health reports
- Any log inside
tick()that would otherwise flood the console
Viewing Logs
Console
Logs appear on stderr in real-time with color coding.
horus log CLI
# View all logs
horus log
# Follow live (like tail -f)
horus log -f
# Filter by node name
horus log SensorNode
# Filter by level
horus log --level warn
# Show last N entries
horus log -n 50
# Filter by time
horus log -s "5m ago"
# Clear logs
horus log --clear
horus monitor
The web dashboard (horus monitor) and TUI (horus monitor -t) show a live log stream with filtering by node and level.
Python Logging
Python nodes use method calls instead of macros:
import horus
def sensor_tick(node):
node.log_info(f"Reading: {value}")
node.log_warning("Battery low")
node.log_error("Sensor disconnected")
node.log_debug(f"Raw: {raw_data}")
sensor = horus.Node(name="SensorNode", tick=sensor_tick, rate=10)
Best Practices
| Do | Don't |
|---|---|
hlog_every!(1000, ...) in tick() at >10 Hz | hlog!(info, ...) every tick at 1000 Hz |
hlog_once!(info, "First frame") for one-time events | if self.first { hlog!(...); self.first = false; } |
hlog!(info, ...) in init() and shutdown() | Log only in tick() |
Include units: "speed: {:.1} m/s" | Bare numbers: "speed: {}" |
Use debug for high-volume data | Use info for everything |
See Also
- Monitor — live log dashboard
- CLI Reference —
horus logcommand details - BlackBox — flight recorder for post-mortem analysis