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 -- Persistent flight recorder for post-mortem analysis. Use for events that matter after a crash or anomaly.
#include <horus/log.hpp>

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(node, event, data)Record an event to the flight recorder

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:

# Stream all logs
horus log

# Filter by node
horus log --node motor_ctrl

# Filter by level
horus log --level warn

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

Logs appear in real-time as nodes emit them. The CLI connects to the same SHM transport that nodes use for 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

The blackbox is a persistent flight recorder. Unlike logs, blackbox entries survive crashes and are designed for post-mortem analysis. Think of it as the "black box" on an aircraft.

record()

void horus::blackbox::record(const char* node, const char* event, const char* data);

Records a structured event to the flight recorder.

Parameters

NameTypeDescription
nodeconst char*Node that recorded the event.
eventconst char*Event category (e.g., "collision", "estop", "joint_limit").
dataconst char*Event payload -- typically JSON or a structured string.
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 BlackBox Data

# Dump all blackbox entries
horus blackbox dump

# Filter by node
horus blackbox dump --node safety

# Filter by event type
horus blackbox dump --event collision

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

When to Use Log vs BlackBox

ScenarioUseWhy
"Motor initialized at 1000Hz"log::infoOperational status, no post-mortem value
"IMU drift exceeds 1 deg/s"log::warnOperator should see it, but not a crash event
"Emergency stop triggered"BothOperator sees it now (log), investigators see it later (blackbox)
"Joint hit limit at 2.35 rad"blackbox::recordCritical for root-cause analysis after incident
"Collision detected, force=45N"blackbox::recordPhysical event that must be preserved
"Starting path to waypoint 3"log::infoOperational, no forensic value
"Motor overcurrent: 12.5A"BothSafety event visible now and preserved

Rule of thumb: If you would want to see it when investigating why a robot stopped working 3 hours ago, record it in the blackbox.


Example: Structured Diagnostics

A motor controller that logs operational status and records safety events for post-mortem analysis.

#include <horus/log.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 for post-mortem
            char event_data[512];
            std::snprintf(event_data, sizeof(event_data),
                "{\"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", "overcurrent", 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),
                "{\"motor\": %d, \"temp_c\": %.1f}", motor_id, temp_c);
            horus::blackbox::record("motor_diag", "thermal_warning", 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