最近看一個工程中将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