@@ -0,0 +1,134 @@
|
||||
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 OpenApiHttpClient}(所有出站开放接口调用的唯一公共入口,
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截 OpenApiHttpClient 全部公开执行方法(getJson/postJson/get/post/postMultipart)。<br>
|
||||
* 各方法签名第 2 参均为 path(如 /oapi/app/list),据此归一化记 uri。
|
||||
*/
|
||||
@Around("execution(* com.yunhai.platform.openapi.OpenApiHttpClient.*(..))")
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user