likes
comments
collection
share

往程序日志中加上唯一标识、让你快速定位到相关日志请求信息

作者站长头像
站长
· 阅读数 17

最近看一个工程中将UUID打印在日志中、看到那个时候我想到的就是唯一请求流水编号、什么意思呢、你可以理解为我调用一个接口他就会生成一个编号、这个编号就代表我之前请求的唯一标识、后续出现问题能够快速定位日志信息。

开始-改造

我看别人改成中的打印很繁琐、每个log.xxx()的时候都要传这个编号、所以肯定是要优化一下的!哈哈哈哈!

这边封装了一个工具类、主要还是要懂ThreadLocal 线程本地变量 !简单理解每个线程都有一份、能做到独立互不干涉。

 package com.stall.config;
 
 import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
 
 /**
  * 日志请求流水、用日志追踪
  *
  * @Author 突突突突突
  * @blog https://juejin.cn/user/844892408381735
  * @Date 2023/3/24 13:24
  */
 public class RequestLogManagement {
     public static ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<>();
   
     /**
      *  初始入口、后续打印调用
      * @param describe 入口描述
      */
     public static void init(String describe) {
         Map<String, Object> threadLocalMap = new HashMap<>();
         String requestUUID = UUID.randomUUID().toString();
         threadLocalMap.put("describe", describe);
         threadLocalMap.put("uuid", requestUUID);
         threadLocal.set(threadLocalMap);
     }
 
     public static String getRequestUUID() {
         return threadLocal.get() == null 
           ? "" : String.valueOf(threadLocal.get().get("uuid"));
     }
 
     public static String getRequestDescribe() {
         return threadLocal.get() == null 
           ? "" : String.valueOf(threadLocal.get().get("describe"));
     }

     public static void remove() {
         threadLocal.remove();
     }
 }

死方式-每个log都手动打印

 /**
  * 登录认证
  *
  * @Author 突突突突突
  * @blog https://juejin.cn/user/844892408381735
  * @Date 2023/3/24 13:49
  */
 @Slf4j
 @RestController
 @RequestMapping("/auth")
 public class WxLoginController {
 
     @Resource
     private AuthService authService;
 
     @PostMapping("/wx/login")
     public R<Object> wxLogin(String code) {
         RequestLogManagement.init("微信登录接口");
         try {
             log.info("{}、开始调用微信登录接口",RequestLogManagement.getRequestUUID());
             authService.wxLogin(code);
             return R.success();
         } catch (InterfaceException e) {
             log.error("{}、收到请求异常信息",RequestLogManagement.getRequestUUID(), e);
             return R.custom(e.getCode(), e.getMessage());
         } catch (Exception e) {
             log.error("{}、收到请求异常信息",RequestLogManagement.getRequestUUID(), e);
             return R.failed();
         }finally {
             RequestLogManagement.remove();
         }
     }
 }

从上面的日志打印就能发现问题一些问题吧、如果我很多接口这个RequestLogManagement.init("微信登录接口");log.info("{}、xxxxxx调用",RequestLogManagement.getRequestUUID());RequestLogManagement.remove();这些内容中很多重复的操作、首先我们解决入口开始描述/入口结束清除数据、用眼睛一看就知道用什么解决这个问题、那就是AOP的方式、在Controller接口请求的方法中的前后进行增强处理。

就是说知道用AOP的方式后、在写牛点自定义一个注解用于AOP能够准确的切入到对应方法。

 @Target(ElementType.METHOD)
 @Retention(RetentionPolicy.RUNTIME)
 public @interface RequestLog {
     /**
      * 日志描述
      */
     String value();
 }
 @Slf4j
 @Aspect
 @Component
 public class RequestLogOperationAspect {
     /**
      * 准备环绕的方法
      */
     @Pointcut("@annotation(com.stall.config.aop.RequestLog)")
     public void execRequestLogService() {
     }
 
     @Around("execRequestLogService()")
     public Object RequestLogAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
         //目标对象
         Class<?> clazz = proceedingJoinPoint.getTarget().getClass();
         //方法签名
         String method = proceedingJoinPoint.getSignature().getName();
         //方法参数
         Object[] thisArgs = proceedingJoinPoint.getArgs();
         //方法参数类型
         Class<?>[] parameterTypes = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod().getParameterTypes();
         //方法
         Method thisMethod = clazz.getMethod(method, parameterTypes);
         //自定义日志接口
         RequestLog methodAnnotation = Objects.requireNonNull(AnnotationUtils.findAnnotation(thisMethod, RequestLog.class));
         //  通用日志打印
         RequestLogManagement.init(methodAnnotation.value());
         log.info("[{}][{}]请求开始、请求参数:{}",RequestLogManagement.getRequestUUID(), methodAnnotation.value(), Arrays.toString(thisArgs));
         Object proceed = null;
         try {
             proceed = proceedingJoinPoint.proceed();
         } finally {
             log.info("[{}][{}]请求结束、请求参数:{}",RequestLogManagement.getRequestUUID(), methodAnnotation.value(), proceed);
             // 清除数据
             RequestLogManagement.remove();
         }
         return proceed;
     }
 }

