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

 更新时间:2026年08月10日 09:12:43   作者:不是光头 强  
生产环境排查重复消费问题,明明代码里 catch 住了异常并打了 log.warn,却在日志文件里死活找不到,本文记录了一次真实排查过程,根因出在 logback 的 root logger 与 BIZ logger 配置差异上,下面小编就和大家详细介绍一下吧

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

一、问题背景

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

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

@Service
@Slf4j
public 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.log、error.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.xml 中 root logger 和 BIZ 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.java
public 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 的输出:

  • ❌ 不写入 warn.log(只有 BIZ logger 才挂了 ASYNC_WARN)
  • ❌ 不写入 info.log(INFO appender 的 LevelFilter 精确匹配 INFO,WARN 被 DENY)
  • ❌ 不写入 error.log(同理)
  • ✅ 只输出到控制台 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 和 @Slf4j 的 log 混用,正是这次踩坑的直接原因。

四、解决方案

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);
    }
}

补充说明:

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

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

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

过滤器行为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 块的日志要带足够上下文:本次修复补充了 deviceSn、eventType、occurTime 等字段,便于从日志直接定位是哪台设备、哪类事件重复。

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

到此这篇关于SpringBoot中logback日志丢失原因排查与解决方法的文章就介绍到这了,更多相关SpringBoot logback日志丢失内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java BigDecimal正确用法详解

    Java BigDecimal正确用法详解

    Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理
    2022-10-10
  • springboot整合Sa-Token实现登录认证和权限校验的详细流程

    springboot整合Sa-Token实现登录认证和权限校验的详细流程

    Sa-Token是国产轻量级权限认证框架,支持登录、权限校验、单点登录及分布式会话,配置简便且API设计直观,相比Spring Security更易上手,适合快速实现安全功能,助力国产开源发展,感兴趣的朋友跟随小编一起看看吧
    2025-09-09
  • 使用nexus3.X上传本地jar包并且通过pom读取的解决方案(全网最新)

    使用nexus3.X上传本地jar包并且通过pom读取的解决方案(全网最新)

    这篇文章主要介绍了使用nexus3.X上传本地jar包并且通过pom读取的解决方案(全网最新),本文内容有点长,结合图文实例给大家讲解的非常详细,需要的朋友可以参考下
    2023-11-11
  • 浅谈spring security入门

    浅谈spring security入门

    这篇文章主要介绍了浅谈spring security入门,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-07-07
  • Java中多个线程交替循环执行的实现

    Java中多个线程交替循环执行的实现

    有些时候面试官经常会问,两个线程怎么交替执行呀,本文就来详细的介绍一下Java中多个线程交替循环执行的实现,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧
    2024-01-01
  • logback StatusListener的定义方法源码解读

    logback StatusListener的定义方法源码解读

    这篇文章主要为大家介绍了logback StatusListener的定义方法源码解读,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-11-11
  • SpringBoot项目速度提升之延迟初始化(Lazy Initialization)详解

    SpringBoot项目速度提升之延迟初始化(Lazy Initialization)详解

    延迟初始化(Lazy Initialization)是一种在需要时才创建或加载对象的策略,以减少启动时间和资源消耗,本文就来讲讲延迟初始化的具体使用吧
    2023-05-05
  • Spring事务注解@Transactional参数详解与实战示例

    Spring事务注解@Transactional参数详解与实战示例

    本文详细介绍了Spring框架中@Transactional注解的各个核心参数,包括传播行为、隔离级别、超时设置、只读事务、回滚规则和事务管理器等,通过理论讲解和实战示例,帮助开发者全面掌握这些参数的使用,提升应用的数据一致性和系统可靠性,感兴趣的朋友跟随小编一起看看吧
    2025-12-12
  • SpringBoot常用请求方式及请求参数传递的方式

    SpringBoot常用请求方式及请求参数传递的方式

    本文给大家介绍SpringBoot常用请求方式及请求参数传递的方式,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2025-08-08
  • IDEA打包的两种方式及注意事项说明

    IDEA打包的两种方式及注意事项说明

    这篇文章主要介绍了IDEA打包的两种方式及注意事项说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-04-04

最新评论