Logging & BlackBox API

HORUS provides two complementary recording mechanisms:

  • horus::log -- Structured log messages visible in the horus log CLI. Use for operational status, warnings, and errors during normal operation.
  • horus::blackbox -- Warn-level entries tagged [blackbox] in the same log buffer, dual-written to the longer-retention error buffer. Use for incident events you want to pick out of the log later.
#include <horus/log.hpp>       // horus::log::info/warn/error
#include <horus/blackbox.hpp>  // horus::blackbox::record

#include <horus/horus.hpp> pulls in both.


Quick Reference

Logging

FunctionLevelUse Case
horus::log::info(node, msg)InfoNormal operation, milestones
horus::log::warn(node, msg)WarnDegraded operation, approaching limits
horus::log::error(node, msg)ErrorFailures, safety events

BlackBox

FunctionPurpose
horus::blackbox::record(category, message)Publish a Warn-level entry tagged [blackbox] to the log buffer

horus::log

info()

void horus::log::info(const char* node, const char* msg);
void horus::log::info(const std::string& node, const std::string& msg);

Emits an informational log message. Visible in horus log with default filters.

Parameters

NameTypeDescription
nodeconst char* or const std::string&Name of the node emitting the log. Should match the node's registered name.
msgconst char* or const std::string&Log message content.
horus::log::info("motor_ctrl", "Motor controller initialized at 1000Hz");
horus::log::info("camera", "Streaming 1280x720 RGB @ 30fps");

warn()

void horus::log::warn(const char* node, const char* msg);
void horus::log::warn(const std::string& node, const std::string& msg);

Emits a warning log message. Indicates degraded operation or approaching limits.

horus::log::warn("imu", "Calibration drift detected: 0.3 deg/s");
horus::log::warn("battery", "Battery at 15% -- consider returning to base");

error()

void horus::log::error(const char* node, const char* msg);
void horus::log::error(const std::string& node, const std::string& msg);

Emits an error log message. Indicates a failure that requires attention.

horus::log::error("safety", "Emergency stop triggered by collision sensor");
horus::log::error("motor_ctrl", "Motor driver communication timeout");

Log Levels

LevelConstantWhen to Use
Info0Normal operation: startup, milestones, periodic status
Warn1Something is wrong but the system continues: sensor drift, low battery, approaching limits
Error2Something failed: communication loss, safety event, unrecoverable state

The log level is passed internally as an integer to the C FFI layer (horus_log(level, node, msg)). Info = 0, Warn = 1, Error = 2.


Viewing Logs with the CLI

All log messages are visible in the horus log command:

# Show recent log entries (last 100)
horus log

# Stream new entries in real time
horus log -f

# Filter by node
horus log motor_ctrl

# Filter by level
horus log --level warn

# Filter by level and node
horus log --level error safety

With -f/--follow, logs appear in real time as nodes emit them; without it, horus log prints the most recent entries (default 100, override with -n/--count) and exits. The CLI reads the same shared-memory log ring buffer that nodes publish into -- a dedicated global buffer, separate from the per-topic segments used for message IPC.


String Formatting

The C++ log API takes const char* or const std::string&. For formatted messages, build the string before logging:

// Using std::to_string
double temp = 72.5;
horus::log::info("sensor",
    ("Temperature: " + std::to_string(temp) + " C").c_str());

// Using snprintf for precise formatting
char buf[256];
std::snprintf(buf, sizeof(buf), "Position: (%.3f, %.3f, %.3f)", x, y, z);
horus::log::info("odom", buf);

// Using std::string concatenation
std::string msg = "Joint angles: [";
for (size_t i = 0; i < 6; ++i) {
    if (i > 0) msg += ", ";
    msg += std::to_string(angles[i]);
}
msg += "]";
horus::log::info("arm", msg);

BlackBox Recording

horus::blackbox::record publishes a Warn-level entry into the HORUS shared-memory log buffer, using category as the node name and prefixing the message with [blackbox] . Because it is Warn level it is also dual-written to the dedicated error buffer (500 slots, days of retention at typical rates), so a tagged entry outlives the process that wrote it and survives flooding by ordinary log traffic.

It is still a bounded ring buffer: entries are evicted as it wraps, and nothing is written to disk unless HORUS_LOG_FILE=true enables the log-file drain. These entries are not written to the scheduler's BlackBox and do not appear in horus blackbox. For a persistent, on-disk flight recorder, enable the scheduler's own recorder with sched.blackbox(size_mb), which writes .horus/blackbox/blackbox.wal.

record()

void horus::blackbox::record(const char* category, const char* message);
void horus::blackbox::record(const std::string& category, const std::string& message);

Publishes a Warn-level entry into the HORUS log buffer with category as the node name and the message prefixed [blackbox] .

Parameters

NameTypeDescription
categoryconst char* or const std::string&Recording node or subsystem (e.g., "safety", "motor_ctrl"). Shown as the entry's node name.
messageconst char* or const std::string&Event payload -- free-form text or a structured string such as JSON. Put the event name in here.
horus::blackbox::record("safety",
    "collision: {\"sensor\": \"bumper_front\", \"force_n\": 45.2}");

horus::blackbox::record("motor_ctrl",
    "overcurrent: {\"motor\": 3, \"current_a\": 12.5, \"limit_a\": 10.0}");

horus::blackbox::record("nav",
    "goal_reached: {\"goal\": \"charging_station\", \"error_m\": 0.02}");

