SpringBoot 如何在日志中增加 trace id 用于链路追踪

本文涉及的产品
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
日志服务 SLS,月写入数据量 50GB 1个月
简介: SpringBoot 如何在日志中增加 trace id 用于链路追踪

目录

  1. 增加 logback 记录日志
  2. 使用 AOP 统一控制输入输出
  3. 使用 MDC 存储 trace id
项目完整代码见: https://gitee.com/zhoumengkang/wechat-demo/tree/master/strace01

增加 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)就可以删掉了。

项目完整代码见: https://gitee.com/zhoumengkang/wechat-demo/tree/master/strace01
相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
目录
相关文章
|
8天前
|
缓存 Java 应用服务中间件
Spring Boot配置优化:Tomcat+数据库+缓存+日志,全场景教程
本文详解Spring Boot十大核心配置优化技巧,涵盖Tomcat连接池、数据库连接池、Jackson时区、日志管理、缓存策略、异步线程池等关键配置,结合代码示例与通俗解释,助你轻松掌握高并发场景下的性能调优方法,适用于实际项目落地。
141 4
|
6月前
|
存储 Java 文件存储
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录—— logback.xml 配置文件解析
本文解析了 `logback.xml` 配置文件的详细内容,包括日志输出格式、存储路径、控制台输出及日志级别等关键配置。通过定义 `LOG_PATTERN` 和 `FILE_PATH`,设置日志格式与存储路径;利用 `&lt;appender&gt;` 节点配置控制台和文件输出,支持日志滚动策略(如文件大小限制和保存时长);最后通过 `&lt;logger&gt;` 和 `&lt;root&gt;` 定义日志级别与输出方式。此配置适用于精细化管理日志输出,满足不同场景需求。
1575 1
|
2月前
|
机器学习/深度学习 XML Java
【spring boot logback】日志logback格式解析
在 Spring Boot 中,Logback 是默认的日志框架,它支持灵活的日志格式配置。通过配置 logback.xml 文件,可以定义日志的输出格式、日志级别、日志文件路径等。
483 5
|
6月前
|
Java 微服务 Spring
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录——使用Logger在项目中打印日志
本文介绍了如何在项目中使用Logger打印日志。通过SLF4J和Logback,可设置不同日志级别(如DEBUG、INFO、WARN、ERROR)并支持占位符输出动态信息。示例代码展示了日志在控制器中的应用,说明了日志配置对问题排查的重要性。附课程源码下载链接供实践参考。
731 0
|
6月前
|
SQL Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录—— application.yml 中对日志的配置
在 Spring Boot 项目中,`application.yml` 文件用于配置日志。通过 `logging.config` 指定日志配置文件(如 `logback.xml`),实现日志详细设置。`logging.level` 可定义包的日志输出级别,例如将 `com.itcodai.course03.dao` 包设为 `trace` 级别,便于开发时查看 SQL 操作。日志级别从高到低为 ERROR、WARN、INFO、DEBUG,生产环境建议调整为较高级别以减少日志量。本课程采用 yml 格式,因其层次清晰,但需注意格式要求。
619 0
|
6月前
|
Java API 开发者
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录——slf4j 介绍
在软件开发中,`System.out.println()`常被用于打印信息,但大量使用会增加资源消耗。实际项目推荐使用slf4j结合logback输出日志,效率更高。Slf4j(Simple Logging Facade for Java)是一个日志门面,允许开发者通过统一方式记录日志,无需关心具体日志系统。它支持灵活切换日志实现(如log4j或logback),且具备简洁占位符和日志级别判断等优势。阿里巴巴《Java开发手册》强制要求使用slf4j,以保证日志处理方式的统一性和维护性。使用时只需通过`LoggerFactory`创建日志实例即可。
452 0
|
10月前
|
Java 中间件
SpringBoot入门(6)- 添加Logback日志
SpringBoot入门(6)- 添加Logback日志
321 5
|
8月前
|
开发框架 运维 监控
Spring Boot中的日志框架选择
在Spring Boot开发中,日志管理至关重要。常见的日志框架有Logback、Log4j2、Java Util Logging和Slf4j。选择合适的日志框架需考虑性能、灵活性、社区支持及集成配置。本文以Logback为例,演示了如何记录不同级别的日志消息,并强调合理配置日志框架对提升系统可靠性和开发效率的重要性。
270 5
|
8月前
|
存储 安全 Java
Spring Boot 3 集成Spring AOP实现系统日志记录
本文介绍了如何在Spring Boot 3中集成Spring AOP实现系统日志记录功能。通过定义`SysLog`注解和配置相应的AOP切面,可以在方法执行前后自动记录日志信息,包括操作的开始时间、结束时间、请求参数、返回结果、异常信息等,并将这些信息保存到数据库中。此外,还使用了`ThreadLocal`变量来存储每个线程独立的日志数据,确保线程安全。文中还展示了项目实战中的部分代码片段,以及基于Spring Boot 3 + Vue 3构建的快速开发框架的简介与内置功能列表。此框架结合了当前主流技术栈,提供了用户管理、权限控制、接口文档自动生成等多项实用特性。
550 8
|
10月前
|
Java 中间件
SpringBoot入门(6)- 添加Logback日志
SpringBoot入门(6)- 添加Logback日志
248 1