3. 日志配置实战:配置文件详解(appender、logger、root)、异步日志配置

好,咱们进入实战环节。前面聊了日志框架选型和日志级别,但真正让日志系统跑起来的,是配置文件。很多新手拿到一个 logback.xml 或者 log4j2.xml,看着一堆标签就发懵。我刚开始带项目的时候也踩过这个坑——配置写错了,日志死活不输出,查了半天发现是 appender 没绑定对。

说白了,日志配置就三个核心概念:appender(输出到哪里)、logger(谁在记录)、root(兜底规则)。搞懂这三者的关系,配置文件就是一张窗户纸。

3.1 配置文件结构总览

先看一个最简的 logback.xml 骨架:

<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

这个配置做了三件事:定义了一个控制台输出器,设置了一个输出格式,然后把 root logger 的级别设为 INFO,并关联到那个输出器。嗯,就这么简单。

但实际项目中,你不会只往控制台打日志。你会需要文件滚动、按天归档、按大小切割、甚至异步写入。下面我一个个拆开讲。

核心关系图: Logger 是日志的生产者,Appender 是日志的消费者。Root 是默认的生产者,所有未显式配置的 Logger 都归它管。

日志配置核心结构 Root Logger com.example.controller com.example.service com.example.dao ConsoleAppender FileAppender AsyncAppender 💡 每个 Logger 可以绑定多个 Appender,子 Logger 默认继承 Root 的 Appender。 可以通过 additivity="false" 关闭继承,避免日志重复输出。

3.2 Appender 详解

Appender 决定了日志的目的地。常见的 Appender 有四种,我列个表对比一下:

Appender 类型 类名 适用场景 注意事项
控制台 ConsoleAppender 开发调试、本地运行 生产环境慎用,会丢失日志
文件 FileAppender 单文件输出,简单场景 不滚动,文件会无限增长
滚动文件 RollingFileAppender 生产环境标配,按时间/大小切割 需要配置触发策略和归档策略
异步 AsyncAppender 高并发、低延迟场景 会丢失少量日志,需权衡

我个人最常用的是 RollingFileAppender。为什么?因为生产环境日志量巨大,不滚动的话,一个文件几天就能撑爆磁盘。我曾经接手过一个项目,日志文件 50GB,用 Vim 打开直接卡死,最后只能写脚本切割——那叫一个狼狈。

来看一个标准的 RollingFileAppender 配置:

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>/var/log/myapp/app.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>/var/log/myapp/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
        <maxHistory>30</maxHistory>
        <totalSizeCap>10GB</totalSizeCap>
    </rollingPolicy>
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
        <maxFileSize>500MB</maxFileSize>
    </triggeringPolicy>
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

这里有几个关键参数:

  • fileNamePattern:归档文件的命名规则。%d{yyyy-MM-dd} 表示按天归档,%i 是序号(当同一天内文件超过大小限制时递增)。
  • maxHistory:保留最近 30 天的日志。超过的自动删除。
  • totalSizeCap:所有归档日志的总大小上限。超过时删除最旧的。
  • maxFileSize:单个日志文件达到 500MB 时触发滚动。

小技巧: 我习惯把 maxHistory 和 totalSizeCap 同时设置。只设 maxHistory 的话,如果某天日志量特别大,30 天的文件可能撑爆磁盘。两个一起设,双重保险。

3.3 Logger 与 Root 的配置

Logger 是按包名或类名配置的日志记录器。Root 是全局默认的 Logger,所有未显式配置的类都归它管。

看一个典型的多 Logger 配置:

<!-- 全局默认级别 -->
<root level="WARN">
    <appender-ref ref="FILE"/>
</root>

<!-- 业务包级别 -->
<logger name="com.example.service" level="DEBUG" additivity="false">
    <appender-ref ref="SERVICE_FILE"/>
</logger>

<!-- 第三方库降级 -->
<logger name="org.springframework" level="ERROR"/>
<logger name="org.apache.kafka" level="WARN"/>

这里有几个要点:

  • additivity="false":关闭日志继承。如果不关,com.example.service 的日志会同时输出到 SERVICE_FILE 和 Root 的 FILE,造成重复。
  • 第三方库降级:Spring、Kafka 这些框架的日志通常很吵。我一般把它们的级别设成 WARN 或 ERROR,只关注异常。
  • 级别覆盖:子 Logger 的级别会覆盖 Root 的级别。Root 是 WARN,但 com.example.service 是 DEBUG,所以这个包下的 DEBUG 日志也会输出。

避坑指南: 我曾经犯过一个错误——给所有 Logger 都加了 additivity="false",结果 Root 的 Appender 一个都没用上。排查了半天才发现,有些包没有显式配置 Logger,日志全丢了。记住:只有当你确定子 Logger 不需要继承 Root 的 Appender 时,才关掉 additivity。

