Logging
You need to log diagnostic messages from your nodes without flooding the console or blocking the real-time loop. 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).
When To Use This
- Adding diagnostic output to nodes during development
- Rate-limiting log output from high-frequency nodes (100+ Hz)
- Logging one-time events (first frame received, calibration complete)
- Filtering and viewing logs by node name or severity level
Use Telemetry Export instead if you need to send numeric metrics to an external monitoring system.
Use BlackBox instead if you need post-mortem flight recorder data with per-tick granularity.
Prerequisites
- A HORUS project with
use horus::prelude::*; - Familiarity with Nodes (the logging context is set automatically per node)
hlog! — Standard Logging
Log a message with a level and the current node context:
// simplified
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
// simplified
use horus::prelude::*;
struct SensorNode {
port: String,
readings: u64,
}
impl Node for SensorNode {
fn name(&self) -> &str { "SensorNode" }
fn init(&mut self) -> Result<()> {
hlog!(info, "Starting sensor on {}", self.port);
Ok(())
}
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.
// simplified
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.
// simplified
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.
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 |
Common Errors
| Symptom | Cause | Fix |
|---|---|---|
| Console flooded with log lines | Using hlog! in a high-frequency tick() | Use hlog_every!(1000, ...) to rate-limit to once per second |
| Log messages missing node name | Logging outside of scheduler context | Only use hlog! inside init(), tick(), or shutdown() where the scheduler sets the node context |
| Python logging not appearing | Using print() instead of node log methods | Use node.log_info(), node.log_warning(), etc. |
Logs not visible in horus log | Application not writing to shared memory buffer | Ensure you use hlog! macros (not println! or eprintln!) |
See Also
- Monitor — Live log dashboard with filtering by node and level
- CLI Reference —
horus logcommand options and filters - BlackBox — Flight recorder for post-mortem per-tick analysis
- Debugging Workflows — Step-by-step debugging with log-based diagnosis