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命令行工具实时查看日志。到时候你会发现,日志系统远比你想象的强大。