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

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

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

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

Common Errors

SymptomCauseFix
Console flooded with log linesUsing hlog! in a high-frequency tick()Use hlog_every!(1000, ...) to rate-limit to once per second
Log messages missing node nameLogging outside of scheduler contextOnly use hlog! inside init(), tick(), or shutdown() where the scheduler sets the node context
Python logging not appearingUsing print() instead of node log methodsUse node.log_info(), node.log_warning(), etc.
Logs not visible in horus logApplication not writing to shared memory bufferEnsure you use hlog! macros (not println! or eprintln!)

See Also

  • Monitor — Live log dashboard with filtering by node and level
  • CLI Referencehorus log command options and filters
  • BlackBox — Flight recorder for post-mortem per-tick analysis
  • Debugging Workflows — Step-by-step debugging with log-based diagnosis