feat(selfop): add opt-in test query timing diagnostics

This commit is contained in:
2026-10-13 10:31:46 +08:00
parent d08ed2f2ca
commit 4c9878c338
5 changed files with 279 additions and 0 deletions
@@ -0,0 +1,55 @@
package com.baiyue.lastleg.selfoperated.analytics.query.diagnostics;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.*;
/** Request-local, parameter-free client timings. No SQL or business identities are retained. */
final class QueryDiagnostics {
static final ThreadLocal<State> CURRENT = new ThreadLocal<>();
private static final ObjectMapper JSON = new ObjectMapper();
static final String PREFIX = "com.baiyue.lastleg.selfoperated.analytics.query.";
private static final Set<String> METHODS = Set.of("AnalyticsReadMapper.scopes", "AnalyticsReadMapper.publishedRun",
"AnalyticsReadMapper.applyBarriers", "AnalyticsReadMapper.count", "AnalyticsReadMapper.page",
"AnalyticsReadMapper.inventoryCount", "AnalyticsReadMapper.inventoryPage", "DashboardReadMapper.daily", "DashboardReadMapper.report");
static String method(String id) {
if (!id.startsWith(PREFIX)) return null;
String method = id.substring(PREFIX.length());
return METHODS.contains(method) ? method : null;
}
static String phase(String method) {
if (method.endsWith(".scopes") || method.endsWith(".publishedRun") || method.endsWith(".applyBarriers")) return "CONTEXT";
if (method.endsWith(".daily")) return "DAILY";
if (method.endsWith(".report")) return "REPORT";
return method.endsWith("Count") || method.endsWith(".count") ? "COUNT" : "PAGE";
}
static final class Timing {
public long calls, failures, nanos;
void add(long elapsed, boolean failed) { calls++; nanos += elapsed; if (failed) failures++; }
}
static final class MethodTiming {
public final String phase;
public final Timing executor = new Timing(), statement = new Timing();
MethodTiming(String method) { phase = QueryDiagnostics.phase(method); }
}
static final class State {
final long started = System.nanoTime();
final Map<String, MethodTiming> methods = new TreeMap<>();
final Deque<String> frames = new ArrayDeque<>();
int statementDepth;
MethodTiming timing(String method) { return methods.computeIfAbsent(method, MethodTiming::new); }
String json(String endpoint, String requestId, int status, boolean failed) {
long total = System.nanoTime() - started;
long executor = methods.values().stream().mapToLong(t -> t.executor.nanos).sum();
Map<String,Object> report = new LinkedHashMap<>();
report.put("event", "SELFOP_QUERY_DIAGNOSTICS"); report.put("endpoint", endpoint);
report.put("requestId", requestId != null && requestId.matches("[A-Za-z0-9._:-]{1,128}") ? requestId : null);
report.put("httpStatus", status); report.put("escapedException", failed);
report.put("requestNanos", total); report.put("unattributedNanos", Math.max(0, total-executor));
report.put("timingMeaning", "CLIENT_EXECUTOR_AND_STATEMENT_INCLUDING_MAPPING_NOT_SERVER_DB_TIME");
report.put("poolWaitAttribution", "NOT_MEASURED_PER_REQUEST"); report.put("methods", methods);
try { return JSON.writeValueAsString(report); }
catch (JsonProcessingException e) { return "{\"event\":\"SELFOP_QUERY_DIAGNOSTICS_SERIALIZATION_FAILED\"}"; }
}
}
}
@@ -0,0 +1,26 @@
package com.baiyue.lastleg.selfoperated.analytics.query.diagnostics;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.*;
import org.springframework.core.Ordered;
/** Explicit TEST opt-in; no datasource decoration or changes to business queries. */
@Configuration(proxyBeanMethods=false)
@ConditionalOnProperty(name="DEPLOY_ENV", havingValue="test", matchIfMissing=false)
public class QueryDiagnosticsConfiguration {
@Bean
@ConditionalOnProperty(name="baiyue.self-operated-analytics.query-diagnostics.enabled", havingValue="true", matchIfMissing=false)
public QueryDiagnosticsInterceptor queryDiagnosticsInterceptor() { return new QueryDiagnosticsInterceptor(); }
@Bean
@ConditionalOnProperty(name="baiyue.self-operated-analytics.query-diagnostics.enabled", havingValue="true", matchIfMissing=false)
public FilterRegistrationBean<QueryDiagnosticsFilter> queryDiagnosticsFilter() {
var log = LoggerFactory.getLogger(QueryDiagnosticsFilter.class);
var registration = new FilterRegistrationBean<>(new QueryDiagnosticsFilter(log::info));
registration.setOrder(Ordered.HIGHEST_PRECEDENCE+1);
registration.setDispatcherTypes(jakarta.servlet.DispatcherType.REQUEST);
return registration;
}
}
@@ -0,0 +1,33 @@
package com.baiyue.lastleg.selfoperated.analytics.query.diagnostics;
import com.baiyue.common.web.CorrelationContext;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import org.slf4j.MDC;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Map;
import java.util.function.Consumer;
public final class QueryDiagnosticsFilter extends OncePerRequestFilter {
private static final String BASE = "/api/platform/v1/last-leg/self-operated-analytics/";
private static final Map<String,String> ENDPOINTS = Map.of(BASE+"dashboard", "DASHBOARD",
BASE+"sku-sales-summary", "SUMMARY", BASE+"inventory-coverage", "INVENTORY");
private final Consumer<String> sink;
QueryDiagnosticsFilter(Consumer<String> sink) { this.sink = sink; }
@Override protected boolean shouldNotFilter(HttpServletRequest request) {
return !"GET".equals(request.getMethod()) || !ENDPOINTS.containsKey(request.getRequestURI());
}
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
var state = new QueryDiagnostics.State(); QueryDiagnostics.CURRENT.set(state);
String requestId = MDC.get(CorrelationContext.REQUEST_ID); boolean failed = true;
try { chain.doFilter(request, response); failed = false; }
finally {
QueryDiagnostics.CURRENT.remove();
// Diagnostic failures must never replace the original request result or exception.
try { sink.accept(state.json(ENDPOINTS.get(request.getRequestURI()), requestId, response.getStatus(), failed)); }
catch (RuntimeException ignored) { logger.warn("SELFOP_QUERY_DIAGNOSTICS_OUTPUT_FAILED"); }
}
}
}
@@ -0,0 +1,42 @@
package com.baiyue.lastleg.selfoperated.analytics.query.diagnostics;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.*;
import java.sql.Statement;
@Intercepts({
@Signature(type=Executor.class, method="query", args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}),
@Signature(type=Executor.class, method="query", args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class,CacheKey.class,BoundSql.class}),
@Signature(type=StatementHandler.class, method="query", args={Statement.class,ResultHandler.class})
})
public final class QueryDiagnosticsInterceptor implements Interceptor {
@Override public Object intercept(Invocation invocation) throws Throwable {
var state = QueryDiagnostics.CURRENT.get();
if (state == null) return invocation.proceed();
boolean executor = invocation.getTarget() instanceof Executor;
String method;
boolean record;
if (executor) {
method = QueryDiagnostics.method(((MappedStatement) invocation.getArgs()[0]).getId());
record = method != null && state.frames.isEmpty();
state.frames.push(method == null ? "" : method);
} else {
method = state.frames.peek();
record = method != null && !method.isEmpty() && state.statementDepth == 0;
state.statementDepth++;
}
long start = System.nanoTime(); boolean failed = true;
try { Object result = invocation.proceed(); failed = false; return result; }
finally {
if (record) {
var timing = state.timing(method);
(executor ? timing.executor : timing.statement).add(System.nanoTime()-start, failed);
}
if (executor) state.frames.pop(); else state.statementDepth--;
}
}
}