Logging & BlackBox API
HORUS provides two complementary recording mechanisms:
horus::log-- Structured log messages visible in thehorus logCLI. 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
| Function | Level | Use Case |
|---|---|---|
horus::log::info(node, msg) | Info | Normal operation, milestones |
horus::log::warn(node, msg) | Warn | Degraded operation, approaching limits |
horus::log::error(node, msg) | Error | Failures, safety events |
BlackBox
| Function | Purpose |
|---|---|
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
| Name | Type | Description |
|---|---|---|
node | const char* or const std::string& | Name of the node emitting the log. Should match the node's registered name. |
msg | const 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
| Level | Constant | When to Use |
|---|---|---|
| Info | 0 | Normal operation: startup, milestones, periodic status |
| Warn | 1 | Something is wrong but the system continues: sensor drift, low battery, approaching limits |
| Error | 2 | Something 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
| Name | Type | Description |
|---|---|---|
category | const char* or const std::string& | Recording node or subsystem (e.g., "safety", "motor_ctrl"). Shown as the entry's node name. |
message | const 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
| Scenario | Use | Why |
|---|---|---|
| "Motor initialized at 1000Hz" | log::info | Operational status, evicted quickly from the main buffer -- that is fine |
| "IMU drift exceeds 1 deg/s" | log::warn | Operator should see it; Warn already lands in the error buffer |
| "Emergency stop triggered" | Both | Operator sees the error now; the [blackbox]-tagged copy is easy to grep out later |
| "Joint hit limit at 2.35 rad" | blackbox::record | Tagged for triage and retained in the longer-lived error buffer |
| "Collision detected, force=45N" | blackbox::record | Physical event worth marking as an incident record |
| "Starting path to waypoint 3" | log::info | Operational, no triage value |
| "Motor overcurrent: 12.5A" | Both | Safety 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