2、日志系统入门:rclcpp日志宏、日志级别设置、日志输出格式定制
说到调试,日志系统绝对是咱们ROS2开发者的第一道防线。我个人习惯,写任何节点之前,先把日志框架搭好。为什么?因为等程序跑起来出了问题,你再回头加日志,那感觉就像在迷宫里现挖墙——费劲不说,还容易漏掉关键信息。
ROS2的日志系统,说白了就是一套帮你把「程序运行时发生了什么」记录下来的工具。它比你在代码里随手写std::cout要强大得多。你想想看,cout只能往终端输出,而ROS2的日志可以控制级别、可以过滤、可以定制格式,甚至能重定向到文件。
2.1 rclcpp日志宏:最常用的调试利器
在rclcpp里,日志输出是通过一组宏来实现的。这些宏长得挺像,但功能各有侧重。我刚开始用的时候也经常搞混,后来总结了一个规律:宏的名字直接告诉你它的用途。
最常用的几个宏如下:
| 宏名称 | 用途 | 示例 |
|---|---|---|
RCLCPP_INFO |
输出常规信息 | RCLCPP_INFO(rclcpp::get_logger("my_node"), "节点已启动"); |
RCLCPP_WARN |
输出警告信息 | RCLCPP_WARN(rclcpp::get_logger("my_node"), "参数未设置,使用默认值"); |
RCLCPP_ERROR |
输出错误信息 | RCLCPP_ERROR(rclcpp::get_logger("my_node"), "连接超时"); |
RCLCPP_DEBUG |
输出调试信息 | RCLCPP_DEBUG(rclcpp::get_logger("my_node"), "当前循环次数: %d", count); |
RCLCPP_FATAL |
输出致命错误 | RCLCPP_FATAL(rclcpp::get_logger("my_node"), "内存分配失败,程序退出"); |
每个宏的第一个参数都是日志器(logger),第二个参数是格式化字符串,后面跟可变参数。嗯,这里要注意:日志器名字建议和节点名保持一致,这样排查问题时一眼就能看出是哪个节点输出的。
我的经验:在写回调函数时,我习惯在关键路径上加上RCLCPP_DEBUG。平时运行时DEBUG级别默认不显示,不影响性能。等出了问题,把日志级别调到DEBUG,就能看到完整的执行轨迹。这招帮我省了不少排查时间。
2.2 日志级别设置:该看什么,不该看什么
日志级别,说白了就是给日志信息分个优先级。ROS2定义了5个级别,从低到高分别是:DEBUG、INFO、WARN、ERROR、FATAL。
默认情况下,只有INFO及以上级别的日志才会显示。这意味着你写的RCLCPP_DEBUG信息,默认是看不见的。为什么会这样?因为DEBUG信息量太大,如果全显示出来,终端会被刷屏,反而干扰你关注真正的问题。
那怎么调整日志级别呢?有两种方式:
方式一:通过环境变量设置(推荐)
启动节点前设置环境变量,对所有节点生效:
export RCUTILS_CONSOLE_OUTPUT_FORMAT="[{severity}] [{name}]: {message}"
export RCUTILS_LOGGING_SEVERITY_THRESHOLD=DEBUG
ros2 run my_package my_node
方式二:在代码中动态设置
有时候你只想调整某个特定节点的日志级别,可以在代码里写:
auto ret = rcutils_logging_set_logger_level(
rclcpp::get_logger("my_node").get_name(),
RCUTILS_LOG_SEVERITY_DEBUG);
if (ret != RCUTILS_RET_OK) {
RCLCPP_ERROR(rclcpp::get_logger("my_node"), "设置日志级别失败");
}
我曾经踩过的坑:有一次我在代码里把日志级别设成了DEBUG,但终端上死活看不到DEBUG信息。查了半天才发现,环境变量RCUTILS_LOGGING_SEVERITY_THRESHOLD被设成了INFO,代码里的设置被环境变量覆盖了。记住:环境变量的优先级高于代码设置。
2.3 日志输出格式定制:让信息一目了然
默认的日志格式长这样:
[INFO] [my_node]: 节点已启动
说实话,够用,但不够好用。尤其当你同时跑多个节点时,光看这个格式很难快速定位问题。我个人习惯把时间戳、文件名、行号都加上去。
定制格式主要通过环境变量RCUTILS_CONSOLE_OUTPUT_FORMAT来实现。支持的占位符如下:
| 占位符 | 含义 | 示例输出 |
|---|---|---|
{severity} |
日志级别 | INFO, WARN, ERROR |
{name} |
日志器名称 | my_node |
{message} |
日志内容 | 节点已启动 |
{function} |
函数名 | main |
{line} |
行号 | 42 |
{file} |
文件名 | my_node.cpp |
{time} |
时间戳 | 2024-01-15 10:30:45.123 |
举个例子,我常用的格式是:
export RCUTILS_CONSOLE_OUTPUT_FORMAT="[{severity}] [{time}] [{name}]({file}:{line}): {message}"
输出效果:
[INFO] [2024-01-15 10:30:45.123] [my_node](my_node.cpp:42): 节点已启动
小技巧:如果你在调试一个偶发性的bug,建议把{function}和{line}加上。这样日志里直接告诉你「哪行代码出了问题」,省得你再去代码里搜字符串。我有个同事,每次出问题都靠这个定位,效率极高。
2.4 实战:一个完整的日志配置示例
说了这么多,咱们来写一个完整的例子。假设你有一个节点,需要记录传感器数据的处理过程:
#include <rclcpp/rclcpp.hpp>
class SensorNode : public rclcpp::Node
{
public:
SensorNode() : Node("sensor_node")
{
// 设置日志级别为DEBUG
rcutils_logging_set_logger_level(
get_logger().get_name(),
RCUTILS_LOG_SEVERITY_DEBUG);
RCLCPP_INFO(get_logger(), "传感器节点初始化完成");
timer_ = this->create_wall_timer(
std::chrono::seconds(1),
std::bind(&SensorNode::timer_callback, this));
}
private:
void timer_callback()
{
// 模拟传感器数据
float temperature = 25.5f;
RCLCPP_DEBUG(get_logger(), "采集到温度数据: %.2f°C", temperature);
if (temperature > 30.0f) {
RCLCPP_WARN(get_logger(), "温度过高: %.2f°C", temperature);
}
if (temperature > 50.0f) {
RCLCPP_ERROR(get_logger(), "温度异常,需要紧急处理");
}
}
rclcpp::TimerBase::SharedPtr timer_;
};
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<SensorNode>());
rclcpp::shutdown();
return 0;
}
启动时,你可以这样定制输出格式:
export RCUTILS_CONSOLE_OUTPUT_FORMAT="[{severity}] [{time}] [{name}]: {message}"
ros2 run sensor_pkg sensor_node
输出效果:
[INFO] [2024-01-15 10:30:45.123] [sensor_node]: 传感器节点初始化完成
[DEBUG] [2024-01-15 10:30:46.124] [sensor_node]: 采集到温度数据: 25.50°C
[DEBUG] [2024-01-15 10:30:47.125] [sensor_node]: 采集到温度数据: 25.60°C
你看,有了时间戳和级别信息,整个运行过程一目了然。如果哪天温度异常,WARN和ERROR级别的日志会立刻引起你的注意。
好了,日志系统入门就讲到这里。下一章咱们聊聊更高级的用法——如何把日志重定向到文件,以及如何用ros2命令行工具实时查看日志。到时候你会发现,日志系统远比你想象的强大。