Log from your code
The stack logs through core_logger, not through RCLCPP_*, printf or
std::cout. One logger means one place to set levels, one format, and output
that reaches both the console and rosout.
Note
core_utils is the deliberate exception — it sits below the logger and
cannot depend on it.
Basic use
#include <core_logger/log.hpp>
LOG_INFO("arm enabled");
LOG_INFO_STREAM("moving to destination " << destination);
LOG_WARN_STREAM("retrying after " << delay_s << " s");
LOG_ERROR_STREAM("plan failed: " << ErrorCodes::ToString(code));
Levels, quietest to loudest: FATAL, ERROR, WARN, INFO,
DEBUG, VERBOSE. Each has a plain and a _STREAM form, and a
_NAMED / _STREAM_NAMED variant that takes an explicit logger name.
Set the level at runtime:
CORE_LOG_LEVEL=VERBOSE ros2 launch <app>_bringup <app>.launch.py
Choosing a level
The test is who reads the line and how often it fires.
Level |
Use for |
|---|---|
|
The operation failed and the caller must handle it. |
|
Something unexpected that the system recovered from. Not for outcomes
that are routine — a planner returning “start and goal too close” when the
arm is already at the goal is expected, and belongs at |
|
Milestones an operator would want in a normal run. One line per phase, not per trajectory point. |
|
Detail for diagnosing a specific subsystem. |
|
Per-iteration and per-message detail, including protocol traffic. |
Warning
Anything that fires per trajectory point or per network message belongs at
VERBOSE. A per-point INFO line makes the console unusable and hides
the lines that matter.
In headers
The bare LOG_* macros do not work in a header that is included from another
package. They resolve ::logger, a non-dependent name that may not exist at
the include site. Use the _NAMED form and pass the logger explicitly:
// in a header
LOG_INFO_STREAM_NAMED("my_component", "value: " << value);
Inside ROS nodes
core_logger’s RclutilsAppender bridges log output onto rosout, so
ros2 topic echo /rosout and Foxglove see the same lines as the console.
Warning
Detach the appender before the node shuts down or the plugin unloads.
dlclose on a plugin and rclcpp shutdown both segfault the host process
if an attached appender outlives the context it writes to.
ScopedRootLogger handles this — it attaches on construction and detaches on
destruction. Prefer it to attaching by hand.
Checking your work
Before opening a PR, confirm nothing slipped back in:
grep -rnE 'RCLCPP_(INFO|WARN|ERROR|DEBUG|FATAL)|printf\(|std::cout' \
--include=*.cpp --include=*.hpp src/ | grep -v core_utils