# Java日志框架选型与配置,别再乱打日志了

3 阅读3分钟

接手一个老项目,打开一看: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性能和安全
日志按天滚动防止磁盘打满
错误日志单独一份便于监控报警

日志这东西,平时看不出重要性,出事故的时候就是救命稻草。花半天把规范定好,后面能省无数个小时。