spring boot +logBack 实现traceId
背景:在分布式服务架构下,一个 Web 请求从网关流入,有可能会调用多个服务对请求进行处理,拿到最终结果。在这个过程中每个服务之间的通信又是单独的网络请求,无论请求流经的哪个服务除了故障或者处理过慢都会对前端造成影响。
一、相关概念
在分布式链路追踪中有两个重要的概念:跟踪(trace)和 跨度(span)。trace 是请求在分布式系统中的整个链路视图,span 则代表整个链路中不同服务内部的视图,span 组合在一起就是整个 trace 的视图。
traceId:用于标识某一次具体的请求ID。当用户的请求进入系统后,会在RPC调用网络的第一层生成一个全局唯一的traceId,并且会随着每一层的RPC调用,不断往后传递,这样的话通过traceId就可以把一次用户请求在系统中调用的路径串联起来。
spanId,用于标识一次RPC调用在分布式请求中的位置。请求到达每个服务后,服务都会为请求生成spanId。当用户的请求进入系统后,处在RPC调用网络的第一层A时spanId初始值是0,进入下一层RPC调用B的时候spanId是0.1,继续进入下一层RPC调用C时spanId是0.1.1,而与B处在同一层的RPC调用D的spanId是0.2,这样的话通过spanId就可以定位某一次RPC请求在系统调用中所处的位置,以及它的上下游依赖分别是谁。
parent-spanId:用于标识上游RPC调用在分布式请求中的位置。请求到达每个服务后,随请求一起从上游传过来的上游服务的 spanId 会被记录成 parent-spanId,或者叫 pspanId。当前服务生成的 spanId 随着请求一起,在传到下游服务时,这个 spanId 又会被下游服务当作 parent-spanId 记录。
MDC:(Mapped Diagnostic Context)映射诊断环境,是 log4j 和 logback 提供的一种方便在线多线程条件下记录日志的功能,可以看成是一个与当前线程绑定的 ThreadLocal。
org.slf4j 1.7.25 版本
public static void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException(“key parameter cannot be null”);
} else if (mdcAdapter == null) {
throw new IllegalStateException(“MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA”);
} else {
mdcAdapter.put(key, val);
}
}
public static MDC.MDCCloseable putCloseable(String key, String val) throws IllegalArgumentException {
put(key, val);
return new MDC.MDCCloseable(key);
}
public static String get(String key) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key parameter cannot be null");
} else if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA");
} else {
return mdcAdapter.get(key);
}
}
public static void remove(String key) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key parameter cannot be null");
} else if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA");
} else {
mdcAdapter.remove(key);
}
}
public static void clear() {
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA");
} else {
mdcAdapter.clear();
}
}
二、写过滤器继承GenericFilterBean,请求就能拦截到
@Component
public class CustomerHttpSpanInterceptor extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
// 填充数据(适用logback、log4j 1.x)
MDC.put("userId","121242");
servletRequest.getAttribute("");
// 填充数据(适用log4j 2.x)
// ThreadContext.put(Contents.REQUEST_ID, UUID.randomUUID().toString());
filterChain.doFilter(servletRequest, servletResponse);
} finally {
// 请求结束时清除数据,否则会造成内存泄露问题
MDC.remove("traceId");
}
}
}
三、设置日志
在 log4j、logback中配置 %X{userId},即可在日志中打印userId。
[%X{userId:-}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%X{X-B3-TraceId:-}] [%X{X-B3-SpanId:-}] [%thread] %-5level %logger{50} %L - %msg%n
(如果是引用了sleuth,就可以直接使用X-B3-TraceId 和X-B3-SpanId
[%X{userId:-}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%X{X-B3-TraceId:-}] [%X{X-B3-SpanId:-}] [%thread] %-5level %logger{50} %L - %msg%n
)
日志打印实例:
[121242] 2022-07-14 10:52:36.621 [0b34b37d0e7655f0] [0b34b37d0e7655f0] [http-nio-13053-exec-1] DEBUG c.t.dao.IXXXXXDao.queryXXXXInfo 159 - <== Total: 1