• 033-从零搭建微服务-日志插件(一)


    写在最前

    如果这个项目让你有所收获,记得 Star 关注哦,这对我是非常不错的鼓励与支持。

    源码地址(后端):mingyue: 🎉 基于 Spring Boot、Spring Cloud & Alibaba 的分布式微服务架构基础服务中心

    源码地址(前端):mingyue-ui: 🎉 基于 Vue3 + TS + Vite + Element plus 等技术,适配 MingYue 后台微服务

    文档地址:Wiki - Gitee.com

    为什么要日志插件?

    在Java应用程序中记录日志是一种良好的实践,它为开发、运维和支持团队提供了很多好处。以下是一些主要的理由:

    1. 故障排除和调试:

      • 日志是定位和解决问题的重要工具。通过在关键代码路径和操作中插入日志语句,开发人员可以追踪应用程序的执行流程,快速定位潜在的错误和异常。

    2. 性能分析:

      • 记录关键操作的执行时间、资源使用情况等信息,有助于性能分析和优化。通过分析日志,可以确定应用程序的瓶颈并改进性能。

    3. 安全审计:

      • 记录关键的安全事件和用户活动,以便进行审计和检测潜在的安全威胁。登录失败、访问敏感信息等事件的记录对于安全监控至关重要。

    4. 系统状态监控:

      • 通过记录系统状态和关键指标,可以实时监控应用程序的运行状况。这有助于及时发现和解决潜在的问题,以提高系统的稳定性和可用性。

    5. 版本追踪和审计:

      • 在代码中记录版本信息、变更历史和代码提交信息,有助于追踪应用程序的演变过程。审计日志还可以用于追溯特定功能或问题的起源。

    6. 用户行为分析:

      • 对于包含用户交互的应用程序,记录用户活动可以帮助了解他们的使用模式、偏好和行为。这对于改进用户体验和调整产品设计非常有帮助。

    7. 合规性和法规要求:

      • 许多行业和法规要求记录关键事件和操作,以确保企业的合规性。通过日志记录,可以满足这些法规的要求,并提供审计证据。

    8. 持久化数据:

      • 将日志存储在持久化介质中,例如文件或数据库,以便在应用程序重新启动后仍然可以访问日志。这有助于在系统故障或应用程序崩溃时还原状态并进行故障排除。

    日志设计

    日志类型

    系统操作日志和用户登录日志是两种不同类型的日志,它们记录了系统中不同方面的活动

    • 系统操作日志:记录系统的各种操作,包括但不限于增删改查、上传与下载文件等。

    • 用户登录日志:记录用户登录和注销的信息。

    记录方式

    • 系统操作日志:采用注解(非侵入)方式记录;

    • 用户登录日志:采用显式(侵入)方式记录;

    日志插件

    添加 mingyue-common-log 插件

    
      
        com.csp.mingyue
        mingyue-common-security
      
    

    Log 注解

    @Target({ ElementType.PARAMETER, ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Log {
    ​
      /**
       * 模块
       */
      String module() default "";
    ​
      /**
       * 功能
       */
      BusinessType businessType() default BusinessType.OTHER;
    ​
      /**
       * 操作人类别
       */
      OperatorUserType operatorUserType() default OperatorUserType.MANAGE;
    ​
      /**
       * 是否保存请求的参数
       */
      boolean isSaveRequestData() default true;
    ​
      /**
       * 是否保存响应的参数
       */
      boolean isSaveResponseData() default true;
    ​
      /**
       * 排除指定的请求参数
       */
      String[] excludeParamNames() default {};
    ​
    }

    Log 切面

    操作日志记录核心类

    @Slf4j
    @Aspect
    @RequiredArgsConstructor
    @AutoConfiguration
    public class LogAspect {
    ​
      private final ServiceInstance serviceInstance;
    ​
      /**
       * 排除敏感属性字段
       */
      public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
    ​
      /**
       * 处理完请求后执行
       * @param joinPoint 切点
       */
      @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
      public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
        handleLog(joinPoint, controllerLog, null, jsonResult);
      }
    ​
      /**
       * 拦截异常操作
       * @param joinPoint 切点
       * @param e 异常
       */
      @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
      public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
        handleLog(joinPoint, controllerLog, e, null);
      }
    ​
      protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
        // 日志记录开始时间
        Long startTime = System.currentTimeMillis();
    ​
        // ========数据库日志========
        OperateLogEvent operateLog = new OperateLogEvent();
    ​
        try {
          // 请求信息
          String ip = ServletUtils.getClientIP();
          operateLog.setReqIp(ip);
          operateLog.setServiceId(serviceInstance.getServiceId());
          operateLog.setReqAddress(AddressUtils.getRealAddressByIP(ip));
          operateLog
              .setReqUrl(StrUtil.sub(Objects.requireNonNull(ServletUtils.getRequest()).getRequestURI(), 0, 255));
    ​
          operateLog.setStatus(BusinessStatus.SUCCESS.ordinal());
    ​
          // 用户信息
          LoginUser loginUser = LoginHelper.getLoginUser();
          operateLog.setUserId(loginUser.getUserId());
          operateLog.setUserName(loginUser.getUsername());
    ​
          if (e != null) {
            operateLog.setStatus(BusinessStatus.FAIL.ordinal());
            operateLog.setException(StrUtil.sub(e.getMessage(), 0, 2000));
          }
    ​
          // 设置方法名称
          String className = joinPoint.getTarget().getClass().getName();
          String methodName = joinPoint.getSignature().getName();
          operateLog.setMethod(className + "." + methodName + "()");
    ​
          // 设置User-Agent
          operateLog.setUserAgent(ServletUtils.getRequest().getHeader(HttpHeaders.USER_AGENT));
          // 设置请求方式
          operateLog.setReqMethod(ServletUtils.getRequest().getMethod());
          // 处理设置注解上的参数
          getControllerMethodDescription(joinPoint, controllerLog, operateLog, jsonResult);
        }
        catch (Exception exp) {
          // 记录本地异常日志
          log.error("异常信息:{}", exp.getMessage());
        }
        finally {
          Long endTime = System.currentTimeMillis();
          operateLog.setDuration(endTime - startTime);
          // 发布事件保存数据库
          SpringUtils.context().publishEvent(operateLog);
        }
      }
    ​
      /**
       * 获取注解中对方法的描述信息 用于Controller层注解
       * @param log 日志
       * @param operateLog 操作日志
       */
      public void getControllerMethodDescription(JoinPoint joinPoint, Log log, OperateLogEvent operateLog,
          Object jsonResult) throws Exception {
        // 设置标题
        operateLog.setModule(log.module());
        // 设置 action 动作
        operateLog.setBusinessType(log.businessType().ordinal());
        // 设置操作人类别
        operateLog.setUserType(log.operatorUserType().ordinal());
        // 是否需要保存 request,参数和值
        if (log.isSaveRequestData()) {
          // 获取参数的信息,传入到数据库中。
          setRequestValue(joinPoint, operateLog, log.excludeParamNames());
        }
        // 是否需要保存 response,参数和值
        if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {
          R resp = JSONUtil.toBean(JSONUtil.toJsonStr(jsonResult), R.class);
          operateLog.setRespMsg(resp.getMsg());
          operateLog.setRespCode(resp.getCode());
          operateLog.setRespResult(StrUtil.sub(JSONUtil.toJsonStr(jsonResult), 0, 2000));
        }
      }
    ​
      /**
       * 获取请求的参数,放到log中
       * @param operLog 操作日志
       * @throws Exception 异常
       */
      private void setRequestValue(JoinPoint joinPoint, OperateLogEvent operLog, String[] excludeParamNames)
          throws Exception {
        Map paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
        String requestMethod = operLog.getReqMethod();
        if (MapUtil.isEmpty(paramsMap) && HttpMethod.PUT.name().equals(requestMethod)
            || HttpMethod.POST.name().equals(requestMethod)) {
          String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
          operLog.setReqParams(StrUtil.sub(params, 0, 2000));
        }
        else {
          MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
          MapUtil.removeAny(paramsMap, excludeParamNames);
          operLog.setReqParams(StrUtil.sub(JSONUtil.toJsonStr(paramsMap), 0, 2000));
        }
      }
    ​
      /**
       * 参数拼装
       */
      private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
        StringJoiner params = new StringJoiner(" ");
        if (ArrayUtil.isEmpty(paramsArray)) {
          return params.toString();
        }
        for (Object o : paramsArray) {
          if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
            String str = JSONUtil.toJsonStr(o);
            Dict dict = JsonUtils.parseMap(str);
            if (MapUtil.isNotEmpty(dict)) {
              MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
              MapUtil.removeAny(dict, excludeParamNames);
              str = JSONUtil.toJsonStr(dict);
            }
            params.add(str);
          }
        }
        return params.toString();
      }
    ​
      /**
       * 判断是否需要过滤的对象。
       * @param o 对象信息。
       * @return 如果是需要过滤的对象,则返回true;否则返回false。
       */
      @SuppressWarnings("rawtypes")
      public boolean isFilterObject(final Object o) {
        Class clazz = o.getClass();
        if (clazz.isArray()) {
          return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        }
        else if (Collection.class.isAssignableFrom(clazz)) {
          Collection collection = (Collection) o;
          for (Object value : collection) {
            return value instanceof MultipartFile;
          }
        }
        else if (Map.class.isAssignableFrom(clazz)) {
          Map map = (Map) o;
          for (Object value : map.values()) {
            return value instanceof MultipartFile;
          }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
            || o instanceof BindingResult;
      }
    ​
    }

    异步调用日志服务

    @Slf4j
    @Component
    @RequiredArgsConstructor
    public class LogEventListener {
    ​
      private final RemoteLogService remoteLogService;
    ​
      /**
       * 保存系统日志记录
       */
      @Async
      @EventListener
      public void saveLog(OperateLogEvent operateLog) {
        log.info("保存系统日志记录落库「{}」", JSONUtil.toJsonStr(operateLog));
        remoteLogService.saveSysOperateLog(BeanUtil.copyProperties(operateLog, SysOperateLog.class));
      }
    ​
    }

    自动注入日志类

    org.springframework.boot.autoconfigure.AutoConfiguration.imports

    com.csp.mingyue.common.log.event.LogEventListener
    com.csp.mingyue.common.log.aspect.LogAspect

    系统操作日志表设计

    DROP TABLE IF EXISTS sys_operate_log;
    CREATE TABLE sys_operate_log (
        operate_log_id    BIGINT(20)         NOT NULL                 COMMENT '操作日志ID',
        module            VARCHAR(50)        DEFAULT ''               COMMENT '模块',
        business_type     INT(2)             DEFAULT 0                COMMENT '业务类型(0其它 1新增 2修改 3删除)',
        method            VARCHAR(100)       DEFAULT ''               COMMENT '方法名称',
        service_id        VARCHAR(32)        DEFAULT NULL             COMMENT '服务ID',
        user_id           BIGINT(20)         NOT NULL                 COMMENT '用户ID',
        user_name         VARCHAR(50)        NOT NULL                 COMMENT '用户账号',
        user_type         TINYINT(1)         DEFAULT 0                COMMENT '用户类型(0其它 1系统用户)',
        user_agent        VARCHAR(1000)      DEFAULT NULL             COMMENT '用户代理',
        req_ip            VARCHAR(128)       DEFAULT ''               COMMENT '请求IP',
        req_address       VARCHAR(255)       DEFAULT ''               COMMENT '请求地点',
        req_url           VARCHAR(255)       DEFAULT ''               COMMENT '请求URL',
        req_method        VARCHAR(20)        DEFAULT NULL             COMMENT '请求方式',
        req_params        TEXT               DEFAULT NULL             COMMENT '请求参数',
        duration          BIGINT             NOT NULL                 COMMENT '执行时长,单位(ms)',
        resp_code         INT                DEFAULT NULL             COMMENT '结果码',
        resp_msg          VARCHAR(512)       NULL DEFAULT ''          COMMENT '结果提示',
        resp_result       VARCHAR(2000)      DEFAULT ''               COMMENT '返回参数',
        status            CHAR(1)            DEFAULT 0                COMMENT '操作状态(0正常 1异常)',
        exception         TEXT               DEFAULT NULL             COMMENT '异常信息',
        operate_time      DATETIME           NOT NULL                 COMMENT '操作时间',
        is_deleted        CHAR(1)            DEFAULT '0'              COMMENT '删除标志(0正常,1删除)',
        create_by         VARCHAR(64)        DEFAULT ''               COMMENT '创建者',
        create_time       DATETIME           DEFAULT NULL             COMMENT '创建时间',
        update_by         VARCHAR(64)        DEFAULT ''               COMMENT '更新者',
        update_time       DATETIME           DEFAULT NULL             COMMENT '更新时间',
        PRIMARY KEY (operate_log_id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC COMMENT='系统操作日志';

    测试日志注解

    使用注解

    @Log(module = "用户管理", businessType = BusinessType.DELETE)

    @DeleteMapping("{userId}")
    @Log(module = "用户管理", businessType = BusinessType.DELETE)
    @Operation(summary = "删除用户", parameters = { @Parameter(name = "userId", description = "用户ID", required = true) })
    public R delUser(@PathVariable Long userId) {
      return R.ok(sysUserService.delUser(userId));
    }

    调用接口

    调用完成后查看数据库是否存在该操作记录即可

    curl -X 'DELETE'   'http://192.168.63.114:7100/system/sysUser/111111111'   -H 'accept: */*'   -H 'Authorization: UWapduuggQcNSqg1oQZ17ZyfPHDxxt8Q'
  • 相关阅读:
    【Python笔记-设计模式】适配器模式
    苏宁API接口
    【CopyOnWriteArrayList源码分析】
    推荐一个简单、灵活、好看、强大的 .Net 图表库
    在虚拟机安装Hadoop
    【Pytorch Lighting】第 1 章:PyTorch Lightning adventure
    推荐几个pdf转txt免费软件,轻松让你做到pdf转txt
    3 基于采样的路径规划 —— RRT算法
    Aop天花板
    算法——回溯法(1)
  • 原文地址:https://blog.csdn.net/csp732171109/article/details/134448899