当前位置: 首页 > news >正文

基于注解脱敏+链路追踪traceId 快速定位错误

日常开发中,你如何快速定位问题的?

a. 排除条件

特别是处理并发请求时,通过对每个方法生成traceId(自定义唯一标识符)用于快速定位目标日志,追踪整个系统中的请求流程。

b. 日志脱敏增强

返回值带着敏感信息,要么不返回,要么加密

请求参数带着敏感信息,要么不记录,要么加密

c. 解决方案

服务端入口处可以生成唯一的id,叫做:traceId

日志中均需要输出traceId的值

接口返回值中,添加一个通用的字段: traceld,将上面的traceld作为这个字段的值

这样前端发现接口有问题的时候,直接将这个traceld提供给我们,我们便可以在日志中快速查询出对应的日志。

MDC组件

使用Slf4j.MDC日志功能增强

org.slf4j.MDC是 SLF4J库中的一个组件,MDC提供了一种机制,允许在日志消息中插入上下文信息,这些信息可以跨多个方法调用和线程边界传播。这对于跟踪和调试分布式系统或多线程应用程序中的请求非常有用。

重点:**MDC 允许将键值对与当前线程关联起来(类似ThreadLocal)**然后,可以在我们的日志语句中引用这些值,从而能够更容易地识别和理解日志消息产生的上下文。

例如,你可能会在 Web 应用程序的每个请求开始时,将用户的 ID 或会话 ID 放入 MDC,然后在你的日志语句中引用这个值。这样,当你查看日志时,你可以很容易地看到哪个用户的哪个请求产生了哪些日志消息。

使用方式

Logback学习系列4(Pattern使用方法)https://www.jianshu.com/p/5cfc26caf50d

  • 设置值: MDC.put("userId", "12345");
  • 在日志语句中使用值: logger.info("Processing request for user: {}", MDC.get("userId"));
  • 清除值 MDC.remove("userId")
例子

假设我们正在开发一个电商网站,该网站由多个微服务组成,包括用户服务、订单服务、支付服务等。每个服务都运行在独立的服务器上,并通过REST API相互调用。为了方便调试和监控,我们需要在整个请求处理过程中跟踪每一个请求,并确保所有相关的日志条目都能关联起来。

  1. 当请求到达第一个服务(例如用户服务)时,TraceFilter生成一个唯一的traceId并将其放入MDC中。

  2. 当用户服务需要调用订单服务时,将当前线程中的traceId作为HTTP请求头的一部分传递给下游服务(订单服务)。在订单服务接收到请求时,从请求头中读取traceId并再次放入MDC中。

    // 客户端拦截器:将TraceId注入HTTP头
    public class OpenFeignRequestInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate requestTemplate) {String traceId = MDC.get("traceId"); // 从当前线程上下文中获取TraceIdrequestTemplate.header("X-Trace-ID", traceId); // 注入到HTTP头}
    }// 服务端处理:从HTTP头提取TraceId
    public class TraceFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {String traceId = request.getParameter("X-Trace-ID"); // 从HTTP头获取TraceIdMDC.put("traceId", traceId); // 写入当前线程上下文chain.doFilter(request, response);}
    }
    
  3. 在每个服务的日志配置文件中,添加traceId到日志格式中,以便每条日志都包含这个信息。开发人员只需搜索traceId: xyz789即可快速定位问题根源。

    [订单服务] [traceId: xyz789] - 开始创建订单
    [库存服务] [traceId: xyz789] - 扣减库存成功
    [支付服务] [traceId: xyz789] - 支付失败,数据库连接超时
    
  4. 现在,无论请求跨越了多少个服务,只要查看日志,就可以通过traceId轻松地将所有相关的日志条目关联起来,从而快速定位问题或理解系统的运行情况。

核心代码
自定义注解

脱敏注解@NoLogAnnotation 相关方法加上注解后隐藏参数信息,脱敏前 密码没加密时会暴露用户密码等敏感信息

@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoLogAnnotation {//可以用在参数列表或者方法上,屏蔽不愿意记录的参数
}

日志配置logback.xml展示TraceId

<?xml version="1.0" encoding="UTF-8"?>
<configuration><appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"><encoder><!-- 日志输出格式 将MDC中的traceId自动嵌入每条日志(唯一)--><pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [traceId:%X{traceId}] - %msg%n</pattern></encoder></appender><logger name="com.zr.study" level="info" /><root level="info"><appender-ref ref="STDOUT" /></root>
</configuration>
链路追踪组件

TraceConfiguration,配置类注入过滤器和切面类进ioc容器

