Spring Boot 中使用 MDC 追踪一次请求全过程(日志链路)

Spring Boot 中使用 MDC 追踪一次请求全过程(日志链路)

ControllerLogAspect

package com.yymt.common.trace;

import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * @description:
 * @author: xl
 * @version: 1.0.0
 * @date: 2024-05-24 14:22:02
 */

@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
@Aspect
@Slf4j
public class ControllerLogAspect {

    // 参数类型是下面类型的也不会打印,可以扩展
    private static List<Class<?>> notLogTypes = Arrays.asList(ServletRequest.class, ServletResponse.class);


    /**
     * 拦截所有controller方法
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("execution(* com.yymt.controller..*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        long l = System.currentTimeMillis();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Object result = null;

        try {
            // 打印处理当前请求的完整类名和方法
            log.info("接口方法,{},{}", methodSignature.getDeclaringTypeName(), methodSignature.getName());

            // 获取所有要打印的参数,丢到map中,key为参数名称,value为参数的值,然后会将这个mp以json的格式输出
            Map<String, Object> logParamsMap = new LinkedHashMap<>();
            String[] parameterNames = methodSignature.getParameterNames();
            Object[] args = joinPoint.getArgs();
            for (int i = 0; i < args.length; i++) {
                if (parameterIsLog(methodSignature, i)) {
                    // 参数名称
                    String parameterName = parameterNames[i];
                    // 参数值
                    Object parameterValue = args[i];
                    logParamsMap.put(parameterName, parameterValue);
                }

            }
            log.info("方法参数列表:{}", JSONUtil.toJsonStr(logParamsMap));

            result = joinPoint.proceed();
            return result;
        } finally {
            if (this.resultIsLog(methodSignature)) {
                log.info("方法返回值:{}", JSONUtil.toJsonStr(result));
            }
        }
    }

    /**
     * 判断参数是否需要打印
     *
     * @param methodSignature
     * @param paramIndex
     * @return
     */
    private boolean parameterIsLog(MethodSignature methodSignature, int paramIndex) {
        if (methodSignature.getMethod().getParameterCount() == 0) {
            return false;
        }

        // 参数上有 @NoLog注解的不会打印
        Annotation[] paramAnnotation = methodSignature.getMethod().getParameterAnnotations()[paramIndex];
        if (paramAnnotation != null && paramAnnotation.length > 0) {
            for (Annotation annotation : paramAnnotation) {
                if (annotation.annotationType() == NoLog.class) {
                    return false;
                }
            }
        }

        // 参数是下面类型的也不会打印
        Class parameterType = methodSignature.getParameterTypes()[paramIndex];
        for (Class<?> type : notLogTypes) {
            if (type.isAssignableFrom(parameterType)) {
                return false;
            }
        }

        return true;
    }

    /**
     * 判断方法的返回值是否需要打印? 方法上有@NoLog 注解的,表示结果不打印返回值
     *
     * @param methodSignature
     * @return
     */
    private boolean resultIsLog(MethodSignature methodSignature) {
        return methodSignature.getMethod().getAnnotation(NoLog.class) == null;
    }

}

ResultTraceIdAspect

package com.yymt.common.trace;

import com.yymt.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * @description: 在返回值中填充traceId,用于方便排查错误
 * @author: xl
 * @version: 1.0.0
 * @date: 2024-05-24 14:22:02
 */

@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
@Aspect
@Slf4j
public class ResultTraceIdAspect {

    @Pointcut("execution(* com.yymt.controller..*(..)) || execution(* com.yymt.common.exception.RRExceptionHandler.*(..))")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object object = joinPoint.proceed();
        if (object instanceof R) {
            ((R) object).put("traceId", TraceUtils.getTraceId());
        }
        return object;
    }

}

TraceFilter

package com.yymt.common.trace;

import cn.hutool.core.util.IdUtil;
import com.yymt.common.trace.TraceUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @description:
 * @author: xl
 * @version: 1.0.0
 * @date: 2024-05-24 15:01:46
 */
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@WebFilter(urlPatterns = "/**", filterName = "TraceFilter")
@Slf4j
public class TraceFilter extends OncePerRequestFilter {



    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String traceID = IdUtil.fastSimpleUUID();
        TraceUtils.setTraceId(traceID);

        log.info("请求start:{}", request.getRequestURL().toString());
        long st = System.currentTimeMillis();
        try {
            filterChain.doFilter(request, response);
        } finally {
            long et = System.currentTimeMillis();
            log.info("请求end:{},耗时(ms):{}", request.getRequestURL().toString(), (et - st));
            TraceUtils.removeTraceId();
        }
    }

}

