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

LevelColorUse For
infoBlueNormal operation events (startup, config loaded, calibration done)
warnYellowAbnormal but recoverable conditions (battery low, sensor noisy)
errorRedFailures that need attention (hardware disconnected, topic timeout)
debugGrayDetailed 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

DoDon't
hlog_every!(1000, ...) in tick() at >10 Hzhlog!(info, ...) every tick at 1000 Hz
hlog_once!(info, "First frame") for one-time eventsif 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 dataUse info for everything

See Also