3、日志过滤与条件输出:使用条件宏、日志级别阈值设置

日志这东西,说白了就是自动驾驶系统的「黑匣子」。但黑匣子要是啥都记,那跟没记一样。我见过太多团队,一开日志就是海量输出,刷屏刷到连关键错误都找不着。今天咱们聊聊怎么让日志「该出声时出声,该闭嘴时闭嘴」。

3.1 日志级别:你的第一道过滤器

ROS2 的日志系统自带一套级别体系。我个人习惯把这套级别理解成「警报等级」——从 DEBUG 到 FATAL,越往后越严重。

级别 宏名称 典型用途
DEBUG RCLCPP_DEBUG 调试信息,比如传感器原始数据
INFO RCLCPP_INFO 正常流程提示,比如节点启动成功
WARN RCLCPP_WARN 异常但可恢复,比如丢了一帧数据
ERROR RCLCPP_ERROR 功能受损,比如传感器断连
FATAL RCLCPP_FATAL 系统崩溃,比如内存溢出

你想想看,如果生产环境开着 DEBUG 级别跑,那日志文件一天能撑爆硬盘。我在项目中遇到过,同事把日志级别设成 DEBUG 就上线了,结果跑了半天,磁盘直接写满,整个系统挂了。嗯,从那以后我每次上线前都会检查日志级别配置。

3.2 条件宏:只在需要时打印

有时候我们只想在特定条件下打日志。比如,只在速度超过 80km/h 时打印警告。ROS2 提供了条件宏,用起来很顺手。

// 只在条件为 true 时打印
RCLCPP_WARN_THROTTLE(rclcpp::get_logger("controller"), 
                     get_clock(), 1000, 
                     "速度过高: %.2f km/h", speed);

// 或者用更直接的条件宏
if (speed > 80.0) {
  RCLCPP_WARN(rclcpp::get_logger("controller"), 
              "超速警告: %.2f km/h", speed);
}

这里有个小技巧:RCLCPP_WARN_THROTTLE 后面的 1000 是节流时间(毫秒)。意思是同样的警告,1 秒内最多打印一次。我在调试路径规划时,经常用这个宏——不然控制频率一高,日志刷得你根本看不清。

我的习惯: 调试阶段用 DEBUG + 条件宏,生产阶段只保留 WARN 及以上。这样既不影响性能,又能抓住关键问题。

3.3 日志级别阈值:运行时动态调整

ROS2 允许你在运行时修改日志级别。这个功能太实用了。比如,车在正常跑时我只想看 ERROR,但出了事故,我想立刻切到 DEBUG 看细节。

// 设置全局日志级别
rclcpp::Logger::set_level(rclcpp::Logger::Level::WARN);

// 或者只针对某个节点
auto logger = rclcpp::get_logger("sensor_fusion");
logger.set_level(rclcpp::Logger::Level::DEBUG);

我曾经在实车测试时遇到一个诡异问题:传感器融合偶尔输出错误位姿。但 ERROR 级别日志啥也没记,因为错误被内部处理了。后来我远程把 sensor_fusion 节点的日志级别降到 DEBUG,立刻抓到了问题——原来是某个滤波器参数在特定工况下发散。你看,动态调级别就是救命稻草。

3.4 条件宏的高级用法:频率控制与采样

除了基本的条件宏,ROS2 还提供了几种「聪明」的打印方式:

  • RCLCPP_*_ONCE:整个生命周期只打印一次。适合初始化信息。
  • RCLCPP_*_SKIPFIRST:跳过第一次,从第二次开始打印。适合过滤启动时的瞬态数据。
  • RCLCPP_*_THROTTLE:按频率节流,避免刷屏。
// 只打印一次
RCLCPP_INFO_ONCE(rclcpp::get_logger("system"), 
                 "系统初始化完成");

// 跳过前 5 次
static int count = 0;
if (++count > 5) {
  RCLCPP_WARN(rclcpp::get_logger("planner"), 
              "规划失败次数: %d", count);
}
避坑指南: 我曾经在循环里用 RCLCPP_INFO 打印路径点,结果每帧打印几百条,直接把日志系统拖垮了。后来改用 THROTTLE 宏,每 500ms 打印一次,问题解决。记住:高频场景下,节流不是可选项,是必选项。

3.5 实战:搭建日志过滤系统

下面是我个人在项目中常用的日志配置模板。它结合了级别阈值和条件宏,适合自动驾驶这种多节点、高频场景。

// 日志配置结构体
struct LogConfig {
  rclcpp::Logger::Level level;
  int throttle_ms;
  bool enable_once;
};

// 示例:传感器节点日志配置
LogConfig sensor_config = {
  .level = rclcpp::Logger::Level::WARN,
  .throttle_ms = 500,
  .enable_once = false
};

// 应用配置
auto logger = rclcpp::get_logger("sensor_node");
logger.set_level(sensor_config.level);

// 条件打印示例
if (sensor_config.enable_once) {
  RCLCPP_INFO_ONCE(logger, "传感器节点已启动");
} else {
  RCLCPP_INFO_THROTTLE(logger, get_clock(), 
                       sensor_config.throttle_ms, 
                       "传感器数据帧率: %d Hz", fps);
}

这个模板的好处是:所有日志行为集中管理。你改一个配置文件,整个节点的日志行为就变了。我在做多传感器融合时,用这套方案同时管理了激光雷达、摄像头、毫米波雷达三个节点的日志,调试效率提升不少。

3.6 知识体系图

下面这张图总结了本章的核心逻辑。说白了就是:日志级别决定「能不能打」,条件宏决定「什么时候打」,两者配合,才能做到精准输出。

日志过滤与条件输出核心逻辑 日志级别阈值 DEBUG / INFO / WARN ERROR / FATAL 条件宏控制 THROTTLE / ONCE SKIPFIRST / 自定义条件 精准输出 关键原则 1. 生产环境:WARN 及以上,避免日志洪流 2. 调试阶段:DEBUG + 条件宏,精准定位问题 3. 高频场景:必须使用 THROTTLE 节流,否则系统崩溃

嗯,到这里你应该明白了。日志过滤不是「要不要打」的问题,而是「怎么打得聪明」的问题。级别阈值是粗筛,条件宏是精筛,两者配合,你才能在自动驾驶这种复杂系统里,快速定位问题,而不是被日志淹没。

一句话总结: 日志级别决定「能不能打」,条件宏决定「什么时候打」。别让日志成为系统的负担,让它成为你的眼睛。

可可精品资料库