build / build-push (push) Successful in 1m34s
- import 全面切换 com.yunhai.platform.openapi.base.*(对齐 Nexus -57 SDK)
- MsgCenter 代理三件(Properties/Controller/ProxyService)+ 测试 11/11(09-19 消息中心二期本地完成态)
- Dockerfile 已在 563edb5 去掉写死 profile;本提交补齐源码侧遗漏
135 lines
5.2 KiB
Java
135 lines
5.2 KiB
Java
package com.yunhai.userhome.loadtestmetrics;
|
|
|
|
/* =============================================================================
|
|
* [LOADTEST-METRICS] 临时埋点 —— 《压测与oapi埋点统计方案.md》阶段二(方案 §3.2)
|
|
* 压测结束后整体移除:删除 loadtestmetrics 包 + 回退 pom.xml/application.yml
|
|
* 中 [LOADTEST-METRICS] 标记段 + DROP TABLE api_metrics。
|
|
* 移除步骤详见仓库根《埋点移除清单.md》。
|
|
* ============================================================================= */
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.aspectj.lang.ProceedingJoinPoint;
|
|
import org.aspectj.lang.annotation.Around;
|
|
import org.aspectj.lang.annotation.Aspect;
|
|
import org.springframework.stereotype.Component;
|
|
import org.springframework.web.context.request.RequestContextHolder;
|
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
|
|
|
import javax.servlet.http.HttpServletRequest;
|
|
|
|
/**
|
|
* [LOADTEST-METRICS] 出站 UserBase oapi 调用计数(direction=out,方案 §3.2)。<br>
|
|
* 切在 yh-openapi-client 的 {@code YschOpenCall}(SDK 重构后所有出站开放接口调用的唯一
|
|
* 公共入口,Spring Bean)上:YschBaseOpenApi 静态门面、OpenApiUserClient / OpenApiAppClient
|
|
* 注入式调用全部经它发出,一处切面覆盖全部出站,不改动任何业务 Service 代码。<br>
|
|
* 入站请求数(direction=in)与出站调用数(direction=out)相除即「放大系数」,
|
|
* 是缓存层次决策的直接依据(方案 §5)。
|
|
*/
|
|
@Slf4j
|
|
@Aspect
|
|
@Component
|
|
public class OutboundOapiMetricsAspect {
|
|
|
|
private static final int MAX_URI_LEN = 200;
|
|
|
|
private final ApiMetricsCollector collector;
|
|
|
|
public OutboundOapiMetricsAspect(ApiMetricsCollector collector) {
|
|
this.collector = collector;
|
|
}
|
|
|
|
/**
|
|
* 拦截 YschOpenCall 全部公开调用方法(get/getList/post/postMultipart/download)。<br>
|
|
* 按名重载第 2 参均为 path(如 /oapi/app/list),据此归一化记 uri。
|
|
*/
|
|
@Around("execution(* com.yunhai.platform.openapi.core.YschOpenCall.*(..))")
|
|
public Object around(ProceedingJoinPoint point) throws Throwable {
|
|
String uri = resolveUri(point);
|
|
String method = resolveHttpMethod(point.getSignature().getName());
|
|
boolean loadTest = inheritLoadTestFlag();
|
|
|
|
long start = System.currentTimeMillis();
|
|
boolean error = false;
|
|
try {
|
|
return point.proceed();
|
|
} catch (Throwable e) {
|
|
error = true;
|
|
throw e;
|
|
} finally {
|
|
try {
|
|
collector.record(uri, method, ApiMetricsEntity.DIRECTION_OUT,
|
|
System.currentTimeMillis() - start, error, loadTest);
|
|
} catch (Exception recordEx) {
|
|
// 埋点自身绝不影响出站调用
|
|
log.warn("[LOADTEST-METRICS] 出站计数失败: {}", recordEx.getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 方法名 → HTTP 方法(getJson/get→GET,postJson/post/postMultipart→POST) */
|
|
private String resolveHttpMethod(String signatureName) {
|
|
return signatureName.startsWith("post") ? "POST" : "GET";
|
|
}
|
|
|
|
/** 取第 2 参 path 归一化;防御签名变化时记方法名兜底,不抛错 */
|
|
private String resolveUri(ProceedingJoinPoint point) {
|
|
try {
|
|
Object[] args = point.getArgs();
|
|
if (args.length >= 2 && args[1] instanceof String) {
|
|
return normalize((String) args[1]);
|
|
}
|
|
} catch (Exception ignore) {
|
|
// 兜底
|
|
}
|
|
return "oapi:" + point.getSignature().getName();
|
|
}
|
|
|
|
/** 出站 path 归一化:保证前导 /、纯数字段归一为 {id}、截断 200 */
|
|
private String normalize(String path) {
|
|
String p = path == null ? "" : path.trim();
|
|
if (p.isEmpty()) {
|
|
return "/";
|
|
}
|
|
if (!p.startsWith("/")) {
|
|
p = "/" + p;
|
|
}
|
|
int q = p.indexOf('?');
|
|
if (q >= 0) {
|
|
p = p.substring(0, q);
|
|
}
|
|
String[] segs = p.split("/");
|
|
StringBuilder sb = new StringBuilder(p.length());
|
|
for (String seg : segs) {
|
|
if (seg.length() > 0 && isDigits(seg)) {
|
|
seg = "{id}";
|
|
}
|
|
sb.append('/').append(seg);
|
|
}
|
|
p = sb.toString();
|
|
return p.length() > MAX_URI_LEN ? p.substring(0, MAX_URI_LEN) : p;
|
|
}
|
|
|
|
private boolean isDigits(String s) {
|
|
for (int i = 0; i < s.length(); i++) {
|
|
if (!Character.isDigit(s.charAt(i))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/** 出站调用的压测标记继承自触发它的入站请求(X-Load-Test 头);定时任务等无请求上下文则为 false */
|
|
private boolean inheritLoadTestFlag() {
|
|
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes) {
|
|
HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
|
String v = req.getHeader(ApiMetricsFilter.HEADER_LOAD_TEST);
|
|
if (v != null && !v.trim().isEmpty()) {
|
|
v = v.trim();
|
|
return !"0".equals(v) && !"false".equalsIgnoreCase(v);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
}
|