feat(log): 三类日志大字段加写入时 size 列,按 50KB 门槛切换在线预览/下载
build / build-push (push) Successful in 54s

- push_record/oplog/oapilog 各加 payload_size/response_size(写时算 UTF-8 字节数),列表只读该整数列、不关联大字段
- 去掉 oplog/oapilog 响应写时截断(全量 GZIP 入库),退役 maxResponseSize 注解属性与常量
- 删除 oapilog 列表逐行 findById+解压回填的 N+1,SysOapiLogListVo 去掉 responseData
- 附三个 resources/sql 迁移(DBA 手动执行,含历史行回填)
This commit is contained in:
chenxing
2026-09-18 09:24:21 +08:00
parent 8799d72d47
commit ef5849377a
17 changed files with 85 additions and 44 deletions
@@ -30,6 +30,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -77,18 +78,17 @@ public class OapiLogAspect {
SysOapiLog entity = buildEntity(point, request, httpMethod, oapiLog);
boolean saveResponse = oapiLog != null ? oapiLog.saveResponse() : true;
int maxResponseSize = oapiLog != null ? oapiLog.maxResponseSize() : 10240;
Object result = null;
try {
result = point.proceed();
entity.setStatusCode(200);
fillResponse(entity, result, saveResponse, maxResponseSize);
fillResponse(entity, result, saveResponse);
} catch (Throwable e) {
entity.setStatusCode(500);
entity.setErrorMsg(truncate(e.getMessage(), 2000));
// 业务异常(DException/BizException)同样构造实际返回的响应体落库,便于详情下载
fillErrorResponse(entity, e, saveResponse, maxResponseSize);
fillErrorResponse(entity, e, saveResponse);
throw e;
} finally {
entity.setCostTime(System.currentTimeMillis() - start);
@@ -216,20 +216,20 @@ public class OapiLogAspect {
}
/**
* 写入响应结果(脱敏 → 截断 → GZIP)。
* 写入响应结果(脱敏 → GZIP,不截断)。
*/
private void fillResponse(SysOapiLog entity, Object result, boolean saveResponse, int maxResponseSize) {
private void fillResponse(SysOapiLog entity, Object result, boolean saveResponse) {
if (!saveResponse || result == null) {
return;
}
writeResponseData(entity, result, maxResponseSize);
writeResponseData(entity, result);
}
/**
* 异常路径:与 {@code GlobalExceptionHandler} 保持一致的响应体(DException/BizException → ApiResponse),
* 使日志下载内容等于调用方实际收到的 body;其余异常不构造(响应不可预期)。
*/
private void fillErrorResponse(SysOapiLog entity, Throwable e, boolean saveResponse, int maxResponseSize) {
private void fillErrorResponse(SysOapiLog entity, Throwable e, boolean saveResponse) {
if (!saveResponse) {
return;
}
@@ -237,7 +237,7 @@ public class OapiLogAspect {
if (response == null) {
return;
}
writeResponseData(entity, response, maxResponseSize);
writeResponseData(entity, response);
}
private ApiResponse<?> toApiResponse(Throwable e) {
@@ -252,7 +252,7 @@ public class OapiLogAspect {
return null;
}
private void writeResponseData(SysOapiLog entity, Object payload, int maxResponseSize) {
private void writeResponseData(SysOapiLog entity, Object payload) {
if (payload == null) {
return;
}
@@ -261,10 +261,8 @@ public class OapiLogAspect {
if (resp == null) {
return;
}
if (resp.length() > maxResponseSize) {
resp = resp.substring(0, maxResponseSize) + "...[截断]";
}
entity.setResponseData(GzipUtil.compress(resp));
entity.setResponseSize(resp.getBytes(StandardCharsets.UTF_8).length);
} catch (Exception e) {
log.warn("开放接口日志响应序列化失败, uri={}", entity.getRequestUri(), e);
}
@@ -23,6 +23,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@@ -45,9 +46,6 @@ public class OperationLogAspect {
/** 不记录日志查询/下载接口自身,避免递归与刷屏。 */
private static final String LOG_CONTROLLER = "com.yunhai.userbase.controller.api.SysOperationLogController";
/** GET 自动记录时使用的默认响应保存上限(10KB)。 */
private static final int DEFAULT_MAX_RESPONSE_SIZE = 10240;
@Autowired
private ISysOperationLogService sysOperationLogService;
@@ -76,13 +74,12 @@ public class OperationLogAspect {
// GET 未标注注解时使用默认参数;标注了则优先用注解配置
boolean saveResponse = operationLog != null ? operationLog.saveResponse() : true;
int maxResponseSize = operationLog != null ? operationLog.maxResponseSize() : DEFAULT_MAX_RESPONSE_SIZE;
Object result = null;
try {
result = point.proceed();
entity.setStatusCode(200);
fillResponse(entity, result, saveResponse, maxResponseSize);
fillResponse(entity, result, saveResponse);
} catch (Throwable e) {
entity.setStatusCode(500);
entity.setErrorMsg(truncate(e.getMessage(), 2000));
@@ -174,18 +171,16 @@ public class OperationLogAspect {
}
/**
* 写入响应结果(GZIP 压缩 + 截断)。
* 写入响应结果(全量 GZIP 压缩入库,不再截断)。
*/
private void fillResponse(SysOperationLog entity, Object result, boolean saveResponse, int maxResponseSize) {
private void fillResponse(SysOperationLog entity, Object result, boolean saveResponse) {
if (!saveResponse || result == null) {
return;
}
try {
String resp = JSON.toJSONString(result);
if (resp.length() > maxResponseSize) {
resp = resp.substring(0, maxResponseSize) + "...[截断]";
}
entity.setResponseData(GzipUtil.compress(resp));
entity.setResponseSize(resp.getBytes(StandardCharsets.UTF_8).length);
} catch (Exception e) {
log.warn("操作日志响应结果序列化失败, uri={}", entity.getRequestUri(), e);
}
@@ -32,9 +32,4 @@ public @interface OapiLog {
* 是否保存响应结果(GZIP 压缩后入库)。
*/
boolean saveResponse() default true;
/**
* 响应结果序列化后的最大字符数,超过则截断。默认 10KB。
*/
int maxResponseSize() default 10240;
}
@@ -27,9 +27,4 @@ public @interface OperationLog {
* 是否保存响应结果(GZIP 压缩后入库)。
*/
boolean saveResponse() default true;
/**
* 响应结果序列化后的最大字符数,超过则截断。默认 10KB。
*/
int maxResponseSize() default 10240;
}
@@ -114,6 +114,12 @@ public class SysOapiLog implements Serializable {
@Column(name = "response_data", columnDefinition = "MEDIUMBLOB")
private byte[] responseData;
/**
* 响应原文(GZIP 前)UTF-8 字节数,写入时算好,列表按此列切换在线预览/下载,避免加载 response_data 大字段。
*/
@Column(name = "response_size")
private Integer responseSize;
/**
* HTTP状态码
*/
@@ -106,6 +106,12 @@ public class SysOperationLog implements Serializable {
@Column(name = "response_data", columnDefinition = "MEDIUMBLOB")
private byte[] responseData;
/**
* 响应原文(GZIP 前)UTF-8 字节数,写入时算好,列表按此列切换在线预览/下载,避免加载 response_data 大字段。
*/
@Column(name = "response_size")
private Integer responseSize;
/**
* HTTP状态码
*/
@@ -38,8 +38,6 @@ public class SysOapiLogListVo {
private String operationDesc;
/** 请求参数JSON(脱敏后) */
private String requestParams;
/** 响应结果(GZIP 解压后原文,脱敏后;随列表带出,详情弹窗直接展示) */
private String responseData;
/** HTTP 状态码 */
private Integer statusCode;
/** 耗时(ms) */
@@ -52,6 +50,8 @@ public class SysOapiLogListVo {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
/** 响应原文字节数(用于前端按大小切换在线预览/下载,response_data 原文不进列表) */
private Integer responseSize;
/**
* 全参构造,顺序需与 {@code SysOapiLog} 的列表投影字段一致。<br>
@@ -61,7 +61,8 @@ public class SysOapiLogListVo {
String requestIp, String requestUri, String requestMethod,
String operationType, String classMethod, String operationDesc,
String requestParams, Integer statusCode, Long costTime,
String userAgent, String errorMsg, LocalDateTime createTime) {
String userAgent, String errorMsg, LocalDateTime createTime,
Integer responseSize) {
this.id = id;
this.logLevel = logLevel;
this.callerAppKey = callerAppKey;
@@ -78,5 +79,6 @@ public class SysOapiLogListVo {
this.userAgent = userAgent;
this.errorMsg = errorMsg;
this.createTime = createTime;
this.responseSize = responseSize;
}
}
@@ -48,6 +48,8 @@ public class SysOperationLogListVo {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
/** 响应原文字节数(用于前端按大小切换在线预览/下载,response_data 原文不进列表) */
private Integer responseSize;
/**
* 全参构造,顺序需与 {@code SysOperationLog} 的列表投影字段一致。<br>
@@ -57,7 +59,7 @@ public class SysOperationLogListVo {
String requestIp, String requestUri, String requestMethod,
String operationType, String classMethod, String requestParams,
Integer statusCode, Long costTime, String userAgent,
String errorMsg, LocalDateTime createTime) {
String errorMsg, LocalDateTime createTime, Integer responseSize) {
this.id = id;
this.logLevel = logLevel;
this.userId = userId;
@@ -73,5 +75,6 @@ public class SysOperationLogListVo {
this.userAgent = userAgent;
this.errorMsg = errorMsg;
this.createTime = createTime;
this.responseSize = responseSize;
}
}
@@ -42,6 +42,10 @@ public class PushRecordEntity {
@Column(name = "payload", columnDefinition = "MEDIUMTEXT")
private String payload;
/** 请求体 UTF-8 字节数(写入时算好,列表按此列切换在线预览/下载,避免查询 payload 大字段)。 */
@Column(name = "payload_size")
private Integer payloadSize;
@Column(name = "sign_t")
private String signT;
@@ -46,6 +46,8 @@ public class PushRecordListVo {
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
/** 请求体字节数(用于前端按大小切换在线预览/下载,payload 原文不进列表) */
private Integer payloadSize;
/**
* 全参构造,顺序需与 {@code PushRecordEntity} 的列表投影字段一致。<br>
@@ -54,7 +56,7 @@ public class PushRecordListVo {
public PushRecordListVo(Long id, Integer subscriberAppKey, String appName, String interfacePath,
String interfaceLabel, String requestUrl, String signT, Integer httpStatus,
Integer respCode, String respMsg, Integer success, Integer attemptNo,
Integer costMs, String triggerType, LocalDateTime createdAt) {
Integer costMs, String triggerType, LocalDateTime createdAt, Integer payloadSize) {
this.id = id;
this.subscriberAppKey = subscriberAppKey;
this.appName = appName;
@@ -70,5 +72,6 @@ public class PushRecordListVo {
this.costMs = costMs;
this.triggerType = triggerType;
this.createdAt = createdAt;
this.payloadSize = payloadSize;
}
}
@@ -73,7 +73,7 @@ public class SysOapiLogServiceImpl implements ISysOapiLogService {
root.get("requestIp"), root.get("requestUri"), root.get("requestMethod"),
root.get("operationType"), root.get("classMethod"), root.get("operationDesc"),
root.get("requestParams"), root.get("statusCode"), root.get("costTime"),
root.get("userAgent"), root.get("errorMsg"), root.get("createTime")));
root.get("userAgent"), root.get("errorMsg"), root.get("createTime"), root.get("responseSize")));
List<Predicate> predicates = buildPredicates(cb, root, query);
cq.where(predicates.toArray(new Predicate[0]));
cq.orderBy(cb.desc(root.get("createTime")));
@@ -89,10 +89,6 @@ public class SysOapiLogServiceImpl implements ISysOapiLogService {
typed.setFirstResult((pageIndex - 1) * pageSize);
typed.setMaxResults(pageSize);
List<SysOapiLogListVo> content = typed.getResultList();
// 响应结果随列表带出(GZIP 解压后),供详情弹窗直接展示,无需再单独请求
for (SysOapiLogListVo vo : content) {
vo.setResponseData(getResponseData(vo.getId()));
}
int totalPages = (int) Math.ceil((double) total / pageSize);
boolean hasNext = pageIndex < totalPages;
@@ -74,7 +74,7 @@ public class SysOperationLogServiceImpl implements ISysOperationLogService {
root.get("requestIp"), root.get("requestUri"), root.get("requestMethod"),
root.get("operationType"), root.get("classMethod"), root.get("requestParams"),
root.get("statusCode"), root.get("costTime"), root.get("userAgent"),
root.get("errorMsg"), root.get("createTime")));
root.get("errorMsg"), root.get("createTime"), root.get("responseSize")));
List<Predicate> predicates = buildPredicates(cb, root, query);
cq.where(predicates.toArray(new Predicate[0]));
cq.orderBy(cb.desc(root.get("createTime")));
@@ -25,6 +25,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
@@ -100,6 +101,8 @@ public class PushConsumer {
rec.setInterfaceLabel(msg.getInterfaceLabel());
rec.setRequestUrl(url == null ? "" : url);
rec.setPayload(msg.getBody());
rec.setPayloadSize(msg.getBody() == null ? 0
: msg.getBody().getBytes(StandardCharsets.UTF_8).length);
rec.setSignT(msg.getT());
rec.setHttpStatus(httpStatus);
rec.setRespCode(respCode);
@@ -66,7 +66,7 @@ public class PushRecordService {
root.get("interfacePath"), root.get("interfaceLabel"), root.get("requestUrl"),
root.get("signT"), root.get("httpStatus"), root.get("respCode"), root.get("respMsg"),
root.get("success"), root.get("attemptNo"), root.get("costMs"),
root.get("triggerType"), root.get("createdAt")));
root.get("triggerType"), root.get("createdAt"), root.get("payloadSize")));
List<Predicate> predicates = buildPredicates(cb, root, q);
cq.where(predicates.toArray(new Predicate[0]));
cq.orderBy(buildOrders(cb, root, pageable.getSort()));
@@ -0,0 +1,11 @@
-- 为 push_record 表新增 payload_size 字段:写入时记录请求体 UTF-8 字节数,
-- 供「推送日志」列表按大小切换在线预览/下载(列表只读该整数列,不再关联 payload 大字段)。
-- 执行方式:由 DBA 手动在对应环境执行,AI 不代跑。
-- 说明:dev 环境 spring.jpa.hibernate.ddl-auto=update 会自动建列,此时可跳过 ALTER、只跑回填。
ALTER TABLE push_record
ADD COLUMN payload_size INT NULL COMMENT '请求体UTF-8字节数(写入时算,列表按此切换预览/下载)';
-- 回填历史行:LENGTH() 对 utf8mb4 MEDIUMTEXT 返回字节数,与下载体积一致。
-- 大表回填会读 off-page blob,建议低峰分批执行。
UPDATE push_record SET payload_size = LENGTH(payload) WHERE payload_size IS NULL;
@@ -0,0 +1,12 @@
-- 为 sys_oapi_log 表新增 response_size 字段:写入时记录响应原文(GZIP 前)UTF-8 字节数,
-- 供「开放接口日志」列表按大小切换在线预览/下载(列表只读该整数列,不再关联 response_data 大字段)。
-- 同时本次移除了写时截断(响应改为全量 GZIP 入库),并删除了列表查询里逐行解压回填的 N+1。
-- 执行方式:由 DBA 手动在对应环境执行,AI 不代跑。
-- 说明:dev 环境 spring.jpa.hibernate.ddl-auto=update 会自动建列,此时可跳过 ALTER、只跑回填。
ALTER TABLE sys_oapi_log
ADD COLUMN response_size INT NULL COMMENT '响应原文GZIP前UTF-8字节数(写入时算,列表按此切换预览/下载)';
-- 回填历史行:老数据 response_data 均为截断后(≤10KB)再 GZIP,LENGTH() 取的是压缩字节数(低估),
-- 但老行解压后本就远小于 50KB 预览阈值,按可预览处理安全;新行由切面写入精确值。
UPDATE sys_oapi_log SET response_size = LENGTH(response_data) WHERE response_size IS NULL;
@@ -0,0 +1,12 @@
-- 为 sys_operation_log 表新增 response_size 字段:写入时记录响应原文(GZIP 前)UTF-8 字节数,
-- 供「操作日志」列表按大小切换在线预览/下载(列表只读该整数列,不再关联 response_data 大字段)。
-- 同时本次移除了写时截断(响应改为全量 GZIP 入库)。
-- 执行方式:由 DBA 手动在对应环境执行,AI 不代跑。
-- 说明:dev 环境 spring.jpa.hibernate.ddl-auto=update 会自动建列,此时可跳过 ALTER、只跑回填。
ALTER TABLE sys_operation_log
ADD COLUMN response_size INT NULL COMMENT '响应原文GZIP前UTF-8字节数(写入时算,列表按此切换预览/下载)';
-- 回填历史行:老数据 response_data 均为截断后(≤10KB)再 GZIP,LENGTH() 取的是压缩字节数(低估),
-- 但老行解压后本就远小于 50KB 预览阈值,按可预览处理安全;新行由切面写入精确值。
UPDATE sys_operation_log SET response_size = LENGTH(response_data) WHERE response_size IS NULL;