健康检查,数据埋点
build / build-push (push) Successful in 1m11s

This commit is contained in:
chenxing
2026-09-03 11:50:22 +08:00
parent ac00581417
commit 59e8197d22
10 changed files with 739 additions and 14 deletions
@@ -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;
}
}