TraceUtils

package com.yymt.common.trace;


import org.slf4j.MDC;

/**
 * @description:
 * @author: xl
 * @version: 1.0.0
 * @date: 2024-05-24 15:33:04
 */
public class TraceUtils {

    public static final String TRACE_ID = "traceId";
    public static ThreadLocal<String> traceThreadLocal = new ThreadLocal<>();

    public static String getTraceId() {
        return traceThreadLocal.get();
    }

    public static void setTraceId(String traceId) {
        traceThreadLocal.set(traceId);
        MDC.put(TRACE_ID, traceId);
    }

    public static void removeTraceId() {
        traceThreadLocal.remove();
        MDC.remove(TRACE_ID);
    }

}

NoLog

package com.yymt.common.trace;

import java.lang.annotation.*;

/**
 * 不打印日志注解
 *
 */
@Target({ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoLog {

}

logback-spring.xml

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

    <logger name="org.springframework.web" level="INFO"/>
    <logger name="org.springboot.sample" level="TRACE"/>

    <springProperty scop="context" name="appName" source="spring.application.name" defaultValue="bbt"/>
    <springProperty scop="context" name="rootLevel" source="bbt.logger.level" defaultValue="INFO"/>
    <springProperty scop="context" name="log.path" source="logging.path" defaultValue=""/>
    <springProperty scop="context" name="spring.profiles.active" source="spring.profiles.active" defaultValue="local"/>

    <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} ${LOG_LEVEL_PATTERN:-%5p} [%thread][%X{traceId}]%logger{50} %3.3L - %msg%n"/>
    <property name="CONSOLE_LOG_PATTERN" value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){magenta} %highlight(%-5level) %clr([%thread][%X{traceId}]){faint} %clr(%logger{50}){cyan} %clr(%3.3L) %clr(-){faint} %msg%n"/>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 引用自定义输出模板 -->
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--记录的日志文件的路径及文件名-->
        <file>${log.path}/debug.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 滚动日志文件保存格式 -->
            <fileNamePattern>${log.path}/%d{yyyy-MM,aux}/debug.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <MaxFileSize>50MB</MaxFileSize>
            <totalSizeCap>10GB</totalSizeCap>
            <MaxHistory>180</MaxHistory>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
        </filter>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--记录的日志文件的路径及文件名-->
        <file>${log.path}/error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 滚动日志文件保存格式 -->
            <fileNamePattern>${log.path}/%d{yyyy-MM,aux}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <MaxFileSize>50MB</MaxFileSize>
            <totalSizeCap>10GB</totalSizeCap>
            <MaxHistory>180</MaxHistory>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <appender name="FILE-TOTAL" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--记录的日志文件的路径及文件名-->
        <file>${log.path}/spring.log</file>

        <!--记录的日志级别-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>ACCEPT</onMismatch>
        </filter>

        <!--日志文件输出格式-->
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>

        <!--日志记录器的滚动策略,按日期,按大小记录-->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/%d{yyyy-MM,aux}/spring.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <MaxFileSize>50MB</MaxFileSize>
            <totalSizeCap>10GB</totalSizeCap>
            <MaxHistory>180</MaxHistory>
        </rollingPolicy>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
<!--        <appender-ref ref="STDOUT"/>-->
        <appender-ref ref="DEBUG_FILE"/>
        <appender-ref ref="ERROR_FILE"/>
        <appender-ref ref="FILE-TOTAL"/>
    </root>

    <!-- 开发、测试环境 -->
    <springProfile name="dev,test,local,docker,report-test,test-gc">
        <logger name="org.springframework.web" level="INFO"/>
        <logger name="org.springboot.sample" level="INFO" />
        <logger name="com.yymt" level="debug" />
    </springProfile>

    <!-- 生产环境 : prod-rj表示蓉江新区-->
    <springProfile name="prod,report-prod,prod-fu,prod-rj,prod-jjpcs,prod-hwsq">
        <logger name="org.springframework.web" level="INFO"/>
        <logger name="org.springboot.sample" level="INFO" />
        <logger name="com.yymt" level="INFO"/>
    </springProfile>

</configuration>

多线程使用

注意如果是开启子线程需要自己设置traceId进去,日志才会打印traceId
String traceId = TraceUtils.getTraceId();
        CompletableFuture.runAsync(() -> {

            // MDC.put(TRACE_ID, traceId);
            TraceUtils.setTraceId(traceId);
            try {
                log.info("runAsync");
            } finally {
                // MDC.clear();
                TraceUtils.removeTraceId();
            }

        }, executor);



参考: Spring Boot 中使用 MDC 追踪一次请求全过程

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值