2、日志系统架构:Carla日志框架、日志级别与格式解析

大家好,我是你们的仿真日志分析讲师。今天我们来聊聊Carla仿真中一个特别基础、但又特别容易被忽视的东西——日志系统。

说实话,我见过太多同学一上来就闷头跑仿真,出了问题两眼一抹黑。日志就在那里,但没人去看。为什么?因为看不懂,或者觉得没必要。嗯,今天我就带你把这块硬骨头啃下来。

2.1 Carla日志框架:Python logging模块

Carla的日志系统,底层用的是Python标准库里的logging模块。你可能会问:为什么不用print?

我刚开始做仿真的时候,也喜欢用print。简单粗暴嘛。但后来发现,print出来的东西,你没法控制它的输出级别,也没法把它写到文件里。更麻烦的是,一旦仿真跑起来,print的输出会跟Carla引擎的输出混在一起,根本分不清谁是谁。

logging模块就不一样了。它提供了几个核心组件:

  • Logger:日志记录器,你代码里调用的那个对象
  • Handler:处理器,决定日志往哪写(控制台、文件、网络等)
  • Formatter:格式化器,决定日志长什么样
  • Filter:过滤器,决定哪些日志能通过

在Carla里,你打开Python API的源码,会看到类似这样的初始化代码:

import logging

# 创建一个logger
logger = logging.getLogger('carla')
logger.setLevel(logging.DEBUG)

# 创建一个控制台handler
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

# 创建一个文件handler
fh = logging.FileHandler('carla_simulation.log')
fh.setLevel(logging.INFO)

# 定义格式
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
fh.setFormatter(formatter)

# 把handler加到logger上
logger.addHandler(ch)
logger.addHandler(fh)

你看,这样配置之后,DEBUG级别的日志会输出到控制台,INFO及以上的会写到文件里。各司其职,互不干扰。

我的习惯: 我个人习惯把DEBUG日志写到文件里,控制台只显示INFO及以上。这样调试的时候翻文件,跑仿真的时候看控制台,两不耽误。

2.2 日志级别:从DEBUG到CRITICAL

Carla沿用了Python logging的五个标准级别。我列个表,你一看就明白:

级别 数值 什么时候用
DEBUG 10 调试信息,比如传感器数据、坐标变换、每帧状态
INFO 20 正常流程信息,比如「车辆已生成」「仿真开始」
WARNING 30 可能有问题但不影响运行,比如「传感器未找到,使用默认配置」
ERROR 40 出错了但还能继续,比如「交通灯数据解析失败,跳过该帧」
CRITICAL 50 严重错误,仿真无法继续,比如「Carla服务器连接丢失」

这里有个关键点:日志级别是有继承关系的。你设置logger.setLevel(logging.WARNING),那么WARNING、ERROR、CRITICAL都会输出,但DEBUG和INFO会被过滤掉。

为什么会这样?说白了,就是怕你被海量信息淹死。你想想看,一个仿真跑起来,每帧都有几十上百条DEBUG日志,要是全打印到控制台,你根本找不到重点。

避坑指南: 我曾经在调试一个传感器融合算法时,把日志级别设成了DEBUG,然后直接跑了一个小时的仿真。结果日志文件写了2个多G,打开都费劲。从那以后,我养成了一个习惯:调试阶段用DEBUG,但一定要写到文件里;正式跑仿真时,控制台只保留INFO及以上。

2.3 日志格式解析:看懂每一行

好,现在日志有了,但你看得懂吗?我们来看一个典型的Carla日志行:

2025-01-15 14:23:45,123 - carla.vehicle - WARNING - Vehicle 42 is out of the map, teleporting to spawn point

这一行里包含了几个关键信息:

  • 时间戳2025-01-15 14:23:45,123,精确到毫秒。这个在排查时序问题时特别有用。
  • 日志器名称carla.vehicle,告诉你这条日志来自哪个模块。Carla里常见的名称有carla.clientcarla.sensorcarla.traffic等。
  • 日志级别WARNING,告诉你这条日志的严重程度。
  • 消息内容Vehicle 42 is out of the map...,具体发生了什么。

你可能会问:这个格式是怎么定义的?其实就是前面代码里的那一行:

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

常见的格式占位符还有这些:

占位符 含义 示例
%(asctime)s 时间戳 2025-01-15 14:23:45,123
%(name)s 日志器名称 carla.vehicle
%(levelname)s 日志级别 WARNING
%(message)s 日志内容 Vehicle 42 is out of the map
%(filename)s 文件名 vehicle_manager.py
%(lineno)d 行号 128
%(funcName)s 函数名 update_vehicle_state

我个人习惯在调试阶段加上%(filename)s%(lineno)d,这样一旦报错,我能立刻定位到是哪一行代码出的问题。但正式跑仿真时我会去掉,因为文件路径和行号会让日志变得很长,影响阅读。

核心要点: 日志格式不是越全越好,而是够用就好。你需要在「信息完整」和「可读性」之间找到平衡。我的建议是:调试时用详细格式,上线时用简洁格式。

2.4 实战小技巧:自定义你的日志格式

最后,我分享一个我自己常用的日志配置模板。你可以直接拿去用:

import logging
import sys

def setup_carla_logger(name='carla', level=logging.INFO):
    logger = logging.getLogger(name)
    logger.setLevel(level)
    
    # 控制台handler:只显示INFO及以上
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setLevel(logging.INFO)
    console_format = logging.Formatter(
        '%(asctime)s | %(levelname)-8s | %(message)s',
        datefmt='%H:%M:%S'
    )
    console_handler.setFormatter(console_format)
    
    # 文件handler:记录所有级别,包含详细信息
    file_handler = logging.FileHandler('carla_debug.log')
    file_handler.setLevel(logging.DEBUG)
    file_format = logging.Formatter(
        '%(asctime)s | %(name)s | %(levelname)-8s | %(filename)s:%(lineno)d | %(message)s'
    )
    file_handler.setFormatter(file_format)
    
    logger.addHandler(console_handler)
    logger.addHandler(file_handler)
    
    return logger

# 使用
log = setup_carla_logger()
log.info('仿真开始')
log.debug('传感器数据: x=1.23, y=4.56')

你看,控制台输出简洁明了,文件里则保留了完整的调试信息。这样既不会在控制台被刷屏,又能在需要时翻文件查细节。

嗯,日志系统这块,今天就讲到这里。下一节我们会聊怎么通过日志来定位仿真中的常见故障。到时候我会拿几个我实际踩过的坑来举例,保证让你印象深刻。