然后改造好后的代码、我们在入口上加一个注解就ok了。

 @RequestLog(value = "微信登录接口")
 @PostMapping("/wx/login")
 public R<Object> wxLogin(String code) {
   try {
     log.info("{}、开始调用微信登录接口",RequestLogManagement.getRequestUUID());
     authService.wxLogin(code);
     return R.success();
   } catch (InterfaceException e) {
     log.error("{}、收到请求异常信息",RequestLogManagement.getRequestUUID(), e);
     return R.custom(e.getCode(), e.getMessage());
   } catch (Exception e) {
     log.error("{}、收到请求异常信息",RequestLogManagement.getRequestUUID(), e);
     return R.failed();
   }
 }

MDC-不需要每个log都手动打印

但是现在解决了那个问题还有这个log.info("{}、xxxxxx调用",RequestLogManagement.getRequestUUID());我总不能说我每次打印日志我都要加一个RequestLogManagement.getRequestUUID()

所以身为大聪明的我又想到AOP的方式、去增强log对象中的所有方法、于是我打开百度找阿找!!!我就发现一个牛很多的写法、就是MDC类对象中可能放入参数、而这个参数能够被日志底层使用、相当于在我们打印日志的时候可以向日志中塞入一个值、类似插槽一样的概念、用就加、不用就不加!!!

MDC底层也是靠ThreadLocal来实现的、他泛型是Map类型、就相当于能放键值对的形式的数据、而MDC就相当于是我们刚刚写RequestLogManagement的一个工具类、提供外部直接调用、要注意的就是一个MDCorg.slf4j.MDC一个是org.jboss.logging.MDC虽然说都能使用、但是里面的方法不一样、最后使用org.slf4j.MDC这个就可以。

来先把RequestLogOperationAspect.RequestLogAround(.)这个方法改造了、这个是我们写的Controller切入执行的入口。

 //自定义日志接口
 RequestLog methodAnnotation = Objects.requireNonNull(AnnotationUtils.findAnnotation(thisMethod, RequestLog.class));
 //  通用日志打印
 RequestLogManagement.init(methodAnnotation.value());
 // 将UUID放入到MDC对象中
 MDC.put("requestId", RequestLogManagement.getRequestUUID());
 log.info("[{}]请求开始、请求参数:{}", methodAnnotation.value(), Arrays.toString(thisArgs));
 Object proceed = null;
 try {
   proceed = proceedingJoinPoint.proceed();
 } finally {
   log.info("[{}]请求结束、请求参数:{}", methodAnnotation.value(), proceed);
   RequestLogManagement.remove();
   // 执行完成后清除。
   MDC.clear();
 }
 logging:
   pattern:
     console: "${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr([%X{requestId}]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"

修改日志的打印格式、主要看%X{requestId}、当前的name就是MDC.put中的key的名称。

默认打印日志 往程序日志中加上唯一标识、让你快速定位到相关日志请求信息

修改后的打印日志 往程序日志中加上唯一标识、让你快速定位到相关日志请求信息 不管我们自己写的RequestLogManagement还是MDC这两种方式都不能在子线程中获取到、解决方法就是在线程外将值赋值出去、然后由子线程重新塞入到自己线程副本的ThreadLocal中。

 Map<String, String> copyOfContextMap = MDC.getCopyOfContextMap();
 new Thread(new Runnable() {
   @Override
   public void run() {
     MDC.setContextMap(copyOfContextMap);
     for (int i = 0; i < 10; i++) {
       log.info(">>>>>>>>>i={}", i);
     }
     MDC.clear();
   }
 }).start();

小结

以上方式主要适用单机环境、如分布式服务之间的调用、肯定有其他的更好更牛的链路的方式。

把上面方式集成到你的单机项目中再配合之前写的 linux下查看项目日志的方式就能快速找到请求流水对应的日志信息。