Spring Boot 实现日志链路追踪,无需引入组件vs引入sleuth实现,让日志定位更方便!
前言
在生产环境中,由于节点众多日志打印超级快基本上无法得到有效的日志,靠搜索关键字
查日志能解决一些,但是不能完全精准的拿到想要的,也不能完全呈现整个链路相关的日
志。
那么为了解决这个问题,需要给同一次业务调用链上的日志串起来。
实现后的效果图

正文
无需引入组件方式实现
-
首选先创建一个登录拦截器

-
引入pom依赖 这里我使用的是slf4j
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
</dependency>
- 整合log4j2.xml,打印日志
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Properties>
<Property name="PID">????</Property>
<Property name="LOG_EXCEPTION_CONVERSION_WORD">%throwable</Property>
<Property name="LOG_LEVEL_PATTERN">%5p</Property>
<Property name="LOG_DATEFORMAT_PATTERN">yyyy-MM-dd HH:mm:ss.SSS</Property>
<property name="APP_NAME">fb-budget-service</property>
<Property name="LOG_SLEUTH_PATTERN">[${APP_NAME},[%traceId],%X{X-B3-TraceId},%X{X-B3-SpanId}]</Property>
<Property name="CONSOLE_LOG_PATTERN">%clr{%d{${LOG_DATEFORMAT_PATTERN}}}{faint}%clr{${LOG_LEVEL_PATTERN}} ${LOG_SLEUTH_PATTERN} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
<Property name="FILE_LOG_PATTERN">%d{${LOG_DATEFORMAT_PATTERN}} ${LOG_LEVEL_PATTERN} ${LOG_SLEUTH_PATTERN} ${sys:PID} --- [%t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" />
</Console>
<!--<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%X{X-B3-TraceId} %d{yyyy-MM-dd HH:mm:ss.SSS} %5p ---[%15.15t] %-40.40logger{39} : %m%n"/>
</Console>-->
</Appenders>
<Loggers>
<Logger name="org.apache.catalina.startup.DigesterFactory" level="error" />
<Logger name="org.apache.catalina.util.LifecycleBase" level="error" />
<Logger name="org.apache.coyote.http11.Http11NioProtocol" level="warn" />
<logger name="org.apache.sshd.common.util.SecurityUtils" level="warn"/>
<Logger name="org.apache.tomcat.util.net.NioSelectorPool" level="warn" />
<Logger name="org.eclipse.jetty.util.component.AbstractLifeCycle" level="error" />
<Logger name="org.hibernate.validator.internal.util.Version" level="warn" />
<logger name="org.springframework.boot.actuate.endpoint.jmx" level="warn"/>
<Root level="info">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</Configuration>
- 自定义日志拦截器 LogInterceptor.java中增加 traceid
前端每次请求都会传的一个 32 位的随机 id 在请求头中和 tokenid 一样,在
LoginInterceptor 里 preHandle 里放到 MDC 里
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
String traceId = request.getHeader("traceId");
MDC.put("traceId",traceId);
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
- MDC
MDC (Mapped Diagnostic Context,映射调试上下文)是 log4j logback 及 log4j2 提供的
一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希
表,内部是基于 threadLoca 实现,可以往其中添加键值对,MDC 中包合的内容可以被同一
执行的代码所访问。当开启 isThreadContextMapinheritable 属性后,当前线程的子线程
会继承其父线程中的 MDC 的内容。当需要记录日志时,日志框架会自动从 MDC 中获取所
需的信息,MDC 的内容需要由程序在适当的时候保存进去
- WebMvcConfigurerAdapter.java 添加拦截器
@Configuration
public class FiscalMvcConfig extends WebMvcConfigurerAdapter {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor).
addPathPatterns("/**")
;
}
}
到这里其实已经完成了,但是还有一个问题,在开启新的线程后MDC在新的线程中获取不到traceId,所以我们需要针对子线程使用情形,做调整,思路:将父线程的trackId传递下去给子线程即可。
- ThreadPoolConfig.java 定义线程池,交给spring管理
/**
* Description 继承ThreadPoolExecutor,多线程处理任务时传递日志trace_id
*/
public class ThreadPoolExecutorMdcUtil extends ThreadPoolExecutor {
public ThreadPoolExecutorMdcUtil(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public ThreadPoolExecutorMdcUtil(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}
public ThreadPoolExecutorMdcUtil(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}
public ThreadPoolExecutorMdcUtil(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
@Override
public void execute(Runnable task) {
super.execute(wrap(task));
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return super.submit(wrap(task));
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(wrap(task));
}
private <T> Callable<T> wrap(final Callable<T> callable) {
Map<String, String> context = MDC.getCopyOfContextMap();
return () -> {
if (context != null) {
MDC.setContextMap(context);
}
try {
return callable.call();
} finally {
MDC.clear();
}
};
}
private Runnable wrap(final Runnable runnable) {
Map<String, String> context = MDC.getCopyOfContextMap();
return () -> {
if (context != null) {
MDC.setContextMap(context);
}
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}
创建线程池时使用ThreadPoolExecutorMdcUtil创建即可,会把父线程的MDC的上下文传递给子线程,这样就可以实现多线程的日志链路追踪了。
当然也可以手动的传递例如
ThreadUtil.execute(() -> {
try {
MDC.put(ConstantConfig.TRACE_ID, traceID);
} catch (Exception e){
log.error("getFormerData2",e);
} finally {
downLatch.countDown();
}
});
引入sleuth实现
- 引入pom依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
<version>1.3.6.RELEASE</version>
</dependency>
- 整合log4j2.xml,打印日志
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Properties>
<Property name="PID">????</Property>
<Property name="LOG_EXCEPTION_CONVERSION_WORD">%throwable</Property>
<Property name="LOG_LEVEL_PATTERN">%5p</Property>
<Property name="LOG_DATEFORMAT_PATTERN">yyyy-MM-dd HH:mm:ss.SSS</Property>
<property name="APP_NAME">fb-budget-service</property>
<Property name="LOG_SLEUTH_PATTERN">[${APP_NAME},[%traceId],%X{X-B3-TraceId},%X{X-B3-SpanId}]</Property>
<Property name="CONSOLE_LOG_PATTERN">%clr{%d{${LOG_DATEFORMAT_PATTERN}}}{faint}%clr{${LOG_LEVEL_PATTERN}} ${LOG_SLEUTH_PATTERN} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
<Property name="FILE_LOG_PATTERN">%d{${LOG_DATEFORMAT_PATTERN}} ${LOG_LEVEL_PATTERN} ${LOG_SLEUTH_PATTERN} ${sys:PID} --- [%t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" />
</Console>
<!--<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%X{X-B3-TraceId} %d{yyyy-MM-dd HH:mm:ss.SSS} %5p ---[%15.15t] %-40.40logger{39} : %m%n"/>
</Console>-->
</Appenders>
<Loggers>
<Logger name="org.apache.catalina.startup.DigesterFactory" level="error" />
<Logger name="org.apache.catalina.util.LifecycleBase" level="error" />
<Logger name="org.apache.coyote.http11.Http11NioProtocol" level="warn" />
<logger name="org.apache.sshd.common.util.SecurityUtils" level="warn"/>
<Logger name="org.apache.tomcat.util.net.NioSelectorPool" level="warn" />
<Logger name="org.eclipse.jetty.util.component.AbstractLifeCycle" level="error" />
<Logger name="org.hibernate.validator.internal.util.Version" level="warn" />
<logger name="org.springframework.boot.actuate.endpoint.jmx" level="warn"/>
<Root level="info">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</Configuration>
- 这里需要注意sleuth封装的key名称是:X-B3-TraceId
- 这里需要注意sleuth封装的value改成: 只能包含这些字符{ 0123456789abcdef }也就是16进制,不能有大写字母
- 如果前端没有传X-B3-TraceId参数会自动生成一个
- 这里添加一个切面统一把X-B3-TraceId添加到接口返回值当中
@RestControllerAdvice
public class ResponseBodyFilter implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (o instanceof Map) {
((Map<Object, Object>) o).putIfAbsent("env", System.getenv("HOSTNAME"));
((Map<Object, Object>) o).putIfAbsent(Span.TRACE_ID_NAME, BudgetUtils.getTraceId());
}
return o;
}
}
到这里也就实现了,总体对比下来发现引入sleuth实现更加方便快捷
本文介绍了如何在SpringBoot应用中使用无需引入额外组件的方式实现日志链路追踪,以及与SpringCloudSleuth的Sleuth组件进行比较,突出了两者在功能实现和便捷性上的差异。
1475





