SpringCloud中Feign透传traceId及日志切面配置

紧接上一篇,我们虽然成功地将springboot服务接入到了plumeLog日志系统,但是仍遗留了几个问题待解决:

1、对于用户的一次http调用,我们需要在整个调用链中(涉及多个微服务)保持traceId是同一个

2、虽然日志接入成功了,但是请求接口的路径、方法、参数及返回结果没有记录

3、控制台、本地文件日志中也要输出traceId的值

一、配置feign调用透传traceId(使用自定义Http头)

对于第一个问题,笔者的解决思路是配置feign拦截器,在feign客户端将PlumeLog上下文中的traceId手动塞入到 traceId Http请求头中 

public class TraceConst {

    public static final String TRACE_ID = "traceId";
}
import com.plumelog.core.TraceId;
import com.tingcream.tmccloud.baseweb.tracelog.util.TraceConst;
import feign.RequestInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class TraceFeignConfig {

    @Bean
    public RequestInterceptor traceIdRequestInterceptor() {
        return template -> {
            // 优先从Plumelog上下文获取,降级从MDC获取
            String traceId = TraceId.logTraceID.get();
            if (traceId == null || traceId.isEmpty()) {
                traceId = MDC.get(TraceConst.TRACE_ID);
            }
            //  拿到了traceid ,放到http请求头中,带入到下游feign服务调用
            if (traceId != null && !traceId.isEmpty()) {
                log.info("feign调用透传http请求头traceId:{}",traceId);
                template.header(TraceConst.TRACE_ID, traceId);
            }
        };
    }
}

二、使用自定义AOP切面记录详细请求参数日志

对于第二个问题,我们需要配置一个AOP切面切入所有的controller层接口即可

import com.alibaba.fastjson2.JSON;
import com.plumelog.core.TraceId;
import com.tingcream.tmccloud.baseweb.tracelog.annotation.IgnoreTraceLog;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * traceLog日志切面 (plume日志)
 */
@Slf4j
@Aspect
@Component
public class TraceLogAspect {


    /**
     * 参数最大长度限制(防止大对象撑爆日志) 2M
     */
    private static final int MAX_PARAM_LENGTH = 2048000;

//    /**
//     * 需要脱敏的字段名
//     */
//    private static final String[] SENSITIVE_FIELDS = {"password", "token", "secret", "authorization"};

    @Pointcut("execution(* com.tingcream..controller..*.*(..))")
    public void webLog() {}

    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {

        // 检查是否需要忽略日志记录
        if (shouldIgnore(joinPoint)) {
            // 直接执行方法,不记录日志
            return joinPoint.proceed();
        }

        long startTime = System.currentTimeMillis();
        String traceId = TraceId.logTraceID.get();

        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes != null ? attributes.getRequest() : null;

        // 开始日志
        log.info("=== 请求开始 === traceId: {}", traceId);
        if (request != null) {
            log.info("请求URL:{},请求方法:{},请求IP:{}", request.getRequestURL(), request.getMethod(), request.getRemoteAddr());
        }

        // 记录请求参数(过滤掉文件流、响应对象等)
        Object[] args = joinPoint.getArgs();
        Object[] loggableArgs = Arrays.stream(args)
                .filter(arg -> !(arg instanceof MultipartFile))
                .filter(arg -> !(arg instanceof HttpServletRequest))
                .filter(arg -> !(arg instanceof HttpServletResponse))
                .toArray();

        if (loggableArgs.length > 0) {
            String argsJson = JSON.toJSONString(loggableArgs);
            // 限制长度
            if (argsJson.length() > MAX_PARAM_LENGTH) {
                argsJson = argsJson.substring(0, MAX_PARAM_LENGTH) + "... (truncated)";
            }
            log.info("请求参数: {}", argsJson);
        } else {
            log.info("请求参数: 无参数");
        }

        // 执行目标方法
        Object result = joinPoint.proceed();

        // 记录返回值和耗时
        long elapsedTime = System.currentTimeMillis() - startTime;
        String resultJson = JSON.toJSONString(result);

        // 限制返回值长度
        if (resultJson.length() > MAX_PARAM_LENGTH) {
            resultJson = resultJson.substring(0, MAX_PARAM_LENGTH) + "... (truncated)";
        }

        log.info("返回结果: {}", resultJson);
        log.info("耗时: {} ms", elapsedTime);
        log.info("=== 请求结束 === traceId: {}", traceId);

        return result;
    }

    /**
     * 判断是否需要忽略日志记录
     */
    private boolean shouldIgnore(ProceedingJoinPoint joinPoint) {
        // 1. 获取目标类
        Class<?> targetClass = joinPoint.getTarget().getClass();

        // 2. 获取目标方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        // 3. 检查方法上是否有 @IgnoreTraceLog 注解
        if (method.isAnnotationPresent(IgnoreTraceLog.class)) {
            return true;
        }

        // 4. 检查类上是否有 @IgnoreTraceLog 注解
        if (targetClass.isAnnotationPresent(IgnoreTraceLog.class)) {
            return true;
        }
        return false;
    }

}
import java.lang.annotation.*;

/**
 * 忽略日志切面切入
 */
@Target({ElementType.METHOD, ElementType.TYPE})  // 可作用于方法或类
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IgnoreTraceLog {

    String value() default "";

}

三、配置logback、slf4j 的控制台及文件日志输出traceId

我们在上一篇的 MyTraceIdFilter中加入了

MDC.put(TRACE_ID, traceId);

这样logback、slf4j等门面日志框架也能从上下文中拿到traceId了,因此我们只需在log.pattern 加上%X{traceId}  即可在控制台、本地日志文件中展示追踪码了。

  <property name="log.pattern"
            value="[追踪码:%X{traceId}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"></property>


  <!-- 控制台 appender-->
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>${log.pattern}</pattern>
      <charset>UTF-8</charset>
    </encoder>
  </appender>



  <!-- 文件 滚动日志 (all)-->
  <appender name="allLog"  class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!-- 当前日志输出路径、文件名 -->
    <file>${log.path}/all.log</file>
    <!--日志输出格式-->
    <encoder>
      <pattern>${log.pattern}</pattern>
      <charset>UTF-8</charset>
    </encoder>
    <!--历史日志归档策略-->
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
      <!-- 历史日志: 归档文件名 -->
      <fileNamePattern>${log.path}/%d{yyyy-MM, aux}/all.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
      <!--单个文件的最大大小-->
      <maxFileSize>64MB</maxFileSize>
      <!--日志文件保留天数-->
      <maxHistory>40</maxHistory>
    </rollingPolicy>

  </appender>

 最后搞好后,我们再查看下plumeLog日志,会得到这样的效果:

请求接口URL、参数、返回结果,以及请求接口耗时 查看都一目了然!!

跨多服务的调用链也能看的非常清楚 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jasnet_u

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值