MDC在做日志跟踪的时候用的比较多。一个系统提供服务,提供给其他系统来调用,当其他系统调用的时候,入参带上一个唯一的请求标识(traceId),把这个traceId输出到日志中,这样两个系统直接就会形成一个执行链,用traceId串联起来,当出现错误时,可以在调用方查询对应的请求日志,也可以在服务方查询请求日志。定位问题很方便。输出日志的地方很多,不能每次输出都去获取traceId,拼接到日志中,这样做很不优雅,也很容易遗漏。这个时候使用MDC配合logback中的pattern就很简单啦。
首先请求过来,将traceId放到MDC中,然后在pattern中用表达式从MDC中获取到对应的traceId。
对MDC不熟悉的同学可以先阅读一下我的上一篇文章: Slf4j MDC 实现机制与应用
首先,我们封装一个 TraceContext工具类,如下:
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import java.util.concurrent.TimeUnit;
/**
* @author Ricky Fung
*/
public abstract class TraceContext {
private static final ThreadLocal<Long> timeThreadLocal = new ThreadLocal();
//---------接口耗时统计
/**
* 开始计时
* @return
*/
public static long start() {
long startTime = System.nanoTime();
timeThreadLocal.set(startTime);
return startTime;
}
/**
* 获取接口耗时
* @return
*/
public static long stopAndGet() {
long endTime = System.nanoTime();
Long startTime = timeThreadLocal.get();
if (startTime == null) {
throw new IllegalArgumentException("必须先调用start方法");
}
//移除
timeThreadLocal.remove();
long costTime = endTime - startTime;
return TimeUnit.MILLISECONDS.convert(costTime, TimeUnit.NANOSECONDS);
}
//---------trace相关
public static String getTraceId() {
return MDC.get(CommonConstants.SLF4J_TRACE_ID);
}
public static String getSpanId() {
return MDC.get(CommonConstants.SLF4j_SPAN_ID);
}
/**
* 获取traceId,如果不存在则生成一个
* @return
*/
public static String computeTraceId() {
String traceId = getTraceId();
if (StringUtils.isEmpty(traceId)) {
traceId = UUIDUtils.getId();
}
return traceId;
}
public static void putTraceId(String traceId) {
MDC.put(CommonConstants.SLF4J_TRACE_ID, traceId);
}
public static void putSpanId(String spanId) {
MDC.put(CommonConstants.SLF4j_SPAN_ID, spanId);
}
//-------
public static void removeTraceId() {
MDC.remove(CommonConstants.SLF4J_TRACE_ID);
}
public static void removeSpanId() {
MDC.remove(CommonConstants.SLF4j_SPAN_ID);
}
public static String genSpanId(String spanId) {
if (StringUtils.isEmpty(spanId)) {
return CommonConstants.ROOT_SPAN_ID;
}
return spanId+".1";
}
public static String rootSpanId() {
return CommonConstants.ROOT_SPAN_ID;
}
}
其中,CommonConstants定义了几个常量:
/**
* @author Ricky Fung
*/
public class CommonConstants {
//HTTP请求头字段
public static final String TRACE_ID_HEADER = "X-Trace-Id";
public static final String SPAN_ID_HEADER = "X-Span-Id";
//Logback参数
public static final String SLF4J_TRACE_ID = "X-TraceId";
public static final String SLF4j_SPAN_ID = "X-SpanId";
public static final String ROOT_SPAN_ID = "0";
}
利用Spring MVC提供的 org.springframework.web.servlet.HandlerInterceptor
我们可以很方便的拦截HTTP请求,对请求处理前后做一些处理。
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author Ricky Fung
*/
public class TraceInterceptor extends HandlerInterceptorAdapter {
private final Logger LOG = LoggerFactory.getLogger(this.getClass());
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取trace
String traceId = request.getHeader(CommonConstants.TRACE_ID_HEADER);
String spanId = request.getHeader(CommonConstants.SPAN_ID_HEADER);
if (StringUtils.isEmpty(traceId)) { //生成根
traceId = UUIDUtils.getId();
}
//生成spanId
if (StringUtils.isEmpty(spanId)) {
spanId = CommonConstants.ROOT_SPAN_ID;
} else {
spanId = TraceContext.genSpanId(spanId);
}
//设置trace
TraceContext.putTraceId(traceId);
TraceContext.putSpanId(spanId);
TraceContext.start();
LOG.info("客服呼叫中心-链路追踪开始, traceId:{}, spanId:{}", traceId, spanId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
long costTime = TraceContext.stopAndGet();
LOG.info("客服呼叫中心-链路追踪结束, 接口URI:{}, 耗时:{} ms", request.getRequestURI(), costTime);
//清除
TraceContext.removeTraceId();
TraceContext.removeSpanId();
}
}
在preHandle方法中,我们获取HttpServletRequest中的TraceId 和 SpanId,如果二者为空我们就生成一个并put到MDC中。
在afterCompletion方法中,我们做一些清理工作,清除我们之前设置的TraceId 和 SpanId。
另外,就是应用在请求三方系统时,需要带上TraceId,这样整个链路就串联起来了,代码如下:
@Resource
private RestTemplate restTemplate;
private String doRequest(Long userId, String token, String payload, String apiUri, HttpMethod httpMethod) {
//trace
String traceId = TraceContext.getTraceId();
String spanId = TraceContext.getSpanId();
LOG.info("客服呼叫中心-转发请求开始, userId:{}, traceId:{}, httpMethod:{}, apiUri:{}", userId, traceId, httpMethod, apiUri);
StringBuilder sb = new StringBuilder(80);
sb.append(bpHost).append(apiUri);
String reqUrl = sb.toString();
//创建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add(CommonConstants.AUTHORIZATION_HEADER, token);
headers.add(CommonConstants.TRACE_ID_HEADER, StringUtils.isNotEmpty(traceId) ? traceId : StringUtils.EMPTY);
headers.add(CommonConstants.SPAN_ID_HEADER, StringUtils.isNotEmpty(spanId) ? spanId : StringUtils.EMPTY);
HttpEntity<String> httpEntity = new HttpEntity<>(payload, headers);
ResponseEntity<String> responseEntity = restTemplate.exchange(reqUrl, httpMethod, httpEntity, String.class);
if (responseEntity.getStatusCode() != HttpStatus.OK) {
LOG.info("客服呼叫中心-转发请求, userId:{}, traceId:{} 请求服务器:{} 状态码异常:{}", userId, traceId, reqUrl, responseEntity.getStatusCodeValue());
}
String json = responseEntity.getBody();
LOG.info("客服呼叫中心-转发请求, userId:{}, traceId:{}, 请求服务器:{} 响应结果:{}", userId, traceId, reqUrl, json);
if (StringUtils.isNotEmpty(json)) {
return json;
}
return null;
}
在logback.xml中如果想输出MDC中的自定义属性,可以通过 %X{propertyName}
方式。
logback-spring.xml 如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{X-TraceId} | %X{X-SpanId}] %logger - %msg%n"/>
<springProperty scope="context" name="prodName" source="spring.application.name" defaultValue="crm-call-center-proxy"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
我们通过 %X{X-TraceId} 和 %X{X-SpanId} 输出TraceContext中put到MDC中的TraceId 和 SpanId。