Viewing Recorded Entries

record() entries land in the shared-memory log buffer at Warn level, so they are read back with horus log -- not horus blackbox:

# Recent warn-and-above entries -- record() entries appear here
horus log --level warn

# Filter by the category passed to record()
horus log --level warn safety

# Stream them as they arrive
horus log --level warn -f

Every message is prefixed [blackbox] , so piping through grep '\[blackbox\]' isolates them from ordinary warnings.

The Scheduler's BlackBox

horus blackbox reads a different store: the scheduler's on-disk flight recorder at .horus/blackbox/blackbox.wal, enabled with sched.blackbox(size_mb) from <horus/scheduler.hpp>. It contains scheduler-generated events only -- blackbox::record() entries never appear there.

# Dump scheduler flight-recorder events
horus blackbox

# Only anomalies (errors, deadline misses, budget violations, e-stops)
horus blackbox --anomalies

# Filter by event type -- an exact (case-insensitive) match against one of
# SchedulerStart, SchedulerStop, NodeAdded, NodeTick, NodeError, DeadlineMiss,
# BudgetViolation, LearningComplete, EmergencyStop, NetPeerDiscovered,
# NetPeerLost, NetReplicationStarted, NetImportRejected, Custom
horus blackbox --event DeadlineMiss

# Filter by node -- matches node-scoped events only (SchedulerStart, NodeAdded,
# NodeTick, NodeError, DeadlineMiss, BudgetViolation)
horus blackbox --node motor_ctrl

# Export for analysis
horus blackbox --json > crash_report.json

When to Use Log vs BlackBox

ScenarioUseWhy
"Motor initialized at 1000Hz"log::infoOperational status, evicted quickly from the main buffer -- that is fine
"IMU drift exceeds 1 deg/s"log::warnOperator should see it; Warn already lands in the error buffer
"Emergency stop triggered"BothOperator sees the error now; the [blackbox]-tagged copy is easy to grep out later
"Joint hit limit at 2.35 rad"blackbox::recordTagged for triage and retained in the longer-lived error buffer
"Collision detected, force=45N"blackbox::recordPhysical event worth marking as an incident record
"Starting path to waypoint 3"log::infoOperational, no triage value
"Motor overcurrent: 12.5A"BothSafety event visible now and tagged for triage

Rule of thumb: If you would want to pick it out of horus log --level warn while investigating why a robot stopped working, record it with blackbox::record. If it must survive on disk for a genuine post-mortem, enable the scheduler's BlackBox with sched.blackbox(size_mb) (or set HORUS_LOG_FILE=true to drain the log buffer to .horus/logs/).


Example: Structured Diagnostics

A motor controller that logs operational status and tags safety events for later triage.

#include <horus/log.hpp>
#include <horus/blackbox.hpp>
#include <cstdio>

struct MotorDiagnostics {
    double current_limit = 10.0;  // amps
    int overcurrent_count = 0;

    void check_motor(int motor_id, double current_a, double temp_c) {
        // Normal status -- log
        char buf[256];
        std::snprintf(buf, sizeof(buf),
            "Motor %d: %.1fA, %.1f C", motor_id, current_a, temp_c);
        horus::log::info("motor_diag", buf);

        // Warning threshold -- warn
        if (current_a > current_limit * 0.8) {
            std::snprintf(buf, sizeof(buf),
                "Motor %d approaching current limit: %.1fA / %.1fA",
                motor_id, current_a, current_limit);
            horus::log::warn("motor_diag", buf);
        }

        // Overcurrent -- error + blackbox
        if (current_a > current_limit) {
            overcurrent_count++;
            std::snprintf(buf, sizeof(buf),
                "Motor %d overcurrent: %.1fA (limit %.1fA)",
                motor_id, current_a, current_limit);
            horus::log::error("motor_diag", buf);

            // Blackbox: structured data tagged for later triage
            char event_data[512];
            std::snprintf(event_data, sizeof(event_data),
                "overcurrent: {\"motor\": %d, \"current_a\": %.2f, \"limit_a\": %.2f, "
                "\"temp_c\": %.1f, \"count\": %d}",
                motor_id, current_a, current_limit, temp_c, overcurrent_count);
            horus::blackbox::record("motor_diag", event_data);
        }

        // Thermal warning
        if (temp_c > 80.0) {
            std::snprintf(buf, sizeof(buf),
                "Motor %d thermal warning: %.1f C", motor_id, temp_c);
            horus::log::warn("motor_diag", buf);

            char event_data[256];
            std::snprintf(event_data, sizeof(event_data),
                "thermal_warning: {\"motor\": %d, \"temp_c\": %.1f}", motor_id, temp_c);
            horus::blackbox::record("motor_diag", event_data);
        }
    }
};

Best Practices

Do:

  • Use the node name consistently -- match the name registered with the scheduler
  • Keep messages concise -- include the key metric, not a paragraph
  • Use blackbox for any event you would want during incident investigation
  • Include units in numeric values ("45.2N", "12.5A", "2.35rad")

Avoid:

  • Logging every tick at info level -- this floods the log at 1000Hz. Log periodic summaries instead
  • Logging inside tight loops without rate limiting
  • Using error level for non-errors (e.g., "no new data this tick" is normal, not an error)
  • Putting sensitive data (passwords, keys) in logs or blackbox