package com.zr.study.trace;
import com.zr.study.aop.ResultTraceIdAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** proxyBeanMethods属性* 指定@Bean注解标注的方法是否使用代理,*  默认是true使用代理,直接从IOC容器之中取得对象;*  false:每次调用@Bean标注的方法获取到的对象和IOC容器中的都不一样,是一个新的对象,以此提高性能*/
@Configuration(proxyBeanMethods = false)
public class TraceConfiguration {//注册过滤器与切面@Beanpublic TraceFilter traceFilter() {return new TraceFilter();}@Beanpublic ResultTraceIdAspect fillRequestIdAspect() {return new ResultTraceIdAspect();}
}
链路工具类

TraceUtils工具类封装了ThreadLocal存储获取删除traceId和MDC增删traceId的方法

  • 存到ThreadLocal:每个请求都运行在其自己的线程上,通过将traceId绑定到线程本地存储(ThreadLocal)
    • 可以确保每个线程都有其独立的traceId,避免了多线程环境下数据混淆问题。
    • 在请求任意过程中都可以获取traceId
  • 存到MDC中:可以在日志条目中自动包含这个traceId(底层也是基于ThreadLocalMap实现)
import org.slf4j.MDC;public class TraceUtils {//结合线程本地存储和日志上下文,实现 traceId 的传递与日志关联public static final String TRACE_ID = "traceId";public static ThreadLocal<String> traceIdThreadLocal = new ThreadLocal<>();//设置 traceId 到当前线程的上下文中public static void setTraceId(String traceId) {traceIdThreadLocal.set(traceId);MDC.put(TRACE_ID, traceId); // 同步到 MDC}public static String getTraceId() {return traceIdThreadLocal.get();}public static void removeTraceId() {traceIdThreadLocal.remove();MDC.remove(TRACE_ID);}
}
链路过滤器

TraceFilter解耦业务,通过过滤器生成TraceId+插入TreadLocal和MDC中,并通过时间戳记录方法执行时间打印到控制台

import cn.hutool.core.util.IdUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;//确保该过滤器是第一个执行的,优先设置 traceId
@Order(Ordered.HIGHEST_PRECEDENCE)
//Web 过滤器,匹配所有请求路径
@WebFilter(urlPatterns = "/**", filterName = "TraceFilter")
public class TraceFilter extends OncePerRequestFilter {private Logger log = LoggerFactory.getLogger(this.getClass());@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {//尝试 从HTTP头获取TraceIdString traceID =request.getParameter("X-Trace-ID"); if (traceID == null) {traceID = IdUtil.fastSimpleUUID(); //生成唯一traceId}TraceUtils.setTraceId(traceID); //同步到线程上下文和MDC日志中long startTime = System.currentTimeMillis();try {filterChain.doFilter(request, response);} finally {//请求后续处理完成或异常,都会返回这里清理traceIdTraceUtils.removeTraceId();}long endTime = System.currentTimeMillis();  //记录请求耗时log.info("请求地址:{},耗时(毫秒):{}", request.getRequestURL().toString(), (endTime - startTime));}
}
切面类

ResultTraceIdAspect:环绕通知切点为Controller和全局异常处理器,目的是无论方法是否异常,给统一方法返回类插入traceId

🔁 执行流程如下:

