feat:V3.1.1整合remi-comm到remi-cloud中
This commit is contained in:
69
remi-comm/remi-comm-log/README.md
Normal file
69
remi-comm/remi-comm-log/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# 工程简介
|
||||
瑞米平台公共的日志模块
|
||||
|
||||
## 操作日志
|
||||
主要面向用户的,要求简单易懂,反映出用户所做的动作。
|
||||
|
||||
### 核心功能
|
||||
- 要实现以下功能:
|
||||
|
||||
- 自定义spring boot starter
|
||||
- 定义日志注解
|
||||
- AOP拦截日志注解方法
|
||||
- 定义日志动态内容模板
|
||||
|
||||
- 模板中又需要实现:
|
||||
- 动态模板表达式解析:用强大的SpEL来解析表达式
|
||||
- 自定义函数:支持目标方法前置/后置的自定义函数
|
||||
|
||||
##
|
||||
|
||||
### SQL脚本
|
||||
```sql
|
||||
DROP TABLE IF EXISTS t_system_log;
|
||||
CREATE TABLE t_system_log
|
||||
(
|
||||
id VARCHAR(32) NOT NULL COMMENT '日志编号',
|
||||
user_id VARCHAR(32) COMMENT '用户ID',
|
||||
user_name VARCHAR(255) COMMENT '用户名称',
|
||||
app_code VARCHAR(255) COMMENT '应用编码',
|
||||
app_name VARCHAR(255) COMMENT '应用名称',
|
||||
business_no VARCHAR(255) COMMENT '业务流水号',
|
||||
business_type VARCHAR(255) COMMENT '业务类型(新增、修改、删除、查询、导入、导出、其他)',
|
||||
log_content LONGTEXT COMMENT '日志模板内容',
|
||||
log_extras LONGTEXT COMMENT '额外的日志内容',
|
||||
request_url VARCHAR(255) COMMENT '请求地址',
|
||||
request_ip VARCHAR(255) COMMENT '请求IP',
|
||||
request_type VARCHAR(255) COMMENT '请求类型(POST、PUT、OPTIONS、DELETE)',
|
||||
content_type VARCHAR(255) COMMENT '请求报文类型(map、json)',
|
||||
request_body LONGTEXT COMMENT '请求报文',
|
||||
response_body LONGTEXT COMMENT '响应报文',
|
||||
request_time DATETIME COMMENT '请求时间',
|
||||
response_time DATETIME COMMENT '响应时间',
|
||||
execute_time BIGINT COMMENT '请求耗时',
|
||||
trace_id VARCHAR(255) COMMENT '链路追踪ID',
|
||||
error_info LONGTEXT COMMENT '异常信息',
|
||||
user_agent VARCHAR(255) COMMENT '浏览器、操作系统、UI引擎标识',
|
||||
revision INTEGER(11) COMMENT '乐观锁',
|
||||
is_deleted INTEGER(1) COMMENT '逻辑删除;0-未删除 1-已删除',
|
||||
group_tenant_id VARCHAR(32) COMMENT '部门租户ID',
|
||||
company_tenant_id VARCHAR(32) COMMENT '公司租户ID',
|
||||
created_by VARCHAR(32) COMMENT '创建人',
|
||||
created_at DATETIME COMMENT '创建时间',
|
||||
updated_by VARCHAR(32) COMMENT '更新人',
|
||||
updated_at DATETIME COMMENT '更新时间',
|
||||
PRIMARY KEY (id)
|
||||
) COMMENT = '系统日志表';
|
||||
```
|
||||
## 系统日志
|
||||
主要用于开发者调试排查系统问题的,不要求固定格式和可读性。
|
||||
|
||||
###
|
||||
|
||||
## 延伸阅读
|
||||
https://tech.meituan.com/2021/09/16/operational-logbook.html
|
||||
|
||||
https://juejin.cn/post/7075184311397253128
|
||||
|
||||
http://mysql.taobao.org/monthly/2017/09/08/
|
||||
|
||||
69
remi-comm/remi-comm-log/pom.xml
Normal file
69
remi-comm/remi-comm-log/pom.xml
Normal file
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>remi-comm-log</artifactId>
|
||||
<name>remi-comm-log</name>
|
||||
<description>remi-comm-log</description>
|
||||
|
||||
<parent>
|
||||
<groupId>ai.remi.top.cloud</groupId>
|
||||
<artifactId>remi-comm</artifactId>
|
||||
<version>3.1.1</version>
|
||||
</parent>
|
||||
|
||||
<!-- 打包上传到私服, 需要和配置文件中的server节点配合使用 -->
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>maven-releases</id>
|
||||
<name>release</name>
|
||||
<url>https://repository.njydsz.com/repository/maven-releases/</url>
|
||||
</repository>
|
||||
<snapshotRepository>
|
||||
<id>maven-snapshots</id>
|
||||
<name>snapshot</name>
|
||||
<url>https://repository.njydsz.com/repository/maven-snapshots/</url>
|
||||
</snapshotRepository>
|
||||
</distributionManagement>
|
||||
|
||||
<dependencies>
|
||||
<!--spring-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<!--nacos-->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
<!--公共依赖-->
|
||||
<dependency>
|
||||
<groupId>ai.remi.top.cloud</groupId>
|
||||
<artifactId>remi-comm-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.remi.top.cloud</groupId>
|
||||
<artifactId>remi-comm-util</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.remi.top.cloud</groupId>
|
||||
<artifactId>remi-comm-redis</artifactId>
|
||||
</dependency>
|
||||
<!--项目依赖-->
|
||||
<dependency>
|
||||
<groupId>ai.remi.top.cloud</groupId>
|
||||
<artifactId>remi-sys-api</artifactId>
|
||||
</dependency>
|
||||
<!--lombok-->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,23 @@
|
||||
package ai.remi.comm.log.annotation;
|
||||
|
||||
import ai.remi.comm.log.config.LogRecordAutoConfigure;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.annotation.Order;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* @author DianJiu 【公众号 点九开源】
|
||||
* @email dianjiuxyz@gmail.com
|
||||
* @date 2022-07-19
|
||||
* @desc 日志记录开启注解
|
||||
*/
|
||||
@Documented
|
||||
@Inherited
|
||||
@Order(Integer.MIN_VALUE)
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Import(LogRecordAutoConfigure.class)
|
||||
public @interface EnableLogRecord {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package ai.remi.comm.log.annotation;
|
||||
|
||||
|
||||
import ai.remi.comm.log.enums.BusinessType;
|
||||
import ai.remi.comm.log.enums.OperatorType;
|
||||
import org.springframework.core.annotation.Order;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* @author DianJiu 【公众号 点九开源】
|
||||
* @email dianjiuxyz@gmail.com
|
||||
* @date 2022-07-19
|
||||
* @desc 操作日志记录
|
||||
*/
|
||||
@Inherited
|
||||
@Documented
|
||||
@Order(Integer.MIN_VALUE)
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface LogRecord {
|
||||
|
||||
/**
|
||||
* 接口名称
|
||||
*/
|
||||
String content() default "";
|
||||
|
||||
/**
|
||||
* 功能操作类型
|
||||
*/
|
||||
BusinessType businessType() default BusinessType.OTHER;
|
||||
|
||||
/**
|
||||
* 操作人类别
|
||||
*/
|
||||
OperatorType operatorType() default OperatorType.MANAGE;
|
||||
|
||||
/**
|
||||
* 是否保存请求的参数
|
||||
*/
|
||||
boolean isSaveRequestData() default true;
|
||||
|
||||
/**
|
||||
* 是否保存响应的参数
|
||||
*/
|
||||
boolean isSaveResponseData() default true;
|
||||
|
||||
/**
|
||||
* 是否保存异常的信息
|
||||
*/
|
||||
boolean isSaveErrorInfo() default true;
|
||||
|
||||
/**
|
||||
* 在方法成功执行后打点,记录方法的执行时间发送到指标系统,默认关闭
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
boolean recordSuccessMetrics() default false;
|
||||
|
||||
/**
|
||||
* 在方法成功失败后打点,记录方法的执行时间发送到指标系统,默认关闭
|
||||
*/
|
||||
boolean recordFailMetrics() default false;
|
||||
|
||||
/**
|
||||
* 出现异常后忽略异常返回默认值,默认关闭
|
||||
*/
|
||||
boolean ignoreException() default false;
|
||||
|
||||
/**
|
||||
* 排除指定的请求参数
|
||||
*/
|
||||
public String[] excludeParamNames() default {};
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
package ai.remi.comm.log.aspect;
|
||||
|
||||
import ai.remi.comm.core.spring.SpringProperties;
|
||||
import ai.remi.comm.log.annotation.LogRecord;
|
||||
import ai.remi.comm.log.domain.OperateLog;
|
||||
import ai.remi.comm.log.enums.BusinessStatus;
|
||||
import ai.remi.comm.log.filter.PropertyPreExcludeFilter;
|
||||
import ai.remi.comm.log.service.AsyncLogService;
|
||||
import ai.remi.comm.redis.service.RedisService;
|
||||
import ai.remi.comm.util.ip.IpAddrUtils;
|
||||
import ai.remi.comm.util.ip.IpInfoUtils;
|
||||
import ai.remi.comm.util.string.StringUtils;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import ai.remi.comm.log.enums.RedisKeyEnum;
|
||||
import eu.bitwalker.useragentutils.UserAgent;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections.MapUtils;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.AfterReturning;
|
||||
import org.aspectj.lang.annotation.AfterThrowing;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.core.NamedThreadLocal;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 操作日志记录处理
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
public class LogRecordAspect {
|
||||
/**
|
||||
* 排除敏感属性字段
|
||||
*/
|
||||
public static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"};
|
||||
|
||||
private final ThreadLocal<Map<String, Object>> INFO_THREADLOCAL = new ThreadLocal<Map<String, Object>>();
|
||||
|
||||
/**
|
||||
* 计算操作消耗时间
|
||||
*/
|
||||
private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<>("Cost Time");
|
||||
|
||||
@Resource
|
||||
private AsyncLogService asyncLogService;
|
||||
|
||||
@Resource
|
||||
private RedisService redisService;
|
||||
|
||||
/**
|
||||
* 处理请求前执行
|
||||
*/
|
||||
@Before(value = "@annotation(logRecord)")
|
||||
public void boBefore(JoinPoint joinPoint, LogRecord logRecord) {
|
||||
TIME_THREADLOCAL.set(System.currentTimeMillis());
|
||||
//登出接口需要前置储存用户信息
|
||||
handleLog(joinPoint, logRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理完请求后执行
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
*/
|
||||
@AfterReturning(pointcut = "@annotation(logRecord)", returning = "jsonResult")
|
||||
public void doAfterReturning(JoinPoint joinPoint, LogRecord logRecord, Object jsonResult) {
|
||||
handleLog(joinPoint, logRecord, null, jsonResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截异常操作
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
* @param e 异常
|
||||
*/
|
||||
@AfterThrowing(value = "@annotation(logRecord)", throwing = "e")
|
||||
public void doAfterThrowing(JoinPoint joinPoint, LogRecord logRecord, Exception e) {
|
||||
handleLog(joinPoint, logRecord, e, null);
|
||||
}
|
||||
|
||||
protected void handleLog(final JoinPoint joinPoint, LogRecord logRecord) {
|
||||
// 传递线程信息
|
||||
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
RequestContextHolder.setRequestAttributes(requestAttributes, true);
|
||||
HttpServletRequest request = requestAttributes.getRequest();
|
||||
// 记录请求内容,threadInfo存储所有内容
|
||||
Map<String, Object> threadInfo = new HashMap<>();
|
||||
//该方法用于获取客户端发出请求时的完整URL,包括协议、服务器名、端口号、资源路径等信息,但不包括后面的查询参数部分。
|
||||
// 注意,getRequestRUL()方法返回的时StringBuffer类型,而不是String类型。
|
||||
threadInfo.put("url", request.getRequestURL());
|
||||
//该方法用于获取请求行中资源名称部分,即位于URL的主机和端口之后、参数部分之前的部分
|
||||
threadInfo.put("uri", request.getRequestURI());
|
||||
//该方法用于获取HTTP请求消息中的请求方式(如GET、POST等)
|
||||
threadInfo.put("httpMethod", request.getMethod());
|
||||
//该方法用于获取请求的协议名,例如http、https或ftp
|
||||
threadInfo.put("scheme", request.getScheme());
|
||||
//该方法用于获取请求客户端的IP地址,其格式类似于"192.168.0.1"
|
||||
//threadInfo.put("ip", request.getRemoteAddr());
|
||||
threadInfo.put("ip", IpAddrUtils.getIpAddr(request));
|
||||
//检查是否为内网IP
|
||||
String ipAddress = IpAddrUtils.internalIp(IpAddrUtils.getIpAddr(request)) ? "内网IP" : IpInfoUtils.getInfo(IpAddrUtils.getIpAddr(request)).getProvince() + "-" + IpInfoUtils.getInfo(IpAddrUtils.getIpAddr(request)).getCity();
|
||||
threadInfo.put("ipAddress", ipAddress);
|
||||
//该方法用于获取Content-Type头字段的值,结果为String类型
|
||||
threadInfo.put("contentType", request.getContentType());
|
||||
//包含了一个特征字符串,用来让网络协议的对端来识别发起请求的用户代理软件的应用类型、操作系统、软件开发商以及版本号。
|
||||
//UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
|
||||
UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
|
||||
threadInfo.put("platform", userAgent.getOperatingSystem().getName());
|
||||
threadInfo.put("browser", userAgent.getBrowser().getName());
|
||||
//从请求头获取操作人
|
||||
threadInfo.put("operator", request.getHeader("X-Unique-Id"));
|
||||
//从请求头获取部门租户
|
||||
threadInfo.put("deptTenantId", request.getHeader("X-Dept-Tenant-Id"));
|
||||
//从请求头获取公司租户
|
||||
threadInfo.put("companyTenantId", request.getHeader("X-Company-Tenant-Id"));
|
||||
//从请求头获取操作人
|
||||
String accessToken = request.getHeader("X-Access-Token");
|
||||
threadInfo.put("token", accessToken);
|
||||
threadInfo.put("userId", redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "id"));
|
||||
threadInfo.put("userCode", redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "userCode"));
|
||||
threadInfo.put("userName", redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "userName"));
|
||||
threadInfo.put("userNameEn", redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "userNameEn"));
|
||||
//从请求头获取系统语言
|
||||
threadInfo.put("language", request.getHeader("X-USER-LANGUAGE"));
|
||||
//从请求头获取业务流水号
|
||||
threadInfo.put("businessNo", request.getHeader("X-Business-No"));
|
||||
INFO_THREADLOCAL.set(threadInfo);
|
||||
}
|
||||
|
||||
protected void handleLog(final JoinPoint joinPoint, LogRecord logRecord, final Exception e, Object jsonResult) {
|
||||
try {
|
||||
// *========解析请求信息=========*//
|
||||
OperateLog operateLog = new OperateLog();
|
||||
|
||||
// *========获取注解信息=========*//
|
||||
//获取方法签名
|
||||
//MethodSignature signature = getSignature(joinPoint);
|
||||
//获取注解方法
|
||||
Method method = getMethod(joinPoint);
|
||||
//获取注解对象
|
||||
//LogRecord logRecord = getAnnotation(joinPoint, LogRecord.class);
|
||||
// 获取连接点所在类名
|
||||
String classname = joinPoint.getTarget().getClass().getSimpleName();
|
||||
// 获取连接点所在方法名称
|
||||
String methodName = method.getName();
|
||||
String name = String.format("%s.%s()", classname, methodName);
|
||||
// 保存请求方法
|
||||
operateLog.setRequestMethod(name);
|
||||
|
||||
// 处理设置注解上的参数
|
||||
getControllerMethodDescription(joinPoint, logRecord, operateLog, jsonResult);
|
||||
|
||||
// 是否需要保存request,参数和值
|
||||
if (logRecord.isSaveRequestData()) {
|
||||
// 获取参数的信息,传入到数据库中
|
||||
setRequestValue(joinPoint, operateLog, logRecord.excludeParamNames());
|
||||
}
|
||||
|
||||
// 是否需要保存response,参数和值
|
||||
if (logRecord.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {
|
||||
String result = JSON.toJSONString(jsonResult);
|
||||
// 对登录日志的特殊处理
|
||||
if (operateLog.getBusinessType()== 9) {
|
||||
Object data = JSON.parseObject(result).get("data");
|
||||
String accessToken = String.valueOf(JSON.parseObject(JSON.toJSONString(data)).get("accessToken"));
|
||||
setOperateUser(accessToken, operateLog);
|
||||
}
|
||||
operateLog.setStatus(String.valueOf(JSON.parseObject(result).get("code")));
|
||||
operateLog.setResponseBody(result);
|
||||
}
|
||||
|
||||
//请求的成功或失败
|
||||
if (e != null) {
|
||||
operateLog.setStatus(BusinessStatus.FAIL.getCode());
|
||||
// 是否需要保存errorMsg,参数和值
|
||||
if (logRecord.isSaveErrorInfo()) {
|
||||
operateLog.setStatus("111111");
|
||||
operateLog.setErrorInfo(StrUtil.sub(e.getMessage(), 0, 5000));
|
||||
}
|
||||
}
|
||||
|
||||
// 保存数据库
|
||||
//asyncLogService.saveOperateLog(operateLog);
|
||||
} catch (Exception exp) {
|
||||
// 记录本地异常日志
|
||||
log.error("异常信息:{}", exp.getMessage());
|
||||
exp.printStackTrace();
|
||||
} finally {
|
||||
TIME_THREADLOCAL.remove();
|
||||
INFO_THREADLOCAL.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注解中对方法的描述信息 用于Controller层注解
|
||||
*
|
||||
* @param logRecord 日志切面
|
||||
* @param operateLog 操作日志
|
||||
*/
|
||||
public void getControllerMethodDescription(JoinPoint joinPoint, LogRecord logRecord, OperateLog operateLog, Object jsonResult) throws Exception {
|
||||
Map<String, Object> objectMap = INFO_THREADLOCAL.get();
|
||||
// 业务模块
|
||||
operateLog.setAppId(SpringProperties.getString("log.record.appId"));
|
||||
operateLog.setAppCode(SpringProperties.getString("log.record.appCode"));
|
||||
// 操作方法名称或描述
|
||||
operateLog.setContent(logRecord.content());
|
||||
// 业务操作类型
|
||||
operateLog.setBusinessType(logRecord.businessType().getCode());
|
||||
// 操作人类别
|
||||
operateLog.setOperatorType(logRecord.operatorType().getCode());
|
||||
// 操作人IP
|
||||
operateLog.setUserIp(String.valueOf(objectMap.get("ip")));
|
||||
operateLog.setIpAddress(String.valueOf(objectMap.get("ipAddress")));
|
||||
// 操作人Id
|
||||
//operateLog.setUserId(Objects.nonNull(objectMap.get("operator")) ? String.valueOf(objectMap.get("operator")) : null);
|
||||
// 操作人信息
|
||||
operateLog.setUserId(Objects.nonNull(objectMap.get("userId")) ? String.valueOf(objectMap.get("userId")) : null);
|
||||
operateLog.setUserCode(Objects.nonNull(objectMap.get("userCode")) ? String.valueOf(objectMap.get("userCode")) : null);
|
||||
operateLog.setUserName(Objects.nonNull(objectMap.get("userName")) ? String.valueOf(objectMap.get("userName")) : null);
|
||||
operateLog.setUserNameEn(Objects.nonNull(objectMap.get("userNameEn")) ? String.valueOf(objectMap.get("userNameEn")) : null);
|
||||
// 用户系统语言
|
||||
operateLog.setUserLanguage(Objects.nonNull(objectMap.get("language")) ? String.valueOf(objectMap.get("language")) : null);
|
||||
// 请求地址
|
||||
operateLog.setRequestUrl(String.valueOf(objectMap.get("url")));
|
||||
// 请求类型
|
||||
operateLog.setRequestType(String.valueOf(objectMap.get("httpMethod")));
|
||||
// 请求报文类型
|
||||
operateLog.setContentType(Objects.nonNull(objectMap.get("contentType")) ? String.valueOf(objectMap.get("contentType")) : null);
|
||||
// 业务流水号
|
||||
operateLog.setBusinessNo(Objects.nonNull(objectMap.get("businessNo")) ? String.valueOf(objectMap.get("businessNo")) : null);
|
||||
// 链路追踪ID
|
||||
//logRecordDTO.setTraceId();
|
||||
// 浏览器、操作系统、UI引擎标识
|
||||
operateLog.setPlatform(String.valueOf(objectMap.get("platform")));
|
||||
operateLog.setBrowser(String.valueOf(objectMap.get("browser")));
|
||||
// 部门租户
|
||||
operateLog.setDeptTenantId(String.valueOf(objectMap.get("deptTenantId")));
|
||||
// 公司租户
|
||||
operateLog.setCompanyTenantId(String.valueOf(objectMap.get("companyTenantId")));
|
||||
// 设置消耗时间
|
||||
operateLog.setRequestTime(LocalDateTime.ofEpochSecond(TIME_THREADLOCAL.get() / 1000, 0, ZoneOffset.ofHours(8)));
|
||||
operateLog.setResponseTime(LocalDateTime.ofEpochSecond(System.currentTimeMillis() / 1000, 0, ZoneOffset.ofHours(8)));
|
||||
operateLog.setExecuteTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存操作人信息
|
||||
*
|
||||
* @param accessToken
|
||||
* @param operateLog
|
||||
*/
|
||||
private void setOperateUser(String accessToken, OperateLog operateLog) {
|
||||
if (StringUtils.isNotBlank(accessToken)) {
|
||||
String id = String.valueOf(redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "id"));
|
||||
operateLog.setUserId(id);
|
||||
String userCode = String.valueOf(redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "userCode"));
|
||||
operateLog.setUserCode(userCode);
|
||||
String userName = String.valueOf(redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "userName"));
|
||||
operateLog.setUserName(userName);
|
||||
String userNameEn = String.valueOf(redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "userNameEn"));
|
||||
operateLog.setUserNameEn(userNameEn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求的参数,放到log中
|
||||
*
|
||||
* @param operateLog 操作日志
|
||||
* @param excludeParamNames 排除指定的请求参数
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
private void setRequestValue(JoinPoint joinPoint, OperateLog operateLog, String[] excludeParamNames) throws Exception {
|
||||
// 获取RequestAttributes
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
// 从获取RequestAttributes中获取HttpServletRequest的信息
|
||||
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
|
||||
// 获取请求类型
|
||||
// String requestType = operateLog.getRequestType();
|
||||
String requestType = request.getMethod();
|
||||
// 获取请求参数
|
||||
Map<String, String> paramsMap = converMap(request.getParameterMap());
|
||||
// 组装请求参数
|
||||
if (MapUtils.isEmpty(paramsMap) && (HttpMethod.PUT.name().equals(requestType) || HttpMethod.POST.name().equals(requestType))) {
|
||||
String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
|
||||
operateLog.setRequestBody(params);
|
||||
} else {
|
||||
operateLog.setRequestBody(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求参数 转换
|
||||
*
|
||||
* @param paramMap request获取的参数数组
|
||||
*/
|
||||
public Map<String, String> converMap(Map<String, String[]> paramMap) {
|
||||
Map<String, String> returnMap = new HashMap<>();
|
||||
for (String key : paramMap.keySet()) {
|
||||
returnMap.put(key, paramMap.get(key)[0]);
|
||||
}
|
||||
return returnMap;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 参数拼装
|
||||
*/
|
||||
private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
|
||||
StringBuilder params = new StringBuilder();
|
||||
if (ArrayUtil.isNotEmpty(paramsArray)) {
|
||||
for (Object o : paramsArray) {
|
||||
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
|
||||
try {
|
||||
String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames));
|
||||
params.append(jsonObj).append(StrUtil.SPACE);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return params.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 忽略敏感属性
|
||||
*/
|
||||
public PropertyPreExcludeFilter excludePropertyPreFilter(String[] excludeParamNames) {
|
||||
return new PropertyPreExcludeFilter().addExcludes(ArrayUtil.addAll(EXCLUDE_PROPERTIES, excludeParamNames));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否需要过滤的对象
|
||||
*
|
||||
* @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.entrySet()) {
|
||||
Map.Entry entry = (Map.Entry) value;
|
||||
return entry.getValue() instanceof MultipartFile;
|
||||
}
|
||||
}
|
||||
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|
||||
|| o instanceof BindingResult;
|
||||
}
|
||||
|
||||
private MethodSignature getSignature(JoinPoint joinPoint) {
|
||||
//通过连接点获取方法签名
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
return signature;
|
||||
}
|
||||
|
||||
private Method getMethod(JoinPoint joinPoint) {
|
||||
//通过连接点获取方法签名
|
||||
MethodSignature signature = getSignature(joinPoint);
|
||||
//获取注解服务的方法
|
||||
Method method = signature.getMethod();
|
||||
return method;
|
||||
}
|
||||
|
||||
private <T extends Annotation> T getAnnotation(JoinPoint joinPoint, Class<T> annotationClass) {
|
||||
Method method = getMethod(joinPoint);
|
||||
//获取注解类
|
||||
T annotation = method.getAnnotation(annotationClass);
|
||||
return annotation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package ai.remi.comm.log.config;
|
||||
|
||||
import ai.remi.comm.log.aspect.LogRecordAspect;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
/**
|
||||
* @author DianJiu 【公众号 点九开源】
|
||||
* @email dianjiuxyz@gmail.com
|
||||
* @date 2022-07-19
|
||||
* @desc
|
||||
*/
|
||||
@Slf4j
|
||||
@RefreshScope
|
||||
@Configuration
|
||||
public class LogRecordAutoConfigure {
|
||||
/**
|
||||
* 是否开启监控配置参数
|
||||
*/
|
||||
@Value("${remi-log.record.enabled}")
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* 日志监控的应用名称
|
||||
*/
|
||||
@Value("${remi-log.record.appName}")
|
||||
private String appName;
|
||||
|
||||
/**
|
||||
* 在方法成功执行后打点,记录方法的执行时间发送到指标系统,默认关闭
|
||||
* 在方法成功失败后打点,记录方法的执行时间发送到指标系统,默认关闭
|
||||
*/
|
||||
@Value("${remi-log.record.pushUrl}")
|
||||
private String pushUrl;
|
||||
|
||||
|
||||
/**
|
||||
* 根据运行环境决定是否开启日志记录监控
|
||||
*/
|
||||
@PostConstruct
|
||||
protected LogRecordAspect init() {
|
||||
if(!enabled){
|
||||
return null;
|
||||
}
|
||||
if (enabled && StringUtils.isBlank(appName)) {
|
||||
log.error("Please config the remi-log.record property in application.yml file to enabled log record, but appName is null.");
|
||||
throw new RuntimeException("Please config the remi-log.record property in application.yml file to config log record appName.");
|
||||
}
|
||||
return new LogRecordAspect();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package ai.remi.comm.log.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.*;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 系统日志表
|
||||
* </p>
|
||||
*
|
||||
* @author 点九
|
||||
* @since 2022-07-20
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@ToString(callSuper = true)
|
||||
public class OperateLog implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -85509022761738038L;
|
||||
|
||||
/**
|
||||
* 业务模块,区分不同业务模块的应用ID
|
||||
*/
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 业务模块,区分不同业务模块的应用编码
|
||||
*/
|
||||
private String appCode;
|
||||
|
||||
/**
|
||||
* 操作方法名称或描述
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 请求状态 成功 or 失败
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 操作人类别
|
||||
*/
|
||||
private Integer operatorType;
|
||||
|
||||
/**
|
||||
* 业务操作类型
|
||||
*/
|
||||
private Integer businessType;
|
||||
|
||||
/**
|
||||
* 操作人Id
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 操作人编码
|
||||
*/
|
||||
private String userCode;
|
||||
|
||||
/**
|
||||
* 操作人员名称
|
||||
*/
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 操作人员英文名称
|
||||
*/
|
||||
private String userNameEn;
|
||||
|
||||
/**
|
||||
* 请求地址
|
||||
*/
|
||||
private String requestUrl;
|
||||
|
||||
/**
|
||||
* 请求方法
|
||||
*/
|
||||
private String requestMethod;
|
||||
|
||||
/**
|
||||
* 请求类型(POST、PUT、OPTIONS、DELETE)
|
||||
*/
|
||||
private String requestType;
|
||||
|
||||
/**
|
||||
* 请求报文类型(map、json)
|
||||
*/
|
||||
private String contentType;
|
||||
|
||||
/**
|
||||
* 请求报文
|
||||
*/
|
||||
private String requestBody;
|
||||
|
||||
/**
|
||||
* 响应报文
|
||||
*/
|
||||
private String responseBody;
|
||||
|
||||
/**
|
||||
* 异常信息
|
||||
*/
|
||||
private String errorInfo;
|
||||
|
||||
/**
|
||||
* 请求时间
|
||||
*/
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime requestTime;
|
||||
|
||||
/**
|
||||
* 响应时间
|
||||
*/
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime responseTime;
|
||||
|
||||
/**
|
||||
* 操作花费的时间 单位:ms
|
||||
*/
|
||||
private Long executeTime;
|
||||
|
||||
/**
|
||||
* 用户系统语言
|
||||
*/
|
||||
private String userLanguage;
|
||||
|
||||
/**
|
||||
* 业务流水号
|
||||
*/
|
||||
private String businessNo;
|
||||
|
||||
/**
|
||||
* 链路追踪ID
|
||||
*/
|
||||
private String traceId;
|
||||
|
||||
/**
|
||||
* 操作系统
|
||||
*/
|
||||
private String platform;
|
||||
|
||||
/**
|
||||
* 浏览器
|
||||
*/
|
||||
private String browser;
|
||||
|
||||
/**
|
||||
* 操作人IP
|
||||
*/
|
||||
private String userIp;
|
||||
|
||||
/**
|
||||
* IP归属地址
|
||||
*/
|
||||
private String ipAddress;
|
||||
|
||||
/**
|
||||
* 公司租户ID
|
||||
*/
|
||||
private String companyTenantId;
|
||||
|
||||
/**
|
||||
* 部门租户ID
|
||||
*/
|
||||
private String deptTenantId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package ai.remi.comm.log.enums;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 操作状态
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public enum BusinessStatus {
|
||||
/**
|
||||
* 成功
|
||||
*/
|
||||
SUCCESS("000000", "成功"),
|
||||
|
||||
/**
|
||||
* 失败
|
||||
*/
|
||||
FAIL("111111", "失败");
|
||||
|
||||
|
||||
private String code;
|
||||
|
||||
private String desc;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package ai.remi.comm.log.enums;
|
||||
|
||||
import ai.remi.comm.core.enums.TypeEnum;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 业务操作类型
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public enum BusinessType implements TypeEnum {
|
||||
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
INSERT(1, "新增"),
|
||||
|
||||
/**
|
||||
* 修改
|
||||
*/
|
||||
UPDATE(2, "修改"),
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
DELETE(3, "删除"),
|
||||
|
||||
/**
|
||||
* 导入
|
||||
*/
|
||||
IMPORT(4, "导入"),
|
||||
|
||||
/**
|
||||
* 导出
|
||||
*/
|
||||
EXPORT(5, "导出"),
|
||||
|
||||
/**
|
||||
* 下载
|
||||
*/
|
||||
DOWNLOAD(6, "下载"),
|
||||
|
||||
/**
|
||||
* 上传
|
||||
*/
|
||||
UPLOAD(7, "上传"),
|
||||
|
||||
/**
|
||||
* 授权
|
||||
*/
|
||||
GRANT(8, "授权"),
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
LOGIN(9, "登录"),
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
LOGOUT(10, "登出"),
|
||||
|
||||
/**
|
||||
* 强退
|
||||
*/
|
||||
FORCE(11, "强退"),
|
||||
|
||||
/**
|
||||
* 修改状态
|
||||
*/
|
||||
STATUS(12, "修改状态"),
|
||||
|
||||
/**
|
||||
* 生成代码
|
||||
*/
|
||||
GENCODE(13, "生成代码"),
|
||||
|
||||
/**
|
||||
* 清空数据
|
||||
*/
|
||||
CLEAN(14, "清空数据"),
|
||||
|
||||
/**
|
||||
* 更新缓存
|
||||
*/
|
||||
REFRESH(15, "更新缓存"),
|
||||
|
||||
/**
|
||||
* 其它
|
||||
*/
|
||||
OTHER(99, "其它");
|
||||
|
||||
private Integer code;
|
||||
|
||||
private String desc;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package ai.remi.comm.log.enums;
|
||||
|
||||
import ai.remi.comm.core.enums.TypeEnum;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 操作人类别
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public enum OperatorType implements TypeEnum {
|
||||
|
||||
/**
|
||||
* 后台用户
|
||||
*/
|
||||
MANAGE(1, "管理端用户"),
|
||||
|
||||
/**
|
||||
* 手机端用户
|
||||
*/
|
||||
MOBILE(2, "移动端用户"),
|
||||
|
||||
/**
|
||||
* 其它
|
||||
*/
|
||||
OTHER(99, "其它");
|
||||
|
||||
private Integer code;
|
||||
|
||||
private String desc;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package ai.remi.comm.log.enums;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Redis key枚举类
|
||||
*
|
||||
* @author dianjiu
|
||||
* @date 2021-12-7 16:10:35
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum RedisKeyEnum {
|
||||
/**
|
||||
* 客户登录token,24小时失效 1:{} = customerId 2:{} = token 使用方式 RedisKeysEnum.CUSTOMER_LOGIN_TOKEN.getKey(customerId,token)
|
||||
*/
|
||||
APP_CODE("sdt-saas-iam:app-code:{}:{}"),
|
||||
USER_INFO("sdt-saas-iam:user-info:{}"),
|
||||
TICKET("sdt-saas-iam:ticket:{}"),
|
||||
AUTH_CODE("sdt-saas-iam:auth-code:{}"),
|
||||
ACCESS_TOKEN("sdt-saas-iam:access-token:{}"),
|
||||
REFRESH_TOKEN("sdt-saas-iam:refresh-token:{}");
|
||||
|
||||
|
||||
/**
|
||||
* redis key
|
||||
*/
|
||||
private String key;
|
||||
|
||||
/**
|
||||
* key失效时间,单位秒
|
||||
*/
|
||||
//private Long expireAt;
|
||||
public String getKey(String... key) {
|
||||
return StrUtil.format(this.key, key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package ai.remi.comm.log.filter;
|
||||
|
||||
import com.alibaba.fastjson2.filter.SimplePropertyPreFilter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 排除JSON敏感属性
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
public class PropertyPreExcludeFilter extends SimplePropertyPreFilter {
|
||||
|
||||
public PropertyPreExcludeFilter addExcludes(String... filters) {
|
||||
for (String filter : filters) {
|
||||
this.getExcludes().add(filter);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package ai.remi.comm.log.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 异步调用日志服务
|
||||
*/
|
||||
@Service
|
||||
public class AsyncLogService {
|
||||
|
||||
/**
|
||||
* 通过OpenFeign客户端注入
|
||||
*/
|
||||
//@Resource
|
||||
//private SysClientApi sysClientApi;
|
||||
|
||||
/**
|
||||
* 保存系统日志记录
|
||||
*/
|
||||
//@Async
|
||||
//public void saveOperateLog(OperateLog operateLog) {
|
||||
//CompanyAuthInfo companyAuthInfo = new CompanyAuthInfo();
|
||||
//companyAuthInfo.setDeptTenantId(operateLog.getDeptTenantId());
|
||||
//companyAuthInfo.setCompanyTenantId(operateLog.getCompanyTenantId());
|
||||
//RequestHolder.add(companyAuthInfo);
|
||||
//LogRecordPostDTO logRecordPostDTO = BeanCopyUtils.copyPropertiesThird(operateLog, LogRecordPostDTO.class);
|
||||
//sysClientApi.addLogRecord(logRecordPostDTO);
|
||||
//}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
ai.remi.comm.log.config.LogRecordAutoConfigure,\
|
||||
ai.remi.comm.log.aspect.LogRecordAspect,\
|
||||
ai.remi.comm.log.service.AsyncLogService
|
||||
Reference in New Issue
Block a user