SpringBoot日志配置和链路排查

SpringBoot日志配置和链路排查

Spring Boot 简化了配置,但日志管理依然需要重视。日志配置、链路追踪、排查思路都是日常开发中会遇到的问题。本文讲实际项目中的日志管理经验。

Spring Boot 日志框架

#

默认集成

Spring Boot 默认使用 SLF4J + Logback:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Service
public class UserService {
private static final Logger log = LoggerFactory.getLogger(UserService.class);

public void createUser(User user) {
log.info("创建用户: {}", user.getUsername());
log.debug("用户详情: {}", user);
}
}

#

日志级别

级别 说明
TRACE 最详细
DEBUG 调试信息
INFO 一般信息
WARN 警告
ERROR 错误
log.trace("trace");
log.debug("debug");
log.info("info");
log.warn("warn");
log.error("error");

日志配置

#

application.yml 配置

logging:
level:
root: INFO
com.example: DEBUG
com.example.mapper: TRACE

pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"

file:
name: logs/app.log
max-size: 10MB
max-history: 30

#

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 彩色控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) [%thread] %cyan(%logger{50}) - %msg%n</pattern>
</encoder>
</appender>

<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
<maxFileSize>100MB</maxFileSize>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>

<!-- 异步文件输出 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
<queueSize>512</queueSize>
</appender>

<!-- 错误日志单独输出 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/error.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>

<!-- 根日志 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>

<!-- 业务日志 -->
<logger name="com.example" level="DEBUG"/>
<logger name="com.example.mapper" level="TRACE"/>
</configuration>

链路追踪(MDC)

#

MDC 基础

MDC(Mapped Diagnostic Context)用于在同一线程的日志中添加上下文信息。

import org.slf4j.MDC;

@Service
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);

public void createOrder(OrderRequest request) {
// 设置 traceId
String traceId = UUID.randomUUID().toString().replace("-", "");
MDC.put("traceId", traceId);

try {
log.info("创建订单开始: {}", request.getOrderNo());
// 业务逻辑
log.info("创建订单完成: {}", request.getOrderNo());
} finally {
// 必须清理
MDC.clear();
}
}
}

#

日志格式中加入 traceId

<!-- logback-spring.xml -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>

输出:

2024-06-29 10:00:00.123 [http-nio-8080-exec-1] [a1b2c3d4e5f6] INFO  c.e.service.OrderService - 创建订单开始: ORDER001

#

过滤器自动设置 traceId

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceIdFilter extends OncePerRequestFilter {

public static final String TRACE_ID = "traceId";
public static final String TRACE_ID_HEADER = "X-Trace-Id";

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

// 从请求头获取或生成 traceId
String traceId = request.getHeader(TRACE_ID_HEADER);
if (StringUtils.isBlank(traceId)) {
traceId = UUID.randomUUID().toString().replace("-", "");
}

// 设置到 MDC
MDC.put(TRACE_ID, traceId);

// 设置到响应头
response.setHeader(TRACE_ID_HEADER, traceId);

try {
filterChain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}

#

Feign 传递 traceId

@Configuration
public class FeignConfig {

@Bean
public RequestInterceptor traceIdInterceptor() {
return template -> {
String traceId = MDC.get(TraceIdFilter.TRACE_ID);
if (StringUtils.isNotBlank(traceId)) {
template.header(TraceIdFilter.TRACE_ID_HEADER, traceId);
}
};
}
}

#

线程池传递 MDC

public class MdcThreadPoolExecutor extends ThreadPoolExecutor {

public MdcThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}

@Override
public void execute(Runnable task) {
Map<String, String> context = MDC.getCopyOfContextMap();
super.execute(() -> {
if (context != null) {
MDC.setContextMap(context);
}
try {
task.run();
} finally {
MDC.clear();
}
});
}
}

#

@Async 传递 MDC

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

@Override
@Bean(name = "taskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("async-");

// 包装为 MDC 传递的 Executor
executor.setTaskDecorator(new MdcTaskDecorator());
executor.initialize();
return executor;
}
}

public class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}

日志收集(ELK)

#

Filebeat 配置

# filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/app/*.log
fields:
app: my-service
env: prod
multiline.pattern: '^\d{4}-\d{2}-\d{2}'
multiline.negate: true
multiline.match: after

output.logstash:
hosts: ["logstash:5044"]

#

Logstash 配置

# logstash.conf
input {
beats {
port => 5044
}
}

filter {
grok {
match => {
"message" => "%{TIMESTAMP_ISO8601:timestamp} \[%{DATA:thread}\] \[%{DATA:traceId}\] %{LOGLEVEL:level} %{DATA:logger} - %{GREEDYDATA:msg}"
}
}
date {
match => [ "timestamp", "ISO8601" ]
}
}

output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "app-logs-%{+YYYY.MM.dd}"
}
}

#

Kibana 查询

# 按 traceId 查询链路
traceId: "a1b2c3d4e5f6"

# 按应用和级别查询
app: "my-service" AND level: "ERROR"

# 时间范围
timestamp: ["now-1h" TO "now"]

最佳实践

#

1. 日志规范

// 好:使用占位符,不拼接字符串
log.info("用户{}创建订单{}", userId, orderId);

// 差:字符串拼接,无条件也执行
log.info("用户" + userId + "创建订单" + orderId);

// 异常日志
log.error("订单创建失败: {}", orderId, e); // 带异常堆栈

#

2. 敏感信息脱敏

@DataMask(type = MaskType.PHONE)
private String phone;

@DataMask(type = MaskType.ID_CARD)
private String idCard;

#

3. 日志采样

if (log.isDebugEnabled()) {
log.debug("详细调试信息: {}", expensiveOperation());
}

总结

组件 用途
SLF4J + Logback 日志框架
MDC 线程级上下文
TraceIdFilter 自动生成/传递 traceId
Feign Interceptor 服务间传递 traceId
MdcTaskDecorator 异步线程传递 MDC
ELK 日志收集与分析

完善的日志和链路追踪体系,是排查线上问题的利器。

核心要点

  1. 日志级别设置:根据环境设置合适的级别

  2. 日志格式配置:添加 traceId 便于链路追踪

  3. 日志输出:控制台输出和文件输出的配置

  4. 日志归档:设置滚动策略和保留时间

总结

日志是排查问题的生命线,合理配置日志可以提升排查效率。在实际项目中,结合 ELK 等工具搭建日志系统,可以更好地管理和分析日志。


   转载规则


《SpringBoot日志配置和链路排查》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录