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

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

相关推荐
没钥匙的锁11 小时前
16-Java反射机制:Spring IOC背后的核心技术
java·开发语言·spring
geinvse_seg2 小时前
飞算JavaAI:把企业采购协同需求拆成一套可运行的慧采平台
java·ai·飞算
烛影摇红透纱窗3 小时前
ForkJoinPool 与工作窃取
java
浩瀚地学3 小时前
【面试算法笔记】0302-哈希表-哈希表实现
java·经验分享·笔记·算法·面试
AC赳赳老秦3 小时前
时间开销自动统计:OpenClaw 记录工作任务时长、分析时间分配、给出优化建议
java·大数据·开发语言·python·自动化·deepseek·openclaw
名侦探7224 小时前
构建 Agent 的五大难点与解决方案
java·大数据·人工智能
Demons_kirit4 小时前
图库原理(以阿里云OSS为案例)
java·阿里云·云计算
SimonKing4 小时前
那个号称"对标 Spring"的国产框架 Solon,到底行不行?
java·后端·程序员
Lethehong4 小时前
飞算Java:从需求梳理到风险闭环,搭建云盾企业信息安全与权限管理平台
java·开发语言
霸道流氓气质4 小时前
SpringBoot中使用字典驱动的动态路由示例
java·spring boot·后端