perf: 优化日志保留与分页查询
- 降低普通只读请求的日志写入并保留敏感 GET 审计 - 增加数据库分批清理、时间索引和文件滚动容量上限 - 增加近 30 天筛选、稳定倒序和分页大小限制
This commit is contained in:
@@ -26,7 +26,9 @@ import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 记录管理端 Controller 操作结果及最小必要请求摘要。
|
||||
@@ -43,20 +45,38 @@ public class LogAspect {
|
||||
private static final String SKILL_API_PREFIX = "/api/v1/skill";
|
||||
private static final List<String> SKILL_IDENTIFIER_KEYS =
|
||||
List.of("id", "resourceId", "skillId", "sourceId");
|
||||
private static final Set<String> READ_ONLY_HTTP_METHODS =
|
||||
Set.of("GET", "HEAD", "OPTIONS");
|
||||
|
||||
private final WriteLogMapper logService;
|
||||
private final LogRecordProperties config;
|
||||
|
||||
/**
|
||||
* 创建操作日志切面。
|
||||
*
|
||||
* @param logService 操作日志写入 Mapper
|
||||
* @param config 操作日志配置
|
||||
*/
|
||||
public LogAspect(WriteLogMapper logService, LogRecordProperties config) {
|
||||
this.logService = logService;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配管理端控制器与通用增删改查控制器方法。
|
||||
*/
|
||||
@Pointcut("within(@org.springframework.web.bind.annotation.RestController *) " +
|
||||
"|| execution(* tech.easyflow.common.web.controller.BaseCurdController.*(..))")
|
||||
public void pointcut() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行业务方法并按配置持久化操作日志。
|
||||
*
|
||||
* @param proceedingJoinPoint 当前连接点
|
||||
* @return 业务方法返回值
|
||||
* @throws Throwable 业务方法或日志写入异常
|
||||
*/
|
||||
@Around("pointcut()")
|
||||
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
@@ -72,6 +92,10 @@ public class LogAspect {
|
||||
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
|
||||
Class<?> controllerClass = signature.getDeclaringType();
|
||||
Method method = signature.getMethod();
|
||||
LogRecord logRecord = method.getAnnotation(LogRecord.class);
|
||||
if (!shouldRecordRequest(request, logRecord)) {
|
||||
return proceedingJoinPoint.proceed();
|
||||
}
|
||||
String params = getRequestParamsString(request);
|
||||
Throwable actionFailure = null;
|
||||
|
||||
@@ -82,7 +106,6 @@ public class LogAspect {
|
||||
throw throwable;
|
||||
} finally {
|
||||
WriteLog sysLog = new WriteLog();
|
||||
LogRecord logRecord = method.getAnnotation(LogRecord.class);
|
||||
if (StpUtil.isLogin()) {
|
||||
|
||||
BigInteger accountId = SaTokenUtil.getLoginAccount().getId();
|
||||
@@ -103,6 +126,22 @@ public class LogAspect {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前请求是否需要写入操作日志。
|
||||
*
|
||||
* @param request 当前 HTTP 请求
|
||||
* @param logRecord 方法上的显式日志标记
|
||||
* @return 需要记录返回 {@code true}
|
||||
*/
|
||||
private boolean shouldRecordRequest(HttpServletRequest request, LogRecord logRecord) {
|
||||
if (config.isRecordReadActions() || logRecord != null) {
|
||||
return true;
|
||||
}
|
||||
String httpMethod = request.getMethod();
|
||||
return httpMethod == null
|
||||
|| !READ_ONLY_HTTP_METHODS.contains(httpMethod.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化操作日志;业务动作已失败时,日志异常不得覆盖原始异常。
|
||||
*
|
||||
@@ -123,6 +162,13 @@ public class LogAspect {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日志动作名称。
|
||||
*
|
||||
* @param logRecord 显式日志标记
|
||||
* @param method 当前控制器方法
|
||||
* @return 日志动作名称
|
||||
*/
|
||||
private String buildActionName(LogRecord logRecord, Method method) {
|
||||
if (logRecord != null && StringUtil.hasText(logRecord.value())) {
|
||||
return logRecord.value();
|
||||
@@ -199,6 +245,12 @@ public class LogAspect {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取并限制请求查询参数,避免超长内容进入数据库。
|
||||
*
|
||||
* @param request 当前 HTTP 请求
|
||||
* @return 有界查询参数文本
|
||||
*/
|
||||
private String getRequestParamsString(HttpServletRequest request) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
Enumeration<String> e = request.getParameterNames();
|
||||
|
||||
@@ -3,17 +3,182 @@ package tech.easyflow.log;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 操作日志记录与保留策略配置。
|
||||
*/
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "easyflow.log-record")
|
||||
public class LogRecordProperties {
|
||||
|
||||
/**
|
||||
* 仅记录此前缀下的请求;为空时不限制请求路径。
|
||||
*/
|
||||
private String recordActionPrefix;
|
||||
|
||||
/**
|
||||
* 是否记录未显式标注的只读请求。
|
||||
*/
|
||||
private boolean recordReadActions;
|
||||
|
||||
/**
|
||||
* 数据库操作日志保留策略。
|
||||
*/
|
||||
private final Retention retention = new Retention();
|
||||
|
||||
/**
|
||||
* 获取请求路径前缀。
|
||||
*
|
||||
* @return 请求路径前缀
|
||||
*/
|
||||
public String getRecordActionPrefix() {
|
||||
return recordActionPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求路径前缀。
|
||||
*
|
||||
* @param recordActionPrefix 请求路径前缀
|
||||
*/
|
||||
public void setRecordActionPrefix(String recordActionPrefix) {
|
||||
this.recordActionPrefix = recordActionPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否记录未显式标注的只读请求。
|
||||
*
|
||||
* @return 是否记录只读请求
|
||||
*/
|
||||
public boolean isRecordReadActions() {
|
||||
return recordReadActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否记录未显式标注的只读请求。
|
||||
*
|
||||
* @param recordReadActions 是否记录只读请求
|
||||
*/
|
||||
public void setRecordReadActions(boolean recordReadActions) {
|
||||
this.recordReadActions = recordReadActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库操作日志保留策略。
|
||||
*
|
||||
* @return 保留策略
|
||||
*/
|
||||
public Retention getRetention() {
|
||||
return retention;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库操作日志保留策略。
|
||||
*/
|
||||
public static class Retention {
|
||||
|
||||
private static final int MAX_BATCH_SIZE = 10_000;
|
||||
|
||||
/**
|
||||
* 是否启用自动清理。
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* 在线日志保留天数。
|
||||
*/
|
||||
private int days = 90;
|
||||
|
||||
/**
|
||||
* 单批清理行数。
|
||||
*/
|
||||
private int batchSize = 5_000;
|
||||
|
||||
/**
|
||||
* 单次调度最多清理批次。
|
||||
*/
|
||||
private int maxBatches = 20;
|
||||
|
||||
/**
|
||||
* 判断是否启用自动清理。
|
||||
*
|
||||
* @return 是否启用
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否启用自动清理。
|
||||
*
|
||||
* @param enabled 是否启用
|
||||
*/
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在线日志保留天数。
|
||||
*
|
||||
* @return 保留天数
|
||||
*/
|
||||
public int getDays() {
|
||||
return days;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置在线日志保留天数。
|
||||
*
|
||||
* @param days 保留天数,必须大于零
|
||||
* @throws IllegalArgumentException 保留天数不合法时抛出
|
||||
*/
|
||||
public void setDays(int days) {
|
||||
if (days < 1) {
|
||||
throw new IllegalArgumentException("日志保留天数必须大于零");
|
||||
}
|
||||
this.days = days;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单批清理行数。
|
||||
*
|
||||
* @return 单批清理行数
|
||||
*/
|
||||
public int getBatchSize() {
|
||||
return batchSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单批清理行数。
|
||||
*
|
||||
* @param batchSize 单批清理行数
|
||||
* @throws IllegalArgumentException 批次大小不在允许范围内时抛出
|
||||
*/
|
||||
public void setBatchSize(int batchSize) {
|
||||
if (batchSize < 1 || batchSize > MAX_BATCH_SIZE) {
|
||||
throw new IllegalArgumentException("日志清理批次大小必须在 1 到 10000 之间");
|
||||
}
|
||||
this.batchSize = batchSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单次调度最多清理批次。
|
||||
*
|
||||
* @return 最大批次数
|
||||
*/
|
||||
public int getMaxBatches() {
|
||||
return maxBatches;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单次调度最多清理批次。
|
||||
*
|
||||
* @param maxBatches 最大批次数,必须大于零
|
||||
* @throws IllegalArgumentException 最大批次数不合法时抛出
|
||||
*/
|
||||
public void setMaxBatches(int maxBatches) {
|
||||
if (maxBatches < 1) {
|
||||
throw new IllegalArgumentException("日志清理最大批次数必须大于零");
|
||||
}
|
||||
this.maxBatches = maxBatches;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user