历史快照
本专题页写于包拆分与 FAST-LIVO2 / CustomMsg / 图像输入落地之前,反映当时的单包结构与
五前端状态。已知过时点:目录布局(apps/launch 现属 asuka_ros 包)、共享的
config_sensor.json(外参已改为各前端 odometry 配置的 sensor 块)、ROS 层 acc_scale
等描述、前端数量(现六种,新增 fastlivo)、CustomMsg 与图像输入链路。2026-09 下旬以来:lightning / superodom 前端与 optimization_miao 后端已移除
(现五前端:fastlio / batchlio / superlio / fastlivo / srlivo;smallpointlio 已于 2026-10-23 移除,odometry/10-11 为其历史快照);
freedom 扩展已整体迁出 asuka(移入 nagisa 包);KeyFrame::stamp 统一为扫描
起始时刻;执行器新增连续失败熔断与 wait_for() 排空检测、扩展与执行器有
ok() 健康查询。
现行事实以主文档 (index / architecture / parameters / odometry 等)与源码为准。
title: "日志系统" description: >- 基于 spdlog 的模块化日志(create_module_logger),为 ROS 封装、算法与扩展模块提供统一输出通道与级别控制。
日志系统
模块职责
日志系统是 asuka 包里最薄的一层公共设施,全部实现集中在两个文件:
- 接口:
src/asuka/include/asuka/utility/logging.hpp(708 B,仅 27 行) - 实现:
src/asuka/src/asuka/utility/logging.cpp(4677 B,129 行)
它基于 spdlog 提供四件事:
- 模块化命名 logger:
create_module_logger(module_name)为每个调用方(ROS 封装、算法插件、扩展模块)创建/复用一个以模块名注册的 spdlog logger,是全仓库统一的日志入口; - 统一输出通道装配:每个模块 logger 默认挂三类 sink——进程级共享的 ringbuffer_sink、可选的彩色控制台 sink、可选的滚动/普通文件 sink;
- 级别控制:
log_level_from_string把配置字符串映射为 spdlog 级别,configure_logging在启动阶段对全局及所有已注册 logger 做一次级别广播; - 默认 logger 的存取:
get/set_default_logger直接转发到 spdlog 的默认 logger,允许整体替换。
配置契约
所有行为由 config.json 顶层 logging 段驱动(经 GlobalConfig 的 param<T> 读取,与配置系统共用一套访问方式):
| 配置键 | 代码默认值 | 作用 |
|---|---|---|
logging_level |
"info" |
基础级别,作用于全局、ringbuffer sink 与文件 sink |
console_output |
true |
是否为模块 logger 挂控制台 sink |
console_level |
"info" |
控制台 sink 的独立级别,可与 logging_level 不同 |
file_output |
true |
是否写文件 |
rotate_logs |
true |
true 用 rotating_file_sink,false 用 basic_file_sink |
max_file_size_kb |
8192 |
单文件上限(KB) |
max_files |
10 |
滚动保留份数 |
logging_dir |
"/tmp" |
日志目录,不存在则递归创建 |
仓库当前实际配置(config/config.json#L8-L17)全部启用,目录指向 /share/pilot_ws/src/asuka/logs,级别均为 info,8 MB × 10 份滚动。
create_module_logger 装配流程
这是整个模块的核心函数(logging.cpp#L68-L126),一次调用完成"查缓存 → 读配置 → 建 sink → 定级别":
flowchart TD
A["create_module_logger(name)"] --> B{"spdlog::get(name)<br/>已注册?"}
B -- 是 --> C["直接返回已有 logger<br/>(幂等)"]
B -- 否 --> D["读 logging 配置段:<br/>logging_dir / logging_level /<br/>console_output / console_level / file_output ..."]
D --> E["目录不存在则<br/>create_directories(logging_dir)"]
E --> F["new spdlog::logger(name)<br/>并 spdlog::register_logger"]
F --> G["挂载进程级共享 ringbuffer_sink<br/>(默认 128 条, 级别=logging_level)"]
G --> H{"console_output?"}
H -- 是 --> I["挂载 stdout_color_sink_mt<br/>级别 = console_level"]
H -- 否 --> J{"file_output?"}
I --> J
J -- 是 --> K{"rotate_logs?"}
K -- "true" --> L["rotating_file_sink_mt<br/>asuka_<name>.log<br/>max_file_size_kb / max_files"]
K -- "false" --> M["basic_file_sink_mt<br/>asuka_<name>.log"]
L --> N["logger 级别 =<br/>min(logging_level, console_level)"]
M --> N
J -- 否 --> N
N --> O["返回 logger"]
关键节点说明:
spdlog::get(name)缓存命中(L69-L72):同名模块重复构造(例如扩展模块被多次实例化)不会重复建 sink,直接返回注册表里的既有实例;- 文件名特例(L76):模块名为
"asuka"时文件名取"main",其余直接用模块名,最终落盘为<logging_dir>/asuka_<name>.log(如asuka_ros.log、asuka_rviz.log); - 共享 ringbuffer sink(L91-L93):
get_ringbuffer_sink是进程级单例(匿名命名空间静态变量,L18-L20),所有模块 logger 共享同一个内存环形缓冲,首调用以默认 128 条容量创建,之后传入的buffer_size被忽略;该函数在公开头文件中导出,为外部(如崩溃处理)读取最近日志预留了接缝; - 双层级别过滤:每个 sink 各自带级别,logger 自身级别取
min(logging_level, console_level)(L119-L123)。效果是:低于logging_level的消息任何 sink 都收不到;控制台额外受console_level约束。例如logging_level=info, console_level=debug时,debug 消息只上屏、不进文件与环形缓冲。
configure_logging(L44-L66)是另一条独立的初始化路径:读一次 logging_level 后通过 spdlog::set_level + apply_all 广播到所有已注册 logger;若 console_output=false,则用 dynamic_cast 识别并从默认 logger 的 sink 列表中剔除 stdout sink(同时检查 _mt 与 _st 两种)。
使用方与调用链
证据中可见的两类调用方:
- ROS 封装层:
AsukaROS构造函数第二件事就是logger = create_module_logger("ros")(asuka_ros.cpp#L23),随后所有关键路径都经由它输出——IMU/雷达时间回环告警(logger->warn("lidar/imu loop back detected..."),L174、L179)、地图保存结果(logger->info("saved map...")与logger->warn("failed to save map..."),L150、L152); - 扩展模块:
RvizViewer构造函数同样以create_module_logger("rviz")获得自己的输出通道(rviz_viewer.cpp#L10)。
按页面定位,五种里程计算法插件与其它扩展模块共用同一入口——任何模块只要在构造期调用 create_module_logger("<自己的名字>"),即可自动获得"环形缓冲 + 可选控制台 + 可选滚动文件"的完整通道,无需各自管理文件句柄或级别。
关键状态
- spdlog 全局注册表:
create_module_logger的幂等性完全依赖spdlog::get/register_logger,注册表内没有的 logger 才会新建; - 进程级 ringbuffer 单例:匿名命名空间内的静态
shared_ptr,生命周期与进程一致; - 配置快照:logger 的 sink 组合与级别在首次创建时从 GlobalConfig 读定,之后
create_module_logger再次被调用只会返回缓存,不会重读配置。
边界条件与风险
- 级别字符串容错:
log_level_from_string做小写归一后匹配trace/debug/info/warn|warning/error/critical/off,未知字符串静默回退为 info,配置拼写错误不会有任何提示; - ringbuffer 惰性初始化无锁:
get_ringbuffer_sink的if (!ringbuffer_sink)检查没有互斥保护,多线程同时首次调用存在理论竞争;实际使用中各模块通常在主线程构造期串行创建 logger,风险不触发; console_output=false的作用范围有限:configure_logging只摘除默认 logger 上的 stdout sink;已通过create_module_logger创建的模块 logger 若此前挂了控制台 sink,不会被这次清理触及(新创建的模块 logger 则因读到console_output=false自然不带控制台);- 日志目录可写性:
logging_dir不存在会自动create_directories,但挂载点只读或磁盘满时文件 sink 构造会抛异常,代码未在此处兜底; - 文件名前缀固定
asuka_:多进程若共用同一logging_dir会写同名文件,滚动 sink 并发写同一文件属未定义行为,部署时需隔离目录。
扩展点
- 新 sink 类型:
create_module_logger是唯一的 sink 装配点,接入网络/远程 sink 只需在此追加分支,调用方零改动; - 崩溃日志导出:
get_ringbuffer_sink公开导出且全模块共享,天然的"最近 N 条日志"内存快照,可被终止处理函数读取落盘; - 按模块分级:当前
logging_level全局生效,若需按模块控制级别,可在create_module_logger内增加<module>_level之类的配置键读取,接口形态无需变化。
Sources:
- src/asuka/include/asuka/utility/logging.hpp#L1-L27
- src/asuka/src/asuka/utility/logging.cpp#L1-L129
- src/asuka/config/config.json#L1-L18
- src/asuka/src/asuka/asuka_ros.cpp#L19-L43
- src/asuka/src/asuka/asuka_ros.cpp#L118-L183
- src/asuka/src/asuka/modules/rviz_viewer.cpp#L9-L35
关联源码
src/asuka/src/asuka/utility/logging.cppsrc/asuka/include/asuka/utility/logging.hpp