Spring Boot traceId 用于链路日志追踪(上)

编程教程 > Java > Spring (22) 2025-01-23 21:26:42

摘要

链路追踪在日常业务流程分析、 性能分析、Bug 处理都极为重要。这两天整理下相关的知识点做一个梳理。分为上下两篇。。

目录

  1. 增加 logback 记录日志
  2. 使用 AOP 统一控制输入输出
  3. 使用 MDC 存储 trace id

增加 logback 记录日志

为什么大家都习惯用使用 logback-spring.xml
而不是logback.log
,因为使用前者名字,可以使用一些结合spring的特殊扩展功能。这里,暂时用不到,后面再用。具体参考 https://docs.spring.io/spring-boot/docs/2.6.1/reference/html/features.html#features.logging.logback-extensions

我们在资源目录下新建logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] [%X{TRACE_ID}] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>

</configuration>

最重要的是FILE_LOG_PATTERN日志模板里面设置了[%X{TRACE_ID}]
就可以从MDC里面获取TRACE_ID的值了

使用 AOP 统一控制输入输出

@Component
@Slf4j
@Aspect
public class ControllerHandler {

    private final static String TRACE_ID = "TRACE_ID";

    @Pointcut("execution(public * com.example.demo.controller..*.*(..)) ")
    public void recordLog() {

    }

    @Before("recordLog()")
    public void before(JoinPoint point){
        MDC.put(TRACE_ID, UUID.randomUUID().toString());
    }

    @AfterReturning(pointcut = "recordLog()", returning = "responseData")
    public void after(JoinPoint point, ResponseData responseData){
        responseData.setRequestId(MDC.get(TRACE_ID));
        log.info(JSON.toJSONString(responseData));
    }
}

最重要的就是

MDC.put(TRACE_ID, UUID.randomUUID().toString());

这样就和上面日志模板里面的[%X{TRACE_ID}]对接上了。

增加一个控制器然后查看日志

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    @GetMapping("/{id}")
    public ResponseData<UserDTO> detail(@PathVariable Long id){
        Preconditions.checkNotNull(id, "id is null");
        
        log.info("user id:{}", id);

        UserDTO userDTO = new UserDTO();
        userDTO.setId(id);
        userDTO.setAge(20);
        userDTO.setUsername("加班写Bug");

        return ResponseData.success(userDTO);
    }
}

访问 http://localhost:8080/user/1001 查看日志

2021-12-04 21:57:34.723  INFO 48311 --- [http-nio-8080-exec-1] [aba37c62-5850-4da0-bfaf-89fa9c4282ea] c.e.demo.controller.UserController       : user id:1001
2021-12-04 21:57:34.859  INFO 48311 --- [http-nio-8080-exec-1] [aba37c62-5850-4da0-bfaf-89fa9c4282ea] com.example.demo.aop.ControllerHandler   : {"code":200,"data":{"age":20,"id":1001,"username":"加班写Bug"},"message":"OK","success":true,"traceId":"aba37c62-5850-4da0-bfaf-89fa9c4282ea"}

实际工作中,可能在访问这台 web 应用服务器之前已经有一些链路,比如有统一的网关层,或者被反向代理。trace id 的可能是需要从该应用的上游往下传递,这里以 nginx 反向代理过程中,在 header 里增加了Trace-Id字段为例

@Component
@Slf4j
@Aspect
public class ControllerHandler {
   private final static String TRACE_ID = "TRACE_ID";
   
   @Autowired
   HttpServletRequest request;
   
   @Pointcut("execution(public * com.example.demo.controller..*.*(..)) ")
   public void recordLog() {
   }
   
   @Before("recordLog()")
   public void before(JoinPoint point){
       if (request != null && request.getHeader("Trace-Id") != null) {
           MDC.put(TRACE_ID, request.getHeader("Trace-Id"));
       }else{
           MDC.put(TRACE_ID, UUID.randomUUID().toString());
       }
   }
   
   @AfterReturning(pointcut = "recordLog()", returning = "responseData")
   public void after(JoinPoint point, ResponseData responseData){
       responseData.setTraceId(MDC.get(TRACE_ID));
       log.info(JSON.toJSONString(responseData));
   }
}

使用 curl 校验下是否生效

curl -H 'Trace-Id:123456' http://localhost:8080/user/1001
{"success":true,"code":200,"message":"OK","traceId":"123456","data":{"id":1001,"username":"加班写Bug","age":20}}

这里是举例说明,trace id 可以有更多的算法来校验其是否合法。

打印更完整的链路日志

打印 trace id 不是目标,能够通过 trace id 精准的定位问题、排查问题才是目的,所以最好是 input 和 output 都打印下。这里使用Around
代替了刚刚的 before 和 after ,并且对控制器的异常直接在这里做了处理。

@Component
@Slf4j
@Aspect
public class ControllerHandler {

    private final static String TRACE_ID = "TRACE_ID";

    @Autowired
    HttpServletRequest request;

    @Pointcut("execution(public * com.example.demo.controller..*.*(..)) ")
    public void recordLog() {

    }

    public void setTraceId(){
        if (request != null && request.getHeader("Trace-Id") != null) {
            MDC.put(TRACE_ID, request.getHeader("Trace-Id"));
        }else{
            MDC.put(TRACE_ID, UUID.randomUUID().toString());
        }
    }

