1.背景简述
- 依赖原始的log4j2配置,很难从某服务庞杂的日志中,单独找寻出某次API调用的全部日志。
- 本文通过在日志中打印唯一的traceId来实现每次调用的追踪。
2.关键思路
2.1.MDC
- 日志追踪目标是每次请求级别的,也就是说同一个接口的每次请求,都应该有不同的traceId。
- 每次接口请求,都是一个单独的线程,所以自然我们很容易考虑到通过ThreadLocal实现上述需求。
- 考虑到log4j本身已经提供了类似的功能MDC,所以直接使用MDC进行实现。
- 关于MDC的简述
- Mapped Diagnostic Context,即:映射诊断环境。
- MDC是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。
- MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。
- 关于MDC的关键操作
- 向MDC中设置值:
MDC.put(key, value);
- 从MDC中取值:
MDC.get(key);
- 将MDC中内容打印到日志中:
%X{key}
- 向MDC中设置值:
2.2.自定义Filter
- 假定已经解决了traceId的存放问题,那么何时进行traceId的存放呢?其实有多重实现思路,例如:过滤器、AOP、拦截器等等。
- 本文采用过滤器的形式,即:自定义一个Filter,继承自
GenericFilterBean
。 - 其他实现方式可自行探索。
3.实现步骤
3.1.原始示例
1.定义一个简单的接口:
/**
* <p></P>
*
* @author hanchao
*/
@RestController
public class DemoController {
private final Logger logger = Logger.getLogger(DemoController.class);
@GetMapping("/demo/by-name")
public String demo(String name) {
logger.info("name:" + name);
return name;
}
}
2.在浏览器调用接口: http://localhost:8080/demo/by-name?name=zhangsan
3.查看相关日志:
INFO pers.hanchao.trace.controller.DemoController:19 - name:zhangsan
3.2.TraceId操作工具类
增加TraceId操作的工具类,提供traceId的默认取值、setter、getter和生成。
/**
* <p>traceId工具类</P>
*
* @author hanchao
*/
public class TraceIdUtil {
private static final String TRACE_ID = "traceId";
/**
* 当traceId为空时,显示的traceId。随意。
*/
private static final String DEFAULT_TRACE_ID = "0";
/**
* 设置traceId
*/
public static void setTraceId(String traceId) {
//如果参数为空,则设置默认traceId
traceId = StringUtils.isBlank(traceId) ? DEFAULT_TRACE_ID : traceId;
//将traceId放到MDC中
MDC.put(TRACE_ID, traceId);
}
/**
* 获取traceId
*/
public static String getTraceId() {
//获取
String traceId = MDC.get(TRACE_ID);
//如果traceId为空,则返回默认值
return StringUtils.isBlank(traceId) ? DEFAULT_TRACE_ID : traceId;
}
/**
* 判断traceId为默认值
*/
public static Boolean defaultTraceId(String traceId) {
return DEFAULT_TRACE_ID.equals(traceId);
}
/**
* 生成traceId
*/
public static String genTraceId() {
return UUID.randomUUID().toString();
}
}
3.3.自定义TraceId过滤器
自定义过滤器,对全部请求进行traceId处理。这里处理有些粗暴,可自行细化。
/**
* <p>traceId过滤器,用于设置traceId</P>
*
* @author hanchao
*/
@WebFilter(urlPatterns = "/*", filterName = "traceIdFilter")
@Order(1)
public class TraceIdFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//traceId初始化
initTraceId((HttpServletRequest) servletRequest);
//执行后续过滤器
filterChain.doFilter(servletRequest,servletResponse);
}
/**
* traceId初始化
*/
private void initTraceId(HttpServletRequest request) {
//尝试获取http请求中的traceId
String traceId = request.getParameter("traceId");
//如果当前traceId为空或者为默认traceId,则生成新的traceId
if (StringUtils.isBlank(traceId) || TraceIdUtil.defaultTraceId(traceId)){
traceId = TraceIdUtil.genTraceId();
}
//设置traceId
TraceIdUtil.setTraceId(traceId);
}
}
3.4.启用自定义过滤器
不要忘记在SpringBoot的启动类加上@ServletComponentScan
注解,否则自定义的Filter无法生效。
/**
* 使用嵌入式容器时,可以使用@ServletComponentScan启用@WebServlet,@ WebFilter和@WebListener注释类的自动注册。
*/
@ServletComponentScan(basePackages = "pers.hanchao.trace.filter")
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
3.5.修改log4j2的layout格式
修改日志的layout
格式,将MDC中的traceId打印出来:
<!-- 原始格式 -->
<PatternLayout pattern="%5p %c:%L - %m %throwable{separator( --> )}%n"/>
<!-- 增加traceId的格式 -->
<PatternLayout pattern="%5p traceId:%X{traceId} %c:%L - %m %throwable{separator( --> )}%n"/>
3.6.受影响的示例
1.在浏览器多次调用接口: http://localhost:8080/demo/by-name?name=zhangsan
2.查看相关日志:
INFO traceId:5ee05f9b-432c-401f-ae24-6adaf2f31cf4 pers.hanchao.trace.controller.DemoController:19 - name:zhangsan
INFO traceId:b835352f-3a22-462c-965e-c426309ae3b8 pers.hanchao.trace.controller.DemoController:19 - name:zhangsan
INFO traceId:942d4f8f-f3c6-4688-a534-3429b6c9e92d pers.hanchao.trace.controller.DemoController:19 - name:zhangsan
4.更多思考
- 如果一个业务操作,需要进行多个服务的多个接口相互调用,则如何传递traceId?