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)。
* 切在 yh-openapi-client 的 {@code YschOpenCall}(SDK 重构后所有出站开放接口调用的唯一 * 公共入口,Spring Bean)上:YschBaseOpenApi 静态门面、OpenApiUserClient / OpenApiAppClient * 注入式调用全部经它发出,一处切面覆盖全部出站,不改动任何业务 Service 代码。
* 入站请求数(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)。
* 按名重载第 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; } }