在Java项目开发中,日志系统就像项目的"黑匣子",记录着系统运行的所有关键信息。我见过太多团队在开发阶段随便配置个控制台输出就完事,结果上线后遇到问题查日志时傻眼了——要么日志文件太大把磁盘撑爆,要么关键错误信息没记录,要么历史日志堆积如山找不到问题点。
log4j2作为目前Java生态中最主流的日志框架,它的强大之处在于灵活的配置能力。一个好的log4j2.xml配置文件应该像瑞士军刀一样:
举个例子,去年我们有个订单系统突然出现性能问题,幸亏配置了按小时滚动的错误日志,配合200MB大小限制,很快就定位到是某第三方接口超时引发的雪崩效应。如果当时用的还是简单的控制台输出,估计排查时间至少要翻倍。
先来看一个最小可用的log4j2.xml骨架:
xml复制<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
这个配置虽然简单,但有几个关键点需要注意:
我建议在开发环境可以先把status设为DEBUG,这样能看到log4j2内部的初始化过程,对排查配置问题很有帮助。曾经有个诡异的日志丢失问题,就是通过这个调试信息发现是配置文件编码错误导致的。
生产环境最关键的配置就是日志滚动策略。来看一个电商订单服务的典型配置:
xml复制<RollingFile name="OrderService"
fileName="logs/order.log"
filePattern="logs/$${date:yyyy-MM}/order-%d{yyyy-MM-dd}-%i.log.gz">
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
<SizeBasedTriggeringPolicy size="200 MB"/>
</Policies>
<DefaultRolloverStrategy max="10">
<Delete basePath="logs" maxDepth="2">
<IfFileName glob="*/order-*.log.gz"/>
<IfLastModified age="180d"/>
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
这个配置实现了:
踩坑提醒:modulate="true"这个参数很关键,它会让日志滚动时间对齐到自然时间点。比如设置24小时滚动,如果不开启modulate,日志会在应用启动24小时后滚动,开启后则会固定在每天午夜滚动。
实际项目中我们通常需要区分开发、测试、生产环境。可以通过property+if条件实现:
xml复制<Properties>
<Property name="env">dev</Property>
<Property name="logPath">logs/${env}</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level - %msg%n"/>
</Console>
<RollingFile name="File"
fileName="${logPath}/app.log"
filePattern="${logPath}/app-%d{yyyy-MM-dd}.log.gz"
append="true">
<Filters>
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
</Policies>
</RollingFile>
<RollingFile name="ErrorFile"
fileName="${logPath}/error.log"
filePattern="${logPath}/error-%d{yyyy-MM-dd}.log.gz">
<ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
</Policies>
</RollingFile>
</Appenders>
这里用ThresholdFilter实现了:
对于高并发系统,同步写日志可能成为性能瓶颈。log4j2的异步日志能显著提升性能:
xml复制<AsyncLogger name="com.example.order" level="debug" additivity="false">
<AppenderRef ref="OrderService"/>
</AsyncLogger>
<AsyncRoot level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</AsyncRoot>
实测数据:在每秒5000订单的场景下,同步日志TPS约3200,切换异步后提升到4800+。但要注意:
建议关键业务日志仍使用同步方式,其他日志用异步。
下面是一个电商系统的完整配置,包含了我们讨论的所有最佳实践:
xml复制<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="300">
<Properties>
<Property name="LOG_HOME">/var/log/order-service</Property>
<Property name="FILE_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Property>
<Property name="MAX_HISTORY">180</Property>
<Property name="MAX_SIZE">200MB</Property>
</Properties>
<Appenders>
<!-- 控制台输出(仅开发环境使用) -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${FILE_PATTERN}"/>
<Filters>
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</Console>
<!-- 主业务日志 -->
<RollingFile name="BusinessLog"
fileName="${LOG_HOME}/business.log"
filePattern="${LOG_HOME}/archive/business-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${FILE_PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
<SizeBasedTriggeringPolicy size="${MAX_SIZE}"/>
</Policies>
<DefaultRolloverStrategy max="10">
<Delete basePath="${LOG_HOME}/archive" maxDepth="1">
<IfFileName glob="business-*.log.gz"/>
<IfLastModified age="${MAX_HISTORY}d"/>
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
<!-- 错误日志(单独文件) -->
<RollingFile name="ErrorLog"
fileName="${LOG_HOME}/error.log"
filePattern="${LOG_HOME}/archive/error-%d{yyyy-MM-dd}.log.gz">
<ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${FILE_PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
</Policies>
</RollingFile>
<!-- 审计日志(固定大小不压缩) -->
<RollingFile name="AuditLog"
fileName="${LOG_HOME}/audit.log"
filePattern="${LOG_HOME}/archive/audit-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="${FILE_PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="500MB"/>
</Policies>
<DefaultRolloverStrategy max="5"/>
</RollingFile>
</Appenders>
<Loggers>
<!-- 第三方库日志降级 -->
<Logger name="org.springframework" level="WARN"/>
<Logger name="com.netflix" level="ERROR"/>
<!-- 异步业务日志 -->
<AsyncLogger name="com.example.order.service" level="DEBUG" additivity="false">
<AppenderRef ref="BusinessLog"/>
</AsyncLogger>
<!-- 同步审计日志 -->
<Logger name="com.example.order.audit" level="INFO" additivity="false">
<AppenderRef ref="AuditLog"/>
</Logger>
<Root level="WARN">
<AppenderRef ref="Console"/>
<AppenderRef ref="ErrorLog"/>
</Root>
</Loggers>
</Configuration>
这个配置的特点:
在实际使用中遇到过不少坑,这里分享几个典型案例:
问题1:日志文件没有按预期滚动
问题2:日志文件占用磁盘空间过大
问题3:异步日志丢失
有个特别隐蔽的问题曾让我们团队折腾了很久:日志文件按天滚动正常,但SizeBased策略不生效。最后发现是filePattern中的%i参数位置放错了,应该放在文件名最后而不是日期前面。这种细节问题特别考验配置的准确性。
最后提醒一点:任何日志配置修改后,最好先用log4j2的测试模式验证(设置status="trace"),确认无误后再部署到生产环境。