美文网首页
对request-id的认识和配置

对request-id的认识和配置

作者: WAHAHA402 | 来源:发表于2020-03-31 14:29 被阅读0次

  参与的公司项目中对每个请求都加上了request-id写入响应头,同时在springboot项目中使用logback打印到日志,非常方便追踪问题,项目中没有使用到分布式微服务,但是好处已经非常明显。在排查问题的时候可以根据浏览器控制台查看收到的request-id请求头去搜索服务器日志文件,快速定位问题。

一、为什么要使用request-id?

使用request-id是为了解决下面的问题
问题一: 客户端访问的Web服务时,如何将客户端请求与服务端日志关联
问题二: 微服务架构下,访问日志如何查询
问题三: 不同项目交互出现异常,如何做日志关联

没有requsest-id的项目,如果项目是分布式微服务来实现的,代码层层封装后,无法通过日志关键与用户请求关联。
微服务架构下,用户请求逻辑层分解多个子任务给下层服务处理,下层服务无法与用户请求关联。
不同项目交互,如何在并发,错误重试,参数相同的情况下,无法通过关键字,时间来确定日志

使用request-id的好处
  • 当前项目,根据request id 可以找到所有与请求相关的日志
  • 不同项目,可以根据request id 确定唯一的请求
  • 用户请求与日志关联,项目间请求日志也可以关联
使用request-id
  • 要有配套日志记录系统
  • 周边系统支持,保持统一
  • request id 每次用户请求,必须保证唯一
  • 多服务间日志聚合、调用关系分析
  • 日志分析
二、request-id实现思路

  使用springboot开发分布式应用,很多都微服务化,当请求过来,可能需要调用多个服务来完成请求动作。在查询日志时,特别是请求量大的情况下,日志多,很难找到对应请求的日志,造成定位异常难,日志难以追踪等问题。针对此类问题,logback 提供了 MDC ( Mapped Diagnostic Contexts 诊断上下文映射 ),MDC可以让开发人员可以在 诊断上下文 中放置信息,这些消息是内部使用了 ThreadLocal实现了线程与线程之间的数据隔离,管理每个线程的上下文信息 。而在日志输出时,可以通过标识符%X{key} 来输出MDC中的设置的内容。因此,在分布式应用在追踪请求时,实现思路如下:

  1. web应用中,添加拦截器,在请求进入时,添加唯一id作为request-id,以标识此次请求。
  2. 添加此 request-id 到MDC中
  3. 若需要调用其它服务,把此request-id作为 header 参数
  4. 在日志输出时,添加此request-id的输出作为标识
  5. 请求结束后,清除此request-id
三、代码实现
1.添加拦截器

  通过拦截器,实现在请求前添加request-id放到 MDC 中,请求完成后清除的动作。类定义如下:

@Component
public class RequestIdFilter extends OncePerRequestFilter {

    private static final String KEY_REQUEST_ID = "X-Request-Id";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        try {
            String requestId = request.getHeader(KEY_REQUEST_ID);
            if (StringUtils.isBlank(requestId)) {
                requestId = UUID.randomUUID().toString();
            }
            //将生成的request-id放入MDC中,允许打印日志时获取
            MDC.put("RequestId", requestId);
            
            //将request-id写入响应头
            response.addHeader(KEY_REQUEST_ID, requestId);
            //传入其他过滤器
            chain.doFilter(request, response);
        } finally {
            MDC.clear();
        }

    }
}

附一个过滤器:CommonsRequestLoggingFilter
继承这个类,可以针对请求打印日志

2.添加RequestLoggingFilter
@Slf4j
@Component
public class RequestLoggingFilter extends CommonsRequestLoggingFilter {
    private static final String KEY_REQUEST_START_TIME = "request_logging_request_start_time";

    private boolean ignoreStatic;

    private AntPathMatcher pathMatcher;
    private Collection<String> ignorePathPatterns;
    private Collection<String> ignorePaths;

    private final ResourceUrlProvider resourceUrlProvider;

    @Autowired
    public RequestLoggingFilter(ResourceUrlProvider resourceUrlProvider) {
        this.resourceUrlProvider = resourceUrlProvider;
        this.pathMatcher = new AntPathMatcher();
        this.ignorePathPatterns = new LinkedHashSet<>();
        this.ignorePaths = new LinkedHashSet<>();
    }

    @Override
    protected void beforeRequest(HttpServletRequest request, String message) {
        if (!shouldSkip(request)) {
            //对不该跳过的请求添加属性,记录请求开始的时刻
            request.setAttribute(KEY_REQUEST_START_TIME, Instant.now());
          //调用父类CommonsRequestLoggingFilter的方法,点进去看实现是打印message(请求路径)
            super.beforeRequest(request, message);
        }
    }

    @Override
    protected void afterRequest(HttpServletRequest request, String message) {
        if (!shouldSkip(request)) {
            Instant startTime = (Instant) (request.getAttribute(KEY_REQUEST_START_TIME));
            long elapse = Duration.between(startTime, Instant.now()).toMillis();
            //打印请求耗时
            message = String.format("%s, roundtrip elapse %d ms.", message, elapse);
            super.afterRequest(request, message);
        }
    }

