SpringBoot中logback日志丢失原因排查与解决做法

作者:袖梨 2026-08-11

生产环境排查重复消费问题,明明代码里 catch 住了异常并打了 log.warn,却在日志文件里死活找不到。控制台输出又因容器化部署而丢失,问题久久无法定位。本文记录一次真实排查过程,根因出在 logback 的 root loggerBIZ logger 配置差异上。

SpringBoot中logback日志丢失原因排查与解决方法

一、问题背景

Kafka 消费者接收设备事件消息后,需要落库到本地 device_event_message 表。该表的 msg_service_business_id 字段上有唯一索引,用于防止 Kafka 重复消费导致的数据重复。

代码逻辑很标准:try 里执行 insertcatch (Exception e)log.warn 打印日志并吞掉异常,不阻断后续推送流程。

@Service@Slf4jpublic class BizMsgServiceImpl {    @KafkaListener(topics = "${kafka.consumer.deviceEventTopic}", ...)    public void kafkaDeviceEventListen(ConsumerRecord<String, String> record) {        // ... 解析事件 ...        String msgServiceBusinessId = event.getDeviceId() + "_" + event.getDeviceSn() + "_" + event.getOccurTime();        // 保存到本地设备事件表        saveDeviceEventMessage(event, containerId, msgServiceBusinessId);        // ... 后续推送消息中心 ...        LogUtils.BIZ.info("发送设备事件消息=> {}", Collections.singletonList(msgInfoReqDTO));        msgInfoService.savaMsgInfoPOS(Collections.singletonList(msgInfoReqDTO));    }    /**     * 将Kafka设备事件保存到本地设备事件表     * business_id 存在唯一索引,重复事件插入失败时仅告警不阻断     */    private void saveDeviceEventMessage(DeviceEventEntity event, String containerId, String businessId) {        try {            DeviceEventMessagePO po = new DeviceEventMessagePO();            // ... set 各字段 ...            deviceEventMessageMapper.insert(po);        } catch (Exception e) {            log.warn("保存设备事件到本地表失败 businessId={}", businessId, e);        }    }}

二、问题现象

线上一段时间后,用户反馈偶发重复推送。排查时翻遍 warn.logerror.log 文件,没有找到任何 保存设备事件到本地表失败 的日志

但数据库里又能查到唯一索引冲突的痕迹(或通过其他途径确认确实发生了重复消费)。于是陷入"明明打了日志却找不到"的困境。

三、根因分析

3.1 第一反应:日志级别?

第一反应是怀疑 log.warn 级别在生产环境被调高过滤了。但查看 logback-spring.xml

<root level="INFO">    <appender-ref ref="STDOUT"/>    <appender-ref ref="ASYNC_INFO"/>    <appender-ref ref="ASYNC_ERROR"/></root>

root level="INFO",WARN 级别高于 INFO,级别上不会被过滤。这条路走不通。

3.2 第二反应:异常没抛出?

怀疑 insert 违反唯一约束时没有抛异常。但 MyBatis-Plus 的 BaseMapper.insert 执行的是标准 INSERT INTO,MySQL 唯一索引冲突会立即抛 SQLException,经 MyBatis 包装为 PersistenceException,再经 Spring 包装为 DuplicateKeyException。这些异常都会被 catch (Exception e) 捕获。

而且即使不抛异常,log.warn 这行代码至少会被执行(除非异常发生在 catch 之前)。所以异常没抛出也不是根因。

3.3 真相:root logger 没有 ASYNC_WARN

仔细对比 logback-spring.xmlroot loggerBIZ logger 的 appender 配置:

<!-- root logger:只挂了 STDOUT、ASYNC_INFO、ASYNC_ERROR,没有 ASYNC_WARN! --><root level="INFO">    <appender-ref ref="STDOUT"/>    <appender-ref ref="ASYNC_INFO"/>    <appender-ref ref="ASYNC_ERROR"/></root><!-- BIZ logger:挂了 ASYNC_WARN --><logger name="BIZ" level="DEBUG" additivity="false">    <appender-ref ref="STDOUT"/>    <appender-ref ref="ASYNC_INFO"/>    <appender-ref ref="ASYNC_DEBUG"/>    <appender-ref ref="ASYNC_WARN"/>    <appender-ref ref="ASYNC_ERROR"/></logger>

再看各文件 appender 的 filter 配置——用的是 LevelFilter 精确匹配:

<appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">    <filter class="ch.qos.logback.classic.filter.LevelFilter">        <level>WARN</level>        <onMatch>ACCEPT</onMatch>        <onMismatch>DENY</onMismatch>   <!-- 非 WARN 一律拒绝 -->    </filter>    <file>${logPath}/log/warn.log</file>    ...</appender><appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">    <filter class="ch.qos.logback.classic.filter.LevelFilter">        <level>INFO</level>        <onMatch>ACCEPT</onMatch>        <onMismatch>DENY</onMismatch>   <!-- WARN 会被拒绝,不进 info.log -->    </filter>    ...</appender>

3.4 两个 logger 的行为差异

关键在于代码里用了哪个 logger:

调用方式logger 名称走的 logger 配置warn.loginfo.log控制台 STDOUT
log.warn(...)@Slf4j 注入)com.dsa.hems.msg.service.BizMsgServiceImplroot❌ 不写入❌ LevelFilter 拒绝 WARN✅(但容器易丢失)
LogUtils.BIZ.warn(...)BIZBIZ logger✅ 写入
// LogUtils.javapublic interface LogUtils {    Logger BIZ = LoggerFactory.getLogger("BIZ");}

saveDeviceEventMessage 用的是 log.warn(走 root logger),而同一个 KafkaListener 方法里其他业务日志用的都是 LogUtils.BIZ.info(走 BIZ logger)。

root logger 没有挂载 ASYNC_WARN appender,所以 log.warn 的输出:

  1. ❌ 不写入 warn.log(只有 BIZ logger 才挂了 ASYNC_WARN)
  2. ❌ 不写入 info.log(INFO appender 的 LevelFilter 精确匹配 INFO,WARN 被 DENY)
  3. ❌ 不写入 error.log(同理)
  4. ✅ 只输出到控制台 STDOUT

而生产环境是 Docker 容器部署,控制台日志不落盘、易丢失,排查时只看文件日志,自然就"找不到日志"了。

3.5 一次完整的调用链对照

@KafkaListener(...)public void kafkaDeviceEventListen(ConsumerRecord<String, String> record) {    LogUtils.BIZ.info("监听到设备事件消息 {} ", value);   // ✅ 走 BIZ,写 info.log    // ...    saveDeviceEventMessage(...);                            // ⚠️ 内部用 log.warn,走 root    // ...    LogUtils.BIZ.info("发送设备事件消息=> {}", ...);        // ✅ 走 BIZ,写 info.log}private void saveDeviceEventMessage(...) {    try {        deviceEventMessageMapper.insert(po);    } catch (Exception e) {        log.warn("保存设备事件到本地表失败 ...", e);          // ❌ 走 root,不写任何文件    }}

同一个方法里,BIZ logger 和 @Slf4jlog 混用,正是这次踩坑的直接原因。

四、解决方案

4.1 方案一(推荐):统一使用 BIZ logger

log.warn 改成 LogUtils.BIZ.warn,与同方法其他日志保持一致:

private void saveDeviceEventMessage(DeviceEventEntity event, String containerId, String businessId) {    try {        DeviceEventMessagePO po = new DeviceEventMessagePO();        po.setMsgServiceBusinessId(businessId);        // ... set 各字段 ...        deviceEventMessageMapper.insert(po);    } catch (DuplicateKeyException e) {        // 唯一索引冲突:Kafka重复消费或同businessId事件重复推送,属于预期场景,仅告警不阻断        LogUtils.BIZ.warn("设备事件重复插入(唯一索引冲突), businessId={}, deviceSn={}, eventType={}, occurTime={}",                businessId, event.getDeviceSn(), event.getEventType(), event.getOccurTime());    } catch (Exception e) {        LogUtils.BIZ.warn("保存设备事件到本地表失败, businessId={}, deviceSn={}, eventType={}",                businessId, event.getDeviceSn(), event.getEventType(), e);    }}

补充说明:

  1. DuplicateKeyException 来自 org.springframework.daomybatis-plus-boot-starter 会传递引入 spring-tx,可放心使用。
  2. 对唯一索引冲突这种预期场景不打印异常堆栈(避免日志噪音,只打关键字段);对其他未知异常打印完整堆栈,方便排查。

4.2 方案二:给 root logger 补上 ASYNC_WARN

从 logback 配置层面兜底,让所有走 root 的 warn 日志都能落盘:

<root level="INFO">    <appender-ref ref="STDOUT"/>    <appender-ref ref="ASYNC_INFO"/>    <appender-ref ref="ASYNC_WARN"/>   <!-- 补上这一行 -->    <appender-ref ref="ASYNC_ERROR"/></root>

建议两个方案都做:方案一统一编码规范,方案二兜底防止其他类再踩坑。

4.3 不推荐的写法

// ❌ 用 @Slf4j 的 log,走 root logger,warn 不落盘log.warn("保存设备事件到本地表失败 businessId={}", businessId, e);// ❌ 异常被吞但无任何日志} catch (Exception e) {    // 啥也不干}

五、延伸:LevelFilter vs ThresholdFilter

这次踩坑还暴露一个易混淆点——LevelFilterThresholdFilter 的区别:

过滤器行为WARN 日志能否进入 INFO appender
LevelFilter(onMatch=ACCEPT, onMismatch=DENY)精确匹配指定级别,其他一律 DENY❌ 不会(WARN ≠ INFO)
ThresholdFilter(level=INFO)大于等于指定级别都通过✅ 会(WARN ≥ INFO)

本项目用的是 LevelFilter 精确匹配,所以 info.log 里只有 INFO,warn.log 里只有 WARN。这种"按级别分文件"的设计本身没问题,但前提是 logger 必须挂载对应的 appender,否则该级别的日志就无处可去。

六、总结与避坑清单

  1. 同一个类里不要混用 log@Slf4j)和 LogUtils.BIZ:二者走不同 logger 配置,行为差异巨大。统一用业务约定的 logger(本项目是 LogUtils.BIZ)。
  2. 改 logback 配置后,确认 root logger 挂载了所有需要的级别 appender:尤其是 ASYNC_WARN,否则所有走 root 的 warn 日志只在控制台,不落盘。
  3. 生产环境务必保留容器控制台日志:用 docker logs 或挂载 volume 收集 stdout,作为文件日志的兜底。
  4. 区分"预期异常"与"未知异常":唯一索引冲突是可预期的,单独 catch 并精简日志(不打堆栈);其他异常打印完整堆栈。这样既不淹没真正的问题,又不丢失排查线索。
  5. catch 块的日志要带足够上下文:本次修复补充了 deviceSneventTypeoccurTime 等字段,便于从日志直接定位是哪台设备、哪类事件重复。

一句话避坑:log.warn 不一定写进 warn.log——取决于你用的是哪个 logger、它挂了哪些 appender。

相关文章

精彩推荐