接手一个老项目,打开一看:System.out.println满天飞,日志文件一天涨到2GB,出问题的时候关键信息一条都没有。
日志是排障的生命线,配好了是利器,配不好就是垃圾。今天聊聊怎么配。
主流日志框架
SLF4J(门面)
├── Logback(实现,Spring Boot默认)
├── Log4j2(实现,性能最好)
└── JUL(JDK自带,基本不用)
一定要用SLF4J的门面,不要直接依赖具体实现。这样将来换日志实现,业务代码不用改。
// 正确
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
// 错误:直接依赖Log4j
import org.apache.log4j.Logger;
日志级别
| 级别 | 用途 | 生产环境 |
|---|---|---|
| ERROR | 影响业务的异常 | 必须打 |
| WARN | 需要关注的警告 | 打 |
| INFO | 关键业务节点 | 打(控制量) |
| DEBUG | 调试信息 | 关闭 |
| TRACE | 极详细追踪 | 关闭 |
原则:ERROR要能报警,WARN要能发现问题,INFO要能还原业务流,DEBUG只在排障时开。
别这样打日志
错误1:字符串拼接
// 错误:无论日志级别开没开,字符串都会拼接
log.debug("用户信息:" + user.toString());
// 正确:占位符,级别不满足时不会计算
log.debug("用户信息:{}", user);
高并发下字符串拼接的性能损耗很明显,这个坑很经典。
错误2:打印异常不传e
// 错误:堆栈丢失
try {
doSomething();
} catch (Exception e) {
log.error("处理失败:" + e.getMessage());
}
// 正确:把异常对象作为最后一个参数
try {
doSomething();
} catch (Exception e) {
log.error("处理订单失败, orderId={}", orderId, e);
}
不传e,你就只能看到一行错误消息,不知道是哪一行抛的,排查基本靠猜。
错误3:没有上下文
// 错误
log.error("查询失败");
// 正确:带上关键参数
log.error("查询订单失败, userId={}, orderId={}", userId, orderId, e);
错误4:在循环里打日志
// 危险:10万条数据就是10万行日志
for (Order order : orders) {
log.info("处理订单:{}", order.getId());
}
改成批量汇总:
log.info("开始处理订单,共{}条", orders.size());
// ... 处理
log.info("订单处理完成,成功{}条,失败{}条", success, failed);
Logback配置实战
Spring Boot默认用Logback,配置logback-spring.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_HOME" value="./logs"/>
<property name="APP_NAME" value="my-app"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 全量日志,按天滚动 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 错误日志单独一份,方便报警 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}-error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}-error.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>90</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 异步输出,避免阻塞业务线程 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>1024</queueSize>
<appender-ref ref="FILE"/>
</appender>
<!-- 按包控制级别 -->
<logger name="com.example.mapper" level="DEBUG"/>
<logger name="org.springframework" level="WARN"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</configuration>
多环境配置
<!-- Spring Boot支持springProfile标签 -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
链路追踪:MDC
微服务下,一个请求经过N个服务,日志散落各处,怎么串起来?
用MDC(Mapped Diagnostic Context):
// 拦截器里设置traceId
@Component
public class TraceInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
String traceId = req.getHeader("X-Trace-Id");
if (traceId == null) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
MDC.put("traceId", traceId);
res.setHeader("X-Trace-Id", traceId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res,
Object handler, Exception ex) {
MDC.clear(); // 必须清理,否则线程池复用会串
}
}
<!-- 日志格式里加%X{traceId} -->
<pattern>%d{HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
输出效果:
14:23:01.123 [http-nio-8080-exec-1] [a1b2c3d4e5f6] INFO c.e.OrderService - 创建订单 orderId=123
14:23:01.456 [http-nio-8080-exec-1] [a1b2c3d4e5f6] INFO c.e.PayService - 支付成功
同一个traceId的所有日志,一搜就全出来了。
日志规范清单
| 规范 | 说明 |
|---|---|
| 用SLF4J门面 | 不直接依赖实现 |
| 用占位符 | 不用字符串拼接 |
| 异常传e | 保留堆栈 |
| 带上关键参数 | 便于定位 |
| 不打敏感信息 | 密码、身份证脱敏 |
| 生产关闭DEBUG | 性能和安全 |
| 日志按天滚动 | 防止磁盘打满 |
| 错误日志单独一份 | 便于监控报警 |
日志这东西,平时看不出重要性,出事故的时候就是救命稻草。花半天把规范定好,后面能省无数个小时。