Spring Boot项目实现请求日志链路追踪

768 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第30天,点击查看活动详情

前言

如何获取一次请求打印的所有日志,是我们在工作中必须要解决的事情。我们在开发的过程中可能本地做功能测试的时候,都是先启动本地项目,然后发送一次请求,然后在开发工具中查看本次请求的日志,根据日志进行功能的调试,调试完成后再进行第二次请求发送,然后再看日志。在开发过程中,这样查看日志时没问题的。

但是在生产环境这绝对是完全不能接受的。因为开发过程中,自己能控制发送请求的时机,会等自己将某次请求的日志查看完成后再发送请求。生产过程中可能会发生很多次请求,而且是不定时的在发送,如果生产上出现了问题,需要查看请求日志,以便排查,但是我们怎么定位那次请求呢。也许我们可以通过grep关键字,比如请求中有手机号或者身份证号这种具有唯一性的关键字,可以对日志文件进行手机号的查看grep 13333333333 monitor.log,这个时候我们定位到了一条或者多条日志,但是出现了一个问题,我们怎么查看这次请求中其他的日志呢,没办法查看,因为生产环境的复杂性,可不是说让使用者都先听一下,让有问题的使用方先发送一次请求,等开发人员看完日志之后大家再开始使用。这是不可能的

怎么解决这个问题呢,实际生产中可以在日志输出的时候都会打印一个具有唯一性的字符串,这个字符串在每一次请求打印的所有日志中都会存在,而且每一次请求这个字符串都不一样,这个时候排查问题就可以先通过请求参数的关键字进行查询,查询到关键字的日志,获取打印日志的唯一性的字符串,再通过唯一字符串搜索本次请求的所有日志。

image.png

现在关键就是如何在每一次请求中打印不一样的唯一字符串到日志中。这个时候就需要用到MDC。下面举例说明如何从0开始在Spring Boot项目中通过MDC打印和跟踪请求日志。

环境说明

开发工具:idea Spring Boot:2.6.9 JDK:1.8

搭建项目

创建一个Spring Boot工程,通过Spring Initializr初始化器创建,项目名称就使用默认的demo。注意更改一下JDK版本为自己当前的版本,建议是JDK1.8或以上版本,点击下一步。

image.png

选择Spring Boot的版本为2.6.9,添加一个Spring Web的依赖,点击完成。

image.png

完整项目结构。

image.png

启动项目,如下图,项目搭建成功。

image.png

添加接口

在src目录下找到启动类DemoApplication,在启动类中进行接口的编写,因为是演示,所以就不在创建controller、service、dao等包结构了。

  • 编码前准备工作 需要现在项目中添加lombok依赖。
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
</dependency>
package com.example.demo;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@RestController
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @GetMapping("/log/link")
    public void addUser(@RequestBody User user) {
        log.info("addUser() called with parameters => 【user = {}】", user);
        log.info("save user to db");
        List<User> userList = new ArrayList<>();
        user.setId(1L);
        userList.add(user);
        log.info("save user to db finish {}", user);
    }
}

@Data
class User {
    private Long id;
    private String name;
    private Integer age;
    private String phone;
    private String address;
    private String email;
}

启动项目,后发送请求,我贴出curl,在终端输入或者导入到postman等接口测试工具均可。

curl --location --request GET 'http://127.0.0.1:8080/log/link' \ --header 'User-Agent: apifox/1.0.0 (https://www.apifox.cn)' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "适观全下变", "age": 56, "phone": "18115362212", "address": "浙江省伊春市余杭区", "email": "o.dqyugnedb@qq.com" }'

image.png

查看请求日志。

image.png 没办法把一次请求的所有的日志都关联起来。

添加日志跟踪ID

  • MDC中添加添加个traceId
MDC.put("traceId", UUID.randomUUID().toString());

这个可以放到filter或者interceptor中,这样就不用在每个接口中重复编写了。

  • 在日志三打一的patter中添加traceId。
logging.pattern.console=%X{traceId} %d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) [%yellow(%thread)] %green(%logger:%L)   :%msg%n

需要注意key是一样的(traceId)。

image.png

再次请求接口,查看打印日志。

image.png

这个时候就可以先通过搜索手机号查询到请求日志,在通过请求日志的traceId的值,搜索所有的日志。

日志处理拦截器

将MDC放大拦截器中进行配置,不用在每个接口重复编写。

  • 编写拦截器
package com.example.demo.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    // 在业务处理器处理请求之前被调用
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        MDC.put("traceId", UUID.randomUUID().toString());
        log.info("preHandle() called with parameters => 【request = {}】, 【response = {}】, 【handler = {}】", request, response, handler);
        return true;
    }

    // 在业务处理器处理请求完成之后,生成视图之前执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle() called with parameters => 【request = {}】, 【response = {}】, 【handler = {}】, 【modelAndView = {}】", request, response, handler, modelAndView);
    }

    // 在DispatcherServlet完全处理完请求之后被调用,可用于清理资源
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion() called with parameters => 【request = {}】, 【response = {}】, 【handler = {}】, 【ex = {}】", request, response, handler, ex);
        MDC.clear();
    }
}
  • 配置拦截器

public class DemoApplication implements WebMvcConfigurer {

    ``````

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor());
    }

}
  • 请求接口 输出的日志。

image.png