天天看點

往程式日志中加上唯一辨別、讓你快速定位到相關日志請求資訊

作者:程式猿阿嘴
往程式日志中加上唯一辨別、讓你快速定位到相關日志請求資訊

最近看一個工程中将UUID列印在日志中、看到那個時候我想到的就是唯一請求流水編号、什麼意思呢、你可以了解為我調用一個接口他就會生成一個編号、這個編号就代表我之前請求的唯一辨別、後續出現問題能夠快速定位日志資訊。

開始-改造

我看别人改程中的列印很繁瑣、每個log.xxx()的時候都要傳這個編号、是以肯定是要優化一下的!哈哈哈哈!

這邊封裝了一個工具類、主要還是要懂ThreadLocal 線程本地變量 !簡單了解每個線程都有一份、能做到獨立互不幹涉。

java複制代碼 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都手動列印

java複制代碼 /**
  * 登入認證
  *
  * @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能夠準确的切入到對應方法。

java複制代碼 @Target(ElementType.METHOD)
 @Retention(RetentionPolicy.RUNTIME)
 public @interface RequestLog {
     /**
      * 日志描述
      */
     String value();
 }
           
java複制代碼 @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了。

java複制代碼 @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的一個工具類、提供外部直接調用、要注意的就是一個MDC是org.slf4j.MDC一個是org.jboss.logging.MDC雖然說都能使用、但是裡面的方法不一樣、最後使用org.slf4j.MDC這個就可以。

來先把RequestLogOperationAspect.RequestLogAround(.)這個方法改造了、這個是我們寫的Controller切入執行的入口。

java複制代碼 //自定義日志接口
 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();
 }
           
yml複制代碼 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中。

typescript複制代碼 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下檢視項目日志的方式就能快速找到請求流水對應的日志資訊。

原文連結:https://juejin.cn/post/7215640327633141819

繼續閱讀