feat(selfop): expose opt-in test query timing headers
This commit is contained in:
+4
@@ -39,12 +39,16 @@ final class QueryDiagnostics {
|
||||
int statementDepth;
|
||||
MethodTiming timing(String method) { return methods.computeIfAbsent(method, MethodTiming::new); }
|
||||
String json(String endpoint, String requestId, int status, boolean failed) {
|
||||
return json(endpoint, requestId, status, failed, "REQUEST_COMPLETE");
|
||||
}
|
||||
String json(String endpoint, String requestId, int status, boolean failed, String capturePoint) {
|
||||
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("capturePoint", capturePoint);
|
||||
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);
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.baiyue.lastleg.selfoperated.analytics.query.diagnostics;
|
||||
|
||||
import com.baiyue.common.web.CorrelationContext;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.*;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
import java.util.Map;
|
||||
|
||||
/** Opt-in diagnostic snapshot before serialization; never changes the response body. */
|
||||
@RestControllerAdvice
|
||||
@ConditionalOnExpression("'${DEPLOY_ENV:}' == 'test' && ${baiyue.self-operated-analytics.query-diagnostics.enabled:false}")
|
||||
public final class QueryDiagnosticsResponseAdvice implements ResponseBodyAdvice<Object> {
|
||||
public static final String HEADER = "X-Selfop-Query-Diagnostics";
|
||||
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");
|
||||
@Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return QueryDiagnostics.CURRENT.get() != null;
|
||||
}
|
||||
@Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType contentType,
|
||||
Class<? extends HttpMessageConverter<?>> converterType, ServerHttpRequest request, ServerHttpResponse response) {
|
||||
var state = QueryDiagnostics.CURRENT.get();
|
||||
String endpoint = ENDPOINTS.get(request.getURI().getPath());
|
||||
if (state != null && endpoint != null && request.getMethod() == org.springframework.http.HttpMethod.GET) {
|
||||
int status = response instanceof ServletServerHttpResponse servlet ? servlet.getServletResponse().getStatus() : 200;
|
||||
response.getHeaders().set(HEADER, state.json(endpoint, MDC.get(CorrelationContext.REQUEST_ID), status, false, "BEFORE_BODY_WRITE"));
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
+36
-4
@@ -25,15 +25,47 @@ class QueryDiagnosticsTest {
|
||||
@AfterEach void cleanup() { QueryDiagnostics.CURRENT.remove(); MDC.clear(); }
|
||||
|
||||
@Test void onlyExplicitTestAndEnabledCreatesBeans() {
|
||||
var runner=new ApplicationContextRunner().withUserConfiguration(QueryDiagnosticsConfiguration.class);
|
||||
runner.run(c->assertEquals(0,c.getBeansOfType(QueryDiagnosticsInterceptor.class).size()));
|
||||
var runner=new ApplicationContextRunner().withUserConfiguration(QueryDiagnosticsConfiguration.class,QueryDiagnosticsResponseAdvice.class);
|
||||
runner.run(c->{assertEquals(0,c.getBeansOfType(QueryDiagnosticsInterceptor.class).size());assertEquals(0,c.getBeansOfType(QueryDiagnosticsResponseAdvice.class).size());});
|
||||
runner.withPropertyValues("DEPLOY_ENV=test").run(c->assertEquals(0,c.getBeansOfType(FilterRegistrationBean.class).size()));
|
||||
runner.withPropertyValues("DEPLOY_ENV=production","baiyue.self-operated-analytics.query-diagnostics.enabled=true")
|
||||
.run(c->assertEquals(0,c.getBeansOfType(QueryDiagnosticsInterceptor.class).size()));
|
||||
.run(c->{assertEquals(0,c.getBeansOfType(QueryDiagnosticsInterceptor.class).size());assertEquals(0,c.getBeansOfType(QueryDiagnosticsResponseAdvice.class).size());});
|
||||
runner.withPropertyValues("baiyue.self-operated-analytics.query-diagnostics.enabled=true")
|
||||
.run(c->assertEquals(0,c.getBeansOfType(QueryDiagnosticsInterceptor.class).size()));
|
||||
runner.withPropertyValues("DEPLOY_ENV=test","baiyue.self-operated-analytics.query-diagnostics.enabled=true")
|
||||
.run(c->{assertEquals(1,c.getBeansOfType(QueryDiagnosticsInterceptor.class).size());assertEquals(1,c.getBeansOfType(FilterRegistrationBean.class).size());});
|
||||
.run(c->{assertEquals(1,c.getBeansOfType(QueryDiagnosticsInterceptor.class).size());assertEquals(1,c.getBeansOfType(FilterRegistrationBean.class).size());assertEquals(1,c.getBeansOfType(QueryDiagnosticsResponseAdvice.class).size());});
|
||||
}
|
||||
|
||||
@Test void responseHeaderRequiresActiveStateAndPreservesBodyAndCapturePoint() throws Exception {
|
||||
var advice=new QueryDiagnosticsResponseAdvice();
|
||||
Object body=Map.of("sku","PRIVATE_SKU","token","PRIVATE_TOKEN");
|
||||
var servletRequest=new MockHttpServletRequest("GET",BASE+"dashboard");
|
||||
servletRequest.setQueryString("sourceAccount=PRIVATE_ACCOUNT");
|
||||
var request=new org.springframework.http.server.ServletServerHttpRequest(servletRequest);
|
||||
var response=new org.springframework.http.server.ServletServerHttpResponse(new MockHttpServletResponse());
|
||||
assertFalse(advice.supports(null,null));
|
||||
assertSame(body,advice.beforeBodyWrite(body,null,null,null,request,response));
|
||||
assertNull(response.getHeaders().getFirst(QueryDiagnosticsResponseAdvice.HEADER));
|
||||
var state=new QueryDiagnostics.State();QueryDiagnostics.CURRENT.set(state);MDC.put("requestId","synthetic-header-1");
|
||||
state.timing("DashboardReadMapper.report").executor.add(123,false);
|
||||
assertTrue(advice.supports(null,null));
|
||||
assertSame(body,advice.beforeBodyWrite(body,null,null,null,request,response));
|
||||
String header=response.getHeaders().getFirst(QueryDiagnosticsResponseAdvice.HEADER);
|
||||
assertNotNull(header);assertFalse(header.contains("PRIVATE"));assertFalse(header.contains("\n"));
|
||||
var json=new ObjectMapper().readTree(header);
|
||||
assertEquals("BEFORE_BODY_WRITE",json.path("capturePoint").asText());
|
||||
assertEquals("DASHBOARD",json.path("endpoint").asText());
|
||||
assertEquals("synthetic-header-1",json.path("requestId").asText());
|
||||
assertEquals(1,json.path("methods").path("DashboardReadMapper.report").path("executor").path("calls").asInt());
|
||||
assertEquals("REQUEST_COMPLETE",new ObjectMapper().readTree(state.json("DASHBOARD","synthetic",200,false)).path("capturePoint").asText());
|
||||
for(String path:List.of("other","dashboard/extra")){
|
||||
var skipped=new org.springframework.http.server.ServletServerHttpResponse(new MockHttpServletResponse());
|
||||
assertSame(body,advice.beforeBodyWrite(body,null,null,null,new org.springframework.http.server.ServletServerHttpRequest(new MockHttpServletRequest("GET",BASE+path)),skipped));
|
||||
assertNull(skipped.getHeaders().getFirst(QueryDiagnosticsResponseAdvice.HEADER));
|
||||
}
|
||||
var post=new org.springframework.http.server.ServletServerHttpResponse(new MockHttpServletResponse());
|
||||
advice.beforeBodyWrite(body,null,null,null,new org.springframework.http.server.ServletServerHttpRequest(new MockHttpServletRequest("POST",BASE+"dashboard")),post);
|
||||
assertNull(post.getHeaders().getFirst(QueryDiagnosticsResponseAdvice.HEADER));
|
||||
}
|
||||
|
||||
@Test void pathsAndMethodsAreExactAndExceptionsCleanRequestState() throws Exception {
|
||||
|
||||
Reference in New Issue
Block a user