为程序日志添加唯一标识|焦点要闻
最近看一个工程中将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
死方式-每个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
从上面的日志打印就能发现问题一些问题吧、如果我很多接口这个 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
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复制代码 MapcopyOfContextMap = 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