最近看一个工程中将UUID打印在日志中、看到那个时分我想到的便是仅有恳求流水编号、什么意思呢、你能够理解为我调用一个接口他就会生成一个编号、这个编号就代表我之前恳求的仅有标识、后续出现问题能够快速定位日志信息。
开端-改造
我看他人改成中的打印很繁琐、每个log.xxx()的时分都要传这个编号、所以肯定是要优化一下的!哈哈哈哈!
这边封装了一个东西类、主要还是要懂ThreadLocal
线程本地变量 !简单理解每个线程都有一份、能做到独立互不干涉。
package com.stall.config;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 日志恳求流水、用日志追踪
*
* @Author 突突突突突
* @blog https:///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:///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
的一个东西类、提供外部直接调用、要注意的便是一个MDC
是org.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下检查项目日志的办法就能快速找到恳求流水对应的日志信息。