一、前言
在编码过程中,常常需要写打印日志语句,我们期望的是同一个业务的日志都在一块,在出问题的时候好根据日志来排查问题。而现实是在应用运行中,日志的输出常常来自不同线程,甚至是在不同微服务中,各种日志记录往往彼此穿插,很难串起来。所以往往在日志中手动增加一些关键字,来对接口的调用链路来进行跟踪。但这种手动增加关键字或唯一标识的做法在微服务场景下,很难在上下游应用的开发人员的编码风格形成统一的规范,并且手动编写也很难称得上优雅。
二、MDC介绍
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,MDC 中包含的内容可以被同一线程中执行的代码所访问。
MDC中的键值对是可以直接被日志框架所使用(即[打印])的,只需要配置相应日志pattern。例如pattern如下:
% d { HH : mm : ss .SSS } [ % thread ] [ % X { TraceId } ] %- 5 level % logger { 50 } - % msg % n
代码如下:
public class MDCTest { private static final Logger log = LoggerFactory .getLogger ( MDCTest .class ) ; @Test void test ( ) { MDC .put ( "TraceId" , "123456789" ) ; log .info ( "hello {}" , "world" ) ; } }
此时控制台将输出:
21 : 16 : 04.342 [ main ] [ 123456789 ] INFO com .nk .MDCTest - hello world
三、实现方案
1、基本思路
修改日志pattern,并在业务开始的时候将trace id放入到MDC,在业务结束时去除MDC的trace id。这样的好处便是代码简洁,不需要手动写trace id,日志风格也能保持统一。
业务开始的时机一般是应用收到HTTP请求,所以可以用Filter或SpringMVC的Interceptor来对MDC中trace id进行初始化和清除。在Dubbo调用的时候也可以通过类似功能的Filter来对MDC中trace id进行操作,从而达到trace id传递的作用。
2、实现(以SpringBoot为例)
2.1 修改log pattern在SpringBoot中,直接修改application.properties即可:
logging .pattern .console =% d { yyyy - MM - dd HH : mm : ss .SSS } [ % thread ] [ % X { TraceId } ] %- 5 level % logger { 50 } - % msg % n
重点在于%X{TraceId},其中TraceId需要作为key出现在MDC里。
2.1.1 业务开始
TraceId工具类,封装MDC关于trace id的基础操作:
public final class TraceIdUtil { private static final String TRACE_ID_KEY = "TraceId" ; private TraceIdUtil ( ) { } public static void putIfAbsent ( ) { if ( StrUtil .isBlank ( get ( ) ) ) { put ( UUID .randomUUID ( ) .toString ( ) ) ; } } public static void remove ( ) { if ( get ( ) != null ) { MDC .remove ( TRACE_ID_KEY ) ; } } public static String get ( ) { return MDC .get ( TRACE_ID_KEY ) ; } public static void put ( String traceId ) { MDC .put ( TRACE_ID_KEY , traceId ) ; } }
Filter方式和Interceptor二选其一既可,其基本思想是一样的。
Filter方式
@Component public class LogFilter implements Filter { @Override public void doFilter ( ServletRequest servletRequest , ServletResponse servletResponse , FilterChain filterChain ) throws IOException , ServletException { TraceIdUtil .putIfAbsent ( ) ; // 生成trace id放入MDC中 try { filterChain .doFilter ( servletRequest , servletResponse ) ; } finally { TraceIdUtil .remove ( ) ; // 移除MDC中的trace id } } }
Interceptor
@Configuration public class LogInterceptor implements WebMvcConfigurer { @Override public void addInterceptors ( InterceptorRegistry registry ) { registry .addInterceptor ( new AsyncHandlerInterceptor ( ) { @Override public boolean preHandle ( HttpServletRequest request , HttpServletResponse response , Object handler ) throws Exception { TraceIdUtil .putIfAbsent ( ) ; // 生成trace id放入MDC中 return AsyncHandlerInterceptor .super .preHandle ( request , response , handler ) ; } @Override public void afterCompletion ( HttpServletRequest request , HttpServletResponse response , Object handler , Exception ex ) throws Exception { TraceIdUtil .remove ( ) ; // 移除MDC中的trace id AsyncHandlerInterceptor .super .afterCompletion ( request , response , handler , ex ) ; } } ) ; WebMvcConfigurer .super .addInterceptors ( registry ) ; } }
2.1.2 业务中使用
正常使用logger,无需关心trace id。例如:
@RestController @RequestMapping ( "/api/user" ) @Slf4j public class UserController { @Autowired private UserService userService ; @GetMapping ( "/{userId}" ) public UserDto queryUser ( @PathVariable Long userId ) { log .info ( "query user by id:{}" , userId ) ; UserDto user = userService .query ( userId ) ; log .info ( "query user result:{}" , user ) ; return user ; } }
请求该接口将输出如下的日志样式:
2022 - 04 - 05 09 : 40 : 17.638 [ http - nio - 8080 - exec - 1 ] [ a02b13d81c224e49956afd4efbb85ca8 ] INFO com .nk .webapp .controller .UserController - ready to query user by id : 1 2022 - 04 - 05 09 : 40 : 17.670 [ http - nio - 8080 - exec - 1 ] [ a02b13d81c224e49956afd4efbb85ca8 ] INFO com .nk .webapp .controller .UserController - query result : UserDto ( userId = 1 , username = zhang3 , age = 23 , email = abc@example 测试数据 )
四、总结
日志链路的跟踪核心是使用MDC作为trace id载体,在业务开始阶段一般通过拦截器就生成trace id并放入到MDC中,并根据MDC的相关特性将trace id投射到日志文本中,从而实现在同一个业务调用链路中的日志具有唯一标识。
原文地址:https://mp.weixin.qq测试数据/s/eEIQ6w_fYgMJ7gzkmuh3Xw
查看更多关于Java Web中日志跟踪的简单实现的详细内容...