先说一个最简单的traceid使用场景(非跨服务情况):
前端请求你的服务后返回结果带有traceid,然后开发同学呢就能根据traceid去服务机器上进行grep -a -i '<traceid值>' *.log 排查问题。
那么依据这个场景中的流程,分析出待解决的问题主要是下面3个:
1.controller请求到服务后traceid如何生成 2.日志中怎么配置traceid 3.返回结果如何带有traceid
首先第一个问题其实有多种解决办法,比如
1.1通过spring aop的注解,让controller的每个方法运行doAround前生成trace
1.2基于对spring mvc的了解,一个请求到达服务的时候,是需要进行一堆Filter的链式处理,那么就可以把traceid生成的功能放到自定义的一个Filter中,order最靠前
第二个问题生成的traceid该放在哪里,能让日志的配置知道这个变量的存在,这个就需要对slf4j配置以及关键的字段MDC有了解。日志打印会配置打印的格式,常见的就是年月日这种变量yyyy-MM-dd,而自定义的变量就是从MDC里面拿的,MDC可以理解为一个map,用于存放当前运行的线程中自定义的kv值
第三个问题也可以通过spring aop来实现,在返回结果中增加该字段,或者用spring web自带的注解@ControllerAdvice可以统一处理http请求的异常或者返回结果
下面就是用Filter+logback(slf4j的一种实现)+ControllerAdvice的demo
增加自定义Filter
@Component
@Order(1) //注意order要在最前面,这样保障traceid请求刚到达就生成
public class TraceIdFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(TraceIdFilter.class);
private static final String TRACE_ID = "traceId";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) request;
String originTraceId = servletRequest.getHeader(TRACE_ID);
if(StringUtils.isEmpty(originTraceId)) {
originTraceId = UUID.randomUUID().toString();
}
MDC.put(TRACE_ID, originTraceId);
try {
chain.doFilter(request, response);
} finally {
if(MDC.getCopyOfContextMap().containsKey(TRACE_ID)) {
MDC.remove(TRACE_ID);
}
}
}
@Override
public void destroy() {
}
}
配置带有自定义变量的日志
pattern配置:%thread是slf4j中系统自带的变量,而traceId是通过Filter塞到MDC里面的
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36}.%M\(%line\) - %msg %n
<appender name="INFO"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/info.log</file>
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<Pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36}.%M\(%line\) - %msg %n
</Pattern>
</encoder>
<rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- rollover daily -->
<fileNamePattern>${LOG_DIR}/archived/info.%d{yyyy-MM-dd}.%i.log
</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>1000MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>10</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
</appender>
返回结果的统一处理
注解使用
@ControllerAdvice,
实
现ResponseBodyAdvice中beforeBodyWrite,把返回结果set上MDC的traceid即可(说明
TraceApiResponse是所有controller用到的统一返回结果,里面有traceid字段)
@ControllerAdvice
@Slf4j
public class ResponseConfig implements ResponseBodyAdvice<Object> {
private static final Logger LOGGER = LoggerFactory.getLogger(ResponseConfig.class);
@ExceptionHandler(Exception.class)
@ResponseBody
public DispatchApiResponse handleException(Exception ex) {
TraceApiResponse apiResponse = new TraceApiResponse();
apiResponse.setResultMsg(ex.getMessage());
return apiResponse;
}
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
/**
* @param body
* @param returnType
* @param selectedContentType
* @param selectedConverterType
* @param request
* @param response
* @return
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 从请求头中提取traceId
String traceId = MDC.get(TracerConfig.TRACE_ID_HEADER);
if (body instanceof TraceApiResponse) {
TraceApiResponse<?> result = (TraceApiResponse<?>) body;
if (traceId != null) {
log.info("接收到的traceId值为:{}", traceId);
result.setTraceId(traceId);
}
if(CollectionUtils.isEmpty(((TraceApiResponse<?>) body).getIpAddrs())) {
try {
((DispatchApiResponse<?>) body).setIpAddrs(Lists.newArrayList(InetAddress.getLocalHost().getHostAddress()));
} catch (UnknownHostException e) {
LOGGER.error("get ip address error");
}
}
}
return body;
}
}
ok,本文介绍了如何如何利用Filter+logback+ControllerAdvice实现极简版端到端的traceid,大功告成
延伸一下:
Filter还可以用于做请求的前置鉴权,增加额外请求体处理等等
ControllerAdvice使用handleException上的注解可以统一请求异常时的返回
微信转载链接:【自己造轮子】极简版实现端到端traceid生成
欢迎关注:大头兵Tomato