【自己造轮子】极简版实现端到端traceid生成

先说一个最简单的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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值