背景
现在大多数企业开发的系统都是分布式系统了,随着系统的复杂如何有效地追踪定位线上问题也变得更加困难。我们可以使用requestId(traceId)来解决这一问题。
实现思路
- 使用过滤器或切面在每个HTTP请求进到Controller前生成一个唯一标识请求的requestId,可以使用UUID,放在ThreadLocal里,方便上下文调用。
public static final String REQUEST_ID_KEY = "requestId";
public static ThreadLocal<String> requestIdThreadLocal = new ThreadLocal<String>();
private static final Logger logger = LoggerFactory.getLogger(RequestIdUtil.class);
public static String getRequestId(HttpServletRequest request) {
String requestId = null;
String parameterRequestId = request.getParameter(REQUEST_ID_KEY);
String headerRequestId = request.getHeader(REQUEST_ID_KEY);
if (parameterRequestId == null && headerRequestId == null) {
logger.info("request parameter 和header 都没有requestId入参");
requestId = UUID.randomUUID().toString();
} else {
requestId = parameterRequestId != null ? parameterRequestId : headerRequestId;
}
requestIdThreadLocal.set(requestId);
return requestId;
}
- requestId结合日志打印,这里使用slf4j标准里的MDC实现,例子使用logback打印日志。
代码使用:
MDC.put("requestId", requestId);
logback配置示例:
<configuration>
<appender name="logfile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<Encoding>UTF-8</Encoding>
<File>${log_base}/java-base-web.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${log_base}/java-base-web-%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<MaxHistory>10</MaxHistory>
<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<MaxFileSize>200MB</MaxFileSize>
</TimeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d^|^%X{requestId}^|^%-5level^|^%logger{36}%M^|^%msg%n</pattern>
</layout>
</appender>
<root level="info">
<appender-ref ref="logfile" />
</root>
</configuration>
主要是在pattern那里指定,使用MDC里面的变量用的是%X{requestId},requestId为放进MDC里面的key,
达到的效果就是打印出来的日志自动带上了requestId。效果如下:
[a0b3589f-fe87-4d3a-9151-8925afd0a6c6] at com.test.demo.TestMain2.test2(TestMain2.java:26)
相当于切日志,这样我们线上就可以使用requestId在ELK上搜索属于这个请求的日志了。
- 使用HTTP Client或者OKHTTP等在后台请求其他系统的服务的时候把放在上下文的requestId放在请求参数或者头里,推荐放在header,上面的切面一开始会判断requestId是否在请求里面,有的话就继续沿用调用方的requestId,这样子在被调用的系统也能追踪到属于同一个调用链的日志了。
String requestId = RequestIdUtil.requestIdThreadLocal.get();
headerMap.put(RequestIdUtil.REQUEST_ID_KEY, requestId);
Map<String, String> paramMap = new HashMap<String, String>();
String resultString = JsonHttpClientUtil.post(testHttpClientUrl, headerMap, paramMap, "UTF-8");
logger.info(resultString);
- requestId可以放到一个封装响应类里面,这样子我们就可以在控制台里面看到响应参数里面的requestId,方便在ELK里面查找日志了。也可以结合异常体系使用,抛的异常打印的日志自然也带上了requestId,同时可以使用钉钉机器人来通知开发区定位问题,同时带上requestId和异常堆栈信息就行了。这样就能方便的知道异常发生的上下文日志了。
总结
上面总结了一些工作中使用起来比较便利的定位问题的方法体系。希望可以给读者带来一些启示。