  1. 进入切面
  2. 调用 proceed() 方法 → 实际上就是调用目标方法(Controller 或 ExceptionHandler 的方法)
  3. 获取返回值 result
  4. 判断是否是 ResultData 类型(自定义统一返回包装类)
  5. 如果是,则设置 traceId
  6. 返回增强后的结果
@Order
@Aspect
@Component
public class ResultTraceIdAspect {//拦截所有 Controller 方法和全局异常处理器方法//对所有控制器层和异常处理器的返回结果进行统一增强处理@Pointcut("execution(* com.zr..*Controller.*(..)) || execution(* com.zr.study.exp.GlobalExceptionHandler.*(..))")public void pointCut() {}@Around("pointCut()")  //环绕通知,如针对controller方法的前后进行增强public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {Object result = null;result = proceedingJoinPoint.proceed(); //放行if (result instanceof ResultData) {((ResultData<?>) result).setTraceId(TraceUtils.getTraceId());}return result;}
}

ResultData是统一返回类,这块相比于之前的,多了traceId链路追踪ID字段给前端

@Data
@Accessors(chain = true)
public class ResultData<T> {private String code;/** 结果状态 ,具体状态码参见枚举类ReturnCodeEnum.java*/private String message;private T data;private long timestamp ;private String traceId;public ResultData (){this.timestamp = System.currentTimeMillis();}public static <T> ResultData<T> success(T data) {ResultData<T> resultData = new ResultData<>();resultData.setCode(ReturnCodeEnum.RC200.getCode());resultData.setMessage(ReturnCodeEnum.RC200.getMessage());resultData.setData(data);return resultData;}public static <T> ResultData<T> fail(String code, String message) {ResultData<T> resultData = new ResultData<>();resultData.setCode(code);resultData.setMessage(message);return resultData;}}
增强Controller

ControllerLogAspect:环绕通知切点为Controller对日志参数做封装并打印

//增强接口的日志监控能力,提升调试和排查问题的效率,同时提供灵活的控制开关(如脱敏、禁用日志等)
@Order(value = Ordered.HIGHEST_PRECEDENCE)  //最高优先级
@Aspect
@Component
public class ControllerLogAspect {private Logger log = LoggerFactory.getLogger(this.getClass());private static final String SALT = "zr"; // 可以使用随机盐或固定盐//拦截以 Controller 结尾的所有方法@Around("execution(* com.zr..*Controller.*(..))")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {Object result = null;MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();try {//打印处理当前请求的完整类名和方法名称//例如:  接口方法:UserController.loginlog.info("接口方法:{}.{}", methodSignature.getDeclaringTypeName(), methodSignature.getName());//获取所有要打印的参数,丢到map中,key为参数名称,value为参数的值,然后会将这个map以json格式输出Map<String, Object> logParamsMap = new LinkedHashMap<>();String[] parameters = methodSignature.getParameterNames();Object[] args = joinPoint.getArgs();if(!ArrayUtil.isEmpty(parameters)){for (int i = 0; i < args.length; i++) {//判断是否需要脱敏if (parameterIsLog(methodSignature, i)) {//参数名称String parameterName = parameters[i];//参数值Object parameterValue = args[i];//将其放入到map中,稍后会以json格式输出logParamsMap.put(parameterName, parameterValue);}}}//输出示例:  方法参数列表:{"username":"admin","password":"secret"}log.info("方法参数列表:{}", JSONUtil.toJsonStr(logParamsMap));result = joinPoint.proceed();   //程序放行......return result;} finally {//判断方法的返回值是否需要打印?方法上有 @NoLogAnnotation 注解的,表示结果不打印方法返回值if (this.resultIsLog(methodSignature)) {log.info("方法返回值:{}", JSONUtil.toJsonStr(result));}}}/*** 指定位置的参数是否需要打印出来?(脱敏操作)*/private boolean parameterIsLog(MethodSignature methodSignature, int paramIndex) {if (methodSignature.getMethod().getParameterCount() == 0) {return false;}// 参数上有 @NoLogAnnotation注解的不会打印Annotation[] parameterAnnotation = methodSignature.getMethod().getParameterAnnotations()[paramIndex];if (parameterAnnotation != null && parameterAnnotation.length > 0) {for (Annotation annotation : parameterAnnotation) {if (annotation.annotationType() == NoLogAnnotation.class) {return false;}}}//参数类型是 ServletRequest / ServletResponse / 其他配置的敏感类型?→ 不打印Class parameterType = methodSignature.getParameterTypes()[paramIndex];for (Class<?> type : noLogTypes) {if (type.isAssignableFrom(parameterType)) {return false;}}return true;}// 参数类型是下面这些类型的,也不会打印,比如:ServletRequest、ServletResponse,大家可以扩展private static List<Class<?>> noLogTypes = Arrays.asList(ServletRequest.class, ServletResponse.class);/*** 判断方法的返回值是否需要打印?方法上有 @NoLogAnnotation 注解的,表示结果不打印方法返回值*/private boolean resultIsLog(MethodSignature methodSignature) {return methodSignature.getMethod().getAnnotation(NoLogAnnotation.class) == null;}
}
测试

查询数据效果
在这里插入图片描述

相关文章:

  • VSCode常用插件推荐
  • 普通IT的股票交易成长史--20250504实盘记录
  • 什么是unordered_map?用大白话说
  • GitLab CI/CD变量使用完全指南
  • 《奇迹世界起源》:宝箱工坊介绍!
  • 2025-04-26-利用奇异值重构矩阵-美团
  • 日本人工智能发展全景观察:从技术革新到社会重构的深度解析
  • 研0大模型学习(第11天)
  • AUTOSAR图解==>AUTOSAR_SWS_V2XManagement
  • Y1模拟一 补题报告
  • Electron 从零开始:构建你的第一个桌面应用
  • 状态值函数与状态-动作值函数
  • SQL手工注入(DVWA)
  • n8n 构建一个 ReAct AI Agent 示例
  • Dify 完全指南(一):从零搭建开源大模型应用平台(Ollama/VLLM本地模型接入实战)》
  • QT聊天项目DAY07
  • MPI,Pthreads和OpenMP等并行实验环境配置
  • n8n 快速入门2:构建自动化工作流
  • Scartch038(四季变换)
  • SSE技术的基本理解以及在项目中的使用
  • 想要“逆转”糖尿病,减少这两处脂肪是关键
  • 中小企业数字化转型的破局之道何在?
  • 人民日报今日谈:坚决克服麻痹思想,狠抓工作落实
  • 当Z世代与传统戏曲在春日校园相遇
  • 郭少雄导演逝世,享年82岁
  • 贵州赤水一处岩体崩塌致4车受损,连夜抢修后已恢复通车