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 都归它管。
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 的绑定关系,有没有配置错误一目了然。这个习惯帮我省了无数排查时间。