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

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

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

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

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

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

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

java 复制代码
public class TraceConst {

    public static final String TRACE_ID = "traceId";
}
java 复制代码
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层接口即可

java 复制代码
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;
    }

}
java 复制代码
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} 即可在控制台、本地日志文件中展示追踪码了。

XML 复制代码
  <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、参数、返回结果,以及请求接口耗时 查看都一目了然!!

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

相关推荐
JAVA面经实录9171 小时前
SpringBoot 全套完整版学习文档(零基础+实战+面试+源码)
java·spring boot·spring·架构
接着奏乐接着舞1 小时前
springcloud xxl-job
后端·spring·spring cloud
nvd111 小时前
从 Spring 到 Quarkus:为什么依赖注入正在从“运行时”退回“编译期”?
java·后端·spring
JAVA面经实录9171 小时前
SpringCloud 完整体系学习文档
java·spring cloud
爱吃羊的老虎1 小时前
【JAVA】Java微服务—Spring Cloud 里用来做服务调用的工具OpenFeign
java·微服务·开源
开源推荐官1 小时前
2026 年主流优质 B2B2C 多商户商城系统推荐
java·架构·开源
真实的菜1 小时前
Java 微服务优雅停机:从踩坑到最佳实践
java·微服务·linq
码不停蹄的玄黓1 小时前
Arthas 核心使用场景
java
1104.北光c°1 小时前
深度剖析 Spring 灵魂:IOC 容器与自动装配的原理、设计与实现
java·开发语言·笔记·后端·spring·rpc·ioc