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) { String traceId = UUID.randomUUID().toString().replace("-", ""); MDC.put("traceId", traceId); try { log.info("创建订单开始: {}", request.getOrderNo()); log.info("创建订单完成: {}", request.getOrderNo()); } finally { MDC.clear(); } } }
|
#
日志格式中加入 traceId
<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 { String traceId = request.getHeader(TRACE_ID_HEADER); if (StringUtils.isBlank(traceId)) { traceId = UUID.randomUUID().toString().replace("-", ""); } 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-"); 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.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 配置
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 |
日志收集与分析 |
完善的日志和链路追踪体系,是排查线上问题的利器。
核心要点
日志级别设置:根据环境设置合适的级别
日志格式配置:添加 traceId 便于链路追踪
日志输出:控制台输出和文件输出的配置
日志归档:设置滚动策略和保留时间
总结
日志是排查问题的生命线,合理配置日志可以提升排查效率。在实际项目中,结合 ELK 等工具搭建日志系统,可以更好地管理和分析日志。