如何覆盖Micrometer Core的@TimedAspect适配WebFlux及GraphQL错误指标捕获
WebFlux + GraphQL 下 Micrometer Timed 指标的错误捕获与自定义属性方案
一、解决响应式场景下 TimedAspect 错误捕获失效问题
原生TimedAspect的try/catch只能捕获同步方法异常,Mono/Flux的异步错误根本进不到同步try块里。别硬改原生Aspect,换响应式思路处理:
1. 自定义响应式适配的 Timed Advice
自己写环绕通知,专门处理返回Publisher类型的方法:
@Aspect @Component public class ReactiveTimedAspect { private final MeterRegistry meterRegistry; private final Clock clock; public ReactiveTimedAspect(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.clock = Clock.SYSTEM; } @Around("@annotation(timed)") public Object timedMethod(ProceedingJoinPoint pjp, Timed timed) throws Throwable { Timer.Sample sample = Timer.start(clock); Object result = pjp.proceed(); if (result instanceof Publisher) { return ((Publisher<?>) result) .doOnSuccess(v -> recordSuccess(sample, timed, pjp)) .doOnError(e -> recordError(sample, timed, pjp, e)); } else { recordSuccess(sample, timed, pjp); return result; } } private void recordSuccess(Timer.Sample sample, Timed timed, ProceedingJoinPoint pjp) { Timer timer = buildTimer(timed, pjp, "success"); sample.stop(timer); } private void recordError(Timer.Sample sample, Timed timed, ProceedingJoinPoint pjp, Throwable e) { Timer timer = buildTimer(timed, pjp, "error"); sample.stop(timer); } private Timer buildTimer(Timed timed, ProceedingJoinPoint pjp, String status) { return Timer.builder(timed.value().isEmpty() ? pjp.getSignature().getName() : timed.value()) .tags(timed.extraTags()) .tag("status", status) .tag("method", pjp.getSignature().getName()) .tag("class", pjp.getTarget().getClass().getSimpleName()) .register(meterRegistry); } }
这个Advice会在Mono/Flux的onSuccess/onError信号触发时,分别记录成功/错误状态的指标,完美适配响应式场景。
2. 禁用原生 TimedAspect
避免两个Aspect冲突,在启动类排除原生自动配置:
@SpringBootApplication(exclude = {TimedAspectAutoConfiguration.class}) public class AppConfig { }
二、在 GraphQL Resolver 中添加自定义指标属性
1. 手动构建 Timer 绑定自定义 Tag
在Resolver里直接创建Timer,把Resolver类名、GraphQL操作名等作为Tag:
@Component public class UserResolver { private final Timer getUserTimer; public UserResolver(MeterRegistry meterRegistry) { this.getUserTimer = Timer.builder("graphql.resolver.execution") .tag("resolver", "UserResolver") .tag("operation", "getUser") .register(meterRegistry); } public Mono<User> getUser(String id) { return Mono.fromCallable(() -> { // 业务逻辑 return new User(id, "test"); }) .transformDeferred(Micrometer.metricsTimer(getUserTimer)); } }
用Micrometer.metricsTimer把Timer绑定到Mono上,自动统计耗时和错误。
2. 用 GraphQL Instrumentation 全局拦截
想统一处理所有Resolver,实现GraphQL的Instrumentation:
@Component public class GraphQLMetricsInstrumentation implements Instrumentation { private final MeterRegistry meterRegistry; public GraphQLMetricsInstrumentation(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } @Override public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) { String operationName = parameters.getOperationName() != null ? parameters.getOperationName() : "unknown"; Timer.Sample sample = Timer.start(Clock.SYSTEM); return new SimpleInstrumentationContext<>() { @Override public void onCompleted(ExecutionResult result, Throwable t) { String status = t != null ? "error" : "success"; Timer timer = Timer.builder("graphql.operation.execution") .tag("operation", operationName) .tag("status", status) .tag("query_type", parameters.getQuery().getDefinitions().get(0).getClass().getSimpleName()) .register(meterRegistry); sample.stop(timer); } }; } @Override public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters) { String fieldName = parameters.getField().getName(); String resolverName = parameters.getField().getDefinition().getResolverName() != null ? parameters.getField().getDefinition().getResolverName() : "unknown"; Timer.Sample sample = Timer.start(Clock.SYSTEM); return environment -> { try { Object result = dataFetcher.get(environment); if (result instanceof Publisher) { return ((Publisher<?>) result) .doOnSuccess(v -> recordTimer("success")) .doOnError(e -> recordTimer("error")); } else { recordTimer("success"); return result; } } catch (Exception e) { recordTimer("error"); throw e; } }; void recordTimer(String status) { Timer timer = Timer.builder("graphql.resolver.field") .tag("resolver", resolverName) .tag("field", fieldName) .tag("status", status) .register(meterRegistry); sample.stop(timer); } } }
这个Instrumentation会分别统计GraphQL整体操作和每个Resolver字段的耗时,自动添加操作名、字段名等自定义Tag。
三、解决直方图无 GraphQL 场景数据的问题
1. 开启 Spring GraphQL 内置指标支持
如果用Spring GraphQL,先打开内置Metrics开关:
spring.graphql.metrics.enabled=true
但内置指标维度有限,建议配合上面的自定义Instrumentation使用。
2. 配置直方图统计自定义 Tag
在application.properties里指定要纳入直方图的Tag维度,确保GraphQL相关Tag被统计:
# 开启HTTP请求直方图 management.metrics.distribution.percentiles-histogram.http.server.requests=true # 自定义GraphQL指标的直方图 management.metrics.distribution.percentiles-histogram.graphql.resolver.field=true management.metrics.distribution.percentiles-histogram.graphql.operation.execution=true # 设置统计分位数 management.metrics.distribution.percentiles=0.5, 0.95, 0.99
3. 避免指标被合并到 /graphql 端点
原生WebFlux会把所有/graphql下的请求合并成一个端点指标,必须用自定义Tag(比如operation、resolver)区分不同GraphQL操作,这样直方图才会展示每个操作的独立数据。
内容的提问来源于stack exchange,提问作者nick
相关产品推荐
相关产品推荐