3.4 异步日志配置

同步日志有个问题:每次写日志都要做 I/O 操作,在高并发场景下会成为性能瓶颈。你想想看,一个接口每秒处理 1000 个请求,每个请求打 5 条日志,那就是每秒 5000 次磁盘写入——不卡才怪。

异步日志的思路很简单:生产者把日志丢进队列,消费者(后台线程)批量写入。这样业务线程不会被 I/O 阻塞。

Logback 的异步配置:

<!-- 先定义一个同步的 RollingFileAppender -->
<appender name="SYNC_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>/var/log/myapp/app.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>/var/log/myapp/app.%d{yyyy-MM-dd}.log</fileNamePattern>
        <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

<!-- 再用 AsyncAppender 包装它 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
    <queueSize>1024</queueSize>
    <discardingThreshold>0</discardingThreshold>
    <includeCallerData>false</includeCallerData>
    <appender-ref ref="SYNC_FILE"/>
</appender>

<root level="INFO">
    <appender-ref ref="ASYNC_FILE"/>
</root>

关键参数说明:

参数 默认值 说明
queueSize 256 队列容量。满了之后,新日志会被丢弃或阻塞。
discardingThreshold 队列容量的 20% 当队列剩余空间低于此阈值时,丢弃 TRACE/DEBUG/INFO 级别的日志。设为 0 表示永不丢弃。
includeCallerData false 是否包含调用者信息(类名、行号)。开启会降低性能。
neverBlock false 队列满时是否阻塞生产者。设为 true 则直接丢弃日志,不阻塞。

我的建议: 生产环境把 discardingThreshold 设为 0,neverBlock 设为 false。为什么?因为日志丢了就丢了,但业务线程不能卡。你想想看,如果因为日志队列满了导致接口超时,那才是真正的灾难。日志可以丢,业务不能停。

Log4j2 的异步配置更简洁,直接加一个系统属性就行:

// 在启动参数中设置
-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

// 或者使用混合异步 Logger
<Loggers>
    <AsyncLogger name="com.example.service" level="DEBUG" additivity="false">
        <AppenderRef ref="FILE"/>
    </AsyncLogger>
    <Root level="INFO">
        <AppenderRef ref="FILE"/>
    </Root>
</Loggers>

Log4j2 的异步性能比 Logback 更好,因为它使用了 LMAX Disruptor 无锁队列。如果你的项目对性能要求极高,可以考虑切到 Log4j2。

3.5 配置实战:一个完整的生产级配置

最后,我贴一个我常用的生产级 logback.xml 模板。这个配置我用了好几年,踩过的坑都填平了:

<configuration scan="true" scanPeriod="60 seconds">

    <!-- 控制台输出(开发环境用) -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{36}) - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 业务日志(异步) -->
    <appender name="ASYNC_BUSINESS" class="ch.qos.logback.classic.AsyncAppender">
        <queueSize>2048</queueSize>
        <discardingThreshold>0</discardingThreshold>
        <appender-ref ref="ROLLING_BUSINESS"/>
    </appender>

    <appender name="ROLLING_BUSINESS" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/var/log/myapp/business.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>/var/log/myapp/business.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <totalSizeCap>10GB</totalSizeCap>
        </rollingPolicy>
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <maxFileSize>500MB</maxFileSize>
        </triggeringPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 错误日志(单独文件,方便排查) -->
    <appender name="ASYNC_ERROR" class="ch.qos.logback.classic.AsyncAppender">
        <queueSize>512</queueSize>
        <appender-ref ref="ROLLING_ERROR"/>
    </appender>

    <appender name="ROLLING_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/var/log/myapp/error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>/var/log/myapp/error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>90</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- Logger 配置 -->
    <logger name="com.example" level="DEBUG" additivity="false">
        <appender-ref ref="ASYNC_BUSINESS"/>
        <appender-ref ref="ASYNC_ERROR"/>
    </logger>

    <logger name="org.springframework" level="WARN"/>
    <logger name="org.apache.kafka" level="WARN"/>

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

</configuration>

这个配置有几个设计思路:

  • 业务日志和错误日志分离:错误日志保留 90 天,方便回溯问题。业务日志保留 30 天,节省空间。
  • 异步写入:所有文件写入都走 AsyncAppender,业务线程不阻塞。
  • 控制台只给 Root:开发环境看控制台,生产环境关掉控制台输出(或者把 Root 的 CONSOLE 注释掉)。
  • 热加载:scan="true" 让配置文件支持热加载,修改后 60 秒内生效,不用重启应用。

最后提醒一句: 配置文件写完后,一定要用 java -jar -Dlogback.debug=true your-app.jar 启动一次,看控制台输出的配置解析日志。它能告诉你每个 Logger 和 Appender 的绑定关系,有没有配置错误一目了然。这个习惯帮我省了无数排查时间。