有了这个高效日志打印工具,我司好久没出现甩锅侠了……

五阳 2024-10-28 10:20:19
为了更方便地排查问题,电商交易系统的日志中需要记录用户id和订单id等字段。然而,每次打印日志都需要手动设置用户id,这一过程非常繁琐,需要想个办法优化下。

 

  •  
  •  
  •  
  •  
log.warn("user:{}, orderId:{} 订单提单成功",userId, orderId);log.warn("user:{}, orderId:{} 订单支付成功",userId, orderId);log.warn("user:{}, orderId:{} 订单收到履约请求",userId, orderId);log.warn("user:{}, orderId:{} 订单履约成功",userId, orderId);

 

一、目标

 

打印日志时,自动填充用户id和订单Id等通参,无需手动指定

 

二、实现思路

 

日志模板中声明占位符 userId,orderId

在业务入口将userId放入到线程ThreadLocal本地变量中

使用SpringAop + 注解 自动将第二步的用户信息放到线程上下文

 

三、配置日志变量,读取上下文变量

 

%X{}可以自定义占位符,例如本例中 使用 userId:%X{userId} orderId:%X{orderId},定义了userId和orderId两个占位符。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
<?xml version="1.0" encoding="UTF-8"?><Configuration status="info">
    <Appenders>        <Console name="consoleAppender" target="SYSTEM_OUT">            <PatternLayout pattern="%d{DEFAULT} [%t] %-5p - userId:%X{userId} orderId:%X{orderId} %m%n%ex" charset="UTF-8"/>        </Console>    </Appenders>    <Loggers>        <!-- Root Logger -->        <AsyncRoot level="info" includeLocation="true">            <appender-ref ref="consoleAppender"/>        </AsyncRoot>    </Loggers></Configuration>

 

四、基于MDC 将订单和用户信息放到线程的上下文Map

 

为了给每个请求添加唯一标识,用户可将上下文信息放入MDC(Mapped Diagnostic Context)。

 

slfj 提供了MDC 类,可以将变量设置在线程上下文中,日志框架会自动将线程上下文中的变量放置到日志占位符中。Slf4j 作为java日志标准,log4j和logback都实现了slfj 日志标准。

 

MDC是基于每个线程进行管理的,允许每个服务器线程具有不同的MDC标记。MDC类中的put()和get()操作仅影响当前线程的MDC。其他线程中的MDC不会受到影响,所以可以理解MDC是基于ThreadLocal的Map。

 

例如下面这种方式,打印日志的效果是这样的。

 

  •  
  •  
  •  
MDC.put("userId", userId);MDC.put("orderId", orderId);log.warn("订单履约完成");

 

当使用log.warn("订单履约完成") 方式打印日志时,代码中会自动包含userId和 订单Id。

 

  •  
2024-08-17 21:35:38,284 [main] WARN  - userId:32894934895 orderId:8497587947594859232 订单履约完成

 

接下来,声明一个注解加切面,自动将用户和订单信息放到日志占位符中。

 

五、注解 + SpringAop,自动将UserId放到MDC

 

通过注解的方式,在方法执行之前自动将UserId注入到MDC中。其中的难点在于如何获取到UserId。

 

我的思路是,方法的入参中肯定包含了UserId。可以在注解中声明UserId的获取路径,在切面中获取到UserId,并将其注入到MDC中。

 

 

1、定义注解

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface UserLog {
   String userId() default "";      String orderId() default "";}

 

使用时,要求输入userId属性的路径。例如UserOrder中包含userId和orderId属性,则像如下方式声明。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
@UserLog(userId = "userId", orderId = "orderId")public void orderPerform(UserOrder order) {   log.warn("订单履约完成");}
@Datapublic static class UserOrder {   String userId;   String orderId;}

 

 

2、定义切面

 

声明注解的Aop切面,在方法执行前,将UserId从入参中取出来,放到MDC中。全部代码如下

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
@Aspect@Componentpublic class UserLogAspect {
   @Pointcut("@annotation(UserLog) && execution(public * *(..))")   public void pointcut() {   }
   @Around(value = "pointcut()")   public Object around(ProceedingJoinPoint joinPoint) throws Throwable {      //无参方法不处理      Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();      Object[] args = joinPoint.getArgs();
      //获取注解      UserLog userLogAnnotation = method.getAnnotation(UserLog.class);      if (userLogAnnotation != null && args != null && args.length > 0) {         //使用工具类获取userId。         String userId = String.valueOf(PropertyUtils.getProperty(args[0], userLogAnnotation.userId()));         String orderId = String.valueOf(PropertyUtils.getProperty(args[0], userLogAnnotation.orderId()));         // 放到MDC中         MDC.put("userId", userId);         MDC.put("orderId", orderId);      }
      try {         Object response = joinPoint.proceed();         return response;      } catch (Exception e) {         throw e;      } finally {         //清理MDC         MDC.clear();      }
   }}

 

 

3、关键代码解读

 

1)获取UserLog注解

 

  •  
UserLog userLogAnnotation = method.getAnnotation(UserLog.class);

 

2)使用PropertyUtils.getProperty 获取userId

 

  •  
PropertyUtils.getProperty(args[0], userLogAnnotation.userId())

 

要注意 PropertyUtils 是commons-beanutils提供的工具类,可以指定属性的路径,自动提取属性值。如果存在多层关系,可以使用 . 级联取属性值。

 

例如 info.userId,则从对象的info属性中取userId属性。

 

  •  
  •  
  •  
  •  
  •  
<dependency>    <groupId>commons-beanutils</groupId>    <artifactId>commons-beanutils</artifactId>    <version>1.9.4</version></dependency>

 

3)使用MDC设置变量和清除变量

 

  •  
  •  
MDC.put("userId", userId);MDC.clear();

 

六、验证使用效果

 

 

1、声明业务Service

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
@Servicepublic class OrderService {   public static final Logger log = LoggerFactory.getLogger(OrderService.class);      @UserLog(userId = "userId", orderId = "orderId")   public void orderPerform(UserOrder order) {      log.warn("订单履约完成");   }
   @Data   public static class UserOrder {      String userId;      String orderId;   }}

 

 

2、测试日志打印

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
@Testpublic void testUserLog() {   OrderService.UserOrder order = new OrderService.UserOrder();   order.setUserId("32894934895");   order.setOrderId("8497587947594859232");   orderService.orderPerform(order);}

 

 

3、日志效果

 

图片

 

七、总结

 

不同的业务场景有不同的日志需求,一般情况下为了排查问题方便,需要唯一标识把一系列请求串联起来,使用 UserLog 注解+Aop ,自动将这部分默认参数放到日志中,可以简化业务日志打印,极大地提高了生产力。

 

另外大家可以自行扩展能力,例如自动打印出入参日志,自动上报监控打点等等。

 

各位朋友,以上工具的关键代码不超过30行,快点试试吧。

 

作者丨五阳
来源丨网址:juejin.cn/post/7407275971902357558
dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn
最新评论
访客 2024年04月08日

如果字段的最大可能长度超过255字节,那么长度值可能…

访客 2024年03月04日

只能说作者太用心了,优秀

访客 2024年02月23日

感谢详解

访客 2024年02月20日

一般干个7-8年(即30岁左右),能做到年入40w-50w;有…

访客 2023年08月20日

230721

活动预告