    public void setIgnoreStatic(boolean ignoreStatic) {
        this.ignoreStatic = ignoreStatic;
    }

    private boolean shouldSkip(HttpServletRequest request) {
        if (!ignoreStatic) {
            return false;
        }

        String path = request.getRequestURI();
        boolean isStatic = resourceUrlProvider.getForLookupPath(path) != null;
        if (isStatic) {
            return true;
        }

        if (ignorePaths.contains(path)) {
            return true;
        }

        for (String pattern : ignorePathPatterns) {
            if (pathMatcher.match(pattern, path)) {
                ignorePaths.add(path);
                return true;
            }
        }

        return false;
    }

    public void addIgnorePathPattern(String... patterns) {
        for (String p : patterns) {
            if (!pathMatcher.isPattern(p)) {
                throw new IllegalArgumentException(String.format("invalid pattern: %s", p));
            }
        }
        Collections.addAll(this.ignorePathPatterns, patterns);
    }

    public void addIgnorePath(String... paths) {
        Collections.addAll(this.ignorePaths, paths);
    }
}
3.针对以上俩过滤器的配置类
@Configuration
public class RequestLoggingFilterConfig {

    private final RequestLoggingFilter requestLoggingFilter;
    private final RequestIdFilter requestIdFilter;

    @Autowired
    public RequestLoggingFilterConfig(RequestLoggingFilter requestLoggingFilter, RequestIdFilter requestIdFilter) {
        this.requestLoggingFilter = requestLoggingFilter;
        this.requestIdFilter = requestIdFilter;
    }

    @Bean
    public FilterRegistrationBean registerRequestIdFilter() {
        FilterRegistrationBean<RequestIdFilter> bean = new FilterRegistrationBean<>(requestIdFilter);
        bean.setFilter(requestIdFilter);
        //设置过滤器类的执行顺序,这里设为最高,确保request-id的过滤器第一个执行
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }

    @Bean
    public FilterRegistrationBean registerRequestLoggingFilter() {
        requestLoggingFilter.setIncludeQueryString(true);
        requestLoggingFilter.setIncludePayload(true);
        requestLoggingFilter.setMaxPayloadLength(10000);
        requestLoggingFilter.setIncludeHeaders(false);
        requestLoggingFilter.setIgnoreStatic(true);
        requestLoggingFilter.addIgnorePath("/", "/v2/api-docs", "/csrf");
        requestLoggingFilter.addIgnorePathPattern("/swagger-resources/*");

        FilterRegistrationBean<RequestLoggingFilter> bean = new FilterRegistrationBean<>(requestLoggingFilter);
        bean.setFilter(requestLoggingFilter);
        bean.setOrder(1);
        return bean;
    }

    
}

springboot中默认使用logback作为日志实现,最后在application-xxx.yml文件中简单配置即可:

logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder: DEBUG
    org.springframework.jdbc.core.JdbcTemplate: DEBUG
    org.springframework.jdbc.core.StatementCreatorUtils: TRACE
    org.springframework.web.filter.CommonsRequestLoggingFilter: DEBUG
   -- 上面两个过滤器所在包路径
    cn.xxx.xxx.core.common.filter.*: DEBUG
  pattern:
  -- 获取MDC中的属性并打印
    level: "%X{RequestId} %5p"

相关文章

  • 对request-id的认识和配置

    参与的公司项目中对每个请求都加上了request-id写入响应头,同时在springboot项目中使用logbac...

  • 对前端的认识和看法

    什么是前端 ? 1 前端它是一个工作,它的工作领域是浏览器,它即要跟美工、设计打交道又要懂点后台代码,知道ajax...

  • 对Retrofit的认识和学习

    对Retrofit的认识和学习 之前 都是用okhttp 我觉得挺好用的 特别是弘扬老师的封装,用起来真的得心应手...

  • 对Xposed的认识和思考

    Xposed是什么 Xposed 是一款可以在不修改 APK 的情况下影响程序运行的框架服务,基于 Xposed ...

  • 对GDP的认识和反思

    ——《GDP简史》书评 提起GDP,相信大家都不陌生,它经常出现在新闻中,一个简单的数字,用于衡量它所属国家或地区...

  • 对前端的认识和看法

    什么是前端 ? 1 前端它是一个工作,它的工作领域是浏览器,它即要跟美工、设计打交道又要懂点后台代码,知道ajax...

  • 对道的认识和思考

    什么是道?什么是值得人为之奋斗一生追求的大道? 这个问题在不同的历史时期有很多的内涵。 对于人道来说,让所有的人都...

  • 对“搞钱”的感受和认识

    经常会听到有人说搞钱这个词,在不同的场景,感受有点不一样。有时候听到搞钱这个词,心理就有不舒服的感觉。最不舒服的就...

  • 对命运和人的认识。

    命运和人每个人都有自己的命运,而每个人的命运也是不同,有人想改变自己的命运,而有些人不想改变自己的命运。 ...

  • 对知识的理解和认识

    某些人方面来看,知识这东西,不可能像一个模子里刻出来的那样,如果真是那样的话,恐怕知识就真的成了无源之水了。一个人...

网友评论

      本文标题:对request-id的认识和配置

      本文链接:https://www.haomeiwen.com/subject/fofgyhtx.html