    @Around("recordLog()")
    public Object record(ProceedingJoinPoint joinPoint) throws Throwable {

        setTraceId();

        String classType = joinPoint.getTarget().getClass().getName();
        Class<?> clazz = Class.forName(classType);
        String clazzSimpleName = clazz.getSimpleName();
        String methodName = joinPoint.getSignature().getName();
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        String[] parameterNames = methodSignature.getParameterNames();
        Object[] parameters = joinPoint.getArgs();
        Map<String, Object> parameterMap = new LinkedHashMap<>();
        for (int i = 0; i < parameters.length; i++) {
            String parameterName = parameterNames[i];
            Object parameter = parameters[i];
            parameterMap.put(parameterName, parameter);
        }

        String parametersJsonString = JSON.toJSONString(parameterMap, SerializerFeature.WriteMapNullValue);
        log.info("{}#{} args:{}", clazzSimpleName, methodName, parametersJsonString);

        Object response;

        try {
            response = joinPoint.proceed(joinPoint.getArgs());
        } catch (Exception e) {
            log.error("{}#{}, exception:{}:", clazzSimpleName, methodName, e.getClass().getSimpleName(), e);

            ResponseData<Object> res = ResponseData.failure(ResponseCode.INTERNAL_ERROR);

            if (e instanceof IllegalArgumentException || e instanceof NullPointerException) {
                res.setCode(ResponseCode.BAD_REQUEST.getCode());
                res.setMessage(e.getMessage());
            }else if (e instanceof DemoRuntimeException) {
                res.setMessage(e.getMessage());
            }
            response = res;
        }

        if (response instanceof ResponseData) {
            ((ResponseData) response).setTraceId(MDC.get(TRACE_ID));
        }

        String resultJsonString = JSON.toJSONString(response, SerializerFeature.WriteMapNullValue,
                SerializerFeature.DisableCircularReferenceDetect);
        log.info("{}#{} response:{}", clazzSimpleName, methodName, resultJsonString);

        return response;
    }
    
}

99% 的应用都不用考虑这点 I/O 性能的影响,真建议打上。再次访问 http://localhost:8080/user/1001 查看日志

2021-12-04 23:24:38.122  INFO 90186 --- [http-nio-8080-exec-1] [e334594c-5ae2-48fa-91dc-10528004f6d2] com.example.demo.aop.ControllerHandler   : UserController#detail args:{"id":1001}
2021-12-04 23:24:38.141  INFO 90186 --- [http-nio-8080-exec-1] [e334594c-5ae2-48fa-91dc-10528004f6d2] c.e.demo.controller.UserController       : user id:1001
2021-12-04 23:24:38.209  INFO 90186 --- [http-nio-8080-exec-1] [e334594c-5ae2-48fa-91dc-10528004f6d2] com.example.demo.aop.ControllerHandler   : UserController#detail response:{"code":200,"data":{"age":20,"id":1001,"username":"加班写Bug"},"message":"OK","success":true,"traceId":"e334594c-5ae2-48fa-91dc-10528004f6d2"}

这样控制器里的log.info("user id:{}", id)就可以删掉了。

但是这还不够,因为在实际开发过程中,经常存在异步调用的情况,而 MDC 是基于 ThreadLocal 不能跨线程传递,下篇说异步的处理方式。

 


评论
User Image
提示:请评论与当前内容相关的回复,广告、推广或无关内容将被删除。

相关文章
traceId链路追踪在日常业务流程分析、 性能分析、Bug 处理都极为重要。这两天整理下相关的知识点做一个梳理。分为上下两篇。。目录增加 logback 记录日志使用
Spring Boot 通过Filter添加logback traceId@Slf4j@Componentpublic class TraceIdFilter
摘要上节解决了单一线程的 trace id 传递,如果子线程和线程池怎么办呢,还有 rpc 远程调用,怎么玩呢?我们在做项目中肯定有很多时候希望通过异步的方式来
Spring Boot logback tomcat部署不生效问题解决
Spring Boot 2.0 入门 logoback配置实战教程,俗话说好马配好鞍。Spring Boot 框架从各方面给我们带来了开发效率。日志自然也不会落下。本文将讲解与Spring Bo...
spring boot项目中使用logback日志,USING LOGBACK WITH SPRING BOOT ,Logback让一个优秀的企业应用程序的日志框架——它是快速,简单但强大的配置...
springmvc集成logback不打印spring mvc启动日志解决
spring boot mybatis 整合使用讲解介绍,spring boot与MyBatis的使用讲解介绍。spring boot mybatis xml mapper方式的入门和通过一个简...
Java编程之Spring Boot 使用SLF4J Logging,spring boot,SLF4J
Spring Boot Logback 输出日志慢8小时解决,因为默认时区问题导致spring boot的logback日志输出慢了8个小时,下面讲解如何解决这个问题。
Spring data-mongodb ID自增长注解实现,mongodb ID自增长
spring boot是一个崭新的spring框架分支项目,本文讲解基本的数据库配置
spring boot 1.5整合redis实现spring的缓存框架,spring boot,redis
Spring Boot 2.1 新特性,已升级Spring 版本为5.1,支持servlet 4.0,支持Tomcat 9.0等等