如何在HotChocolate中记录Resolver/操作名称及调用时长?
针对你的需求,完全没必要去碰私有成员,HotChocolate v11本身就提供了官方诊断机制和扩展点,能优雅地获取操作名称、Resolver信息及调用时长,下面给你两种靠谱的实现方案:
方案一:利用HotChocolate诊断监听器(推荐)
HotChocolate内置了完整的诊断事件系统,可以监听请求生命周期的各个关键节点,包括请求开始/结束、Resolver执行开始/结束等,完美匹配你的日志需求,而且完全基于公开API实现。
步骤1:创建自定义诊断监听器
继承DiagnosticEventListener(来自HotChocolate.Diagnostics命名空间),重写需要的事件方法:
using HotChocolate.Diagnostics; using System.Diagnostics; public class GraphQLDiagnosticListener : DiagnosticEventListener { // 监听GraphQL请求启动 public override void RequestStarted(IRequestContext context) { var operationName = context.Operation.Name ?? "匿名操作"; // 记录请求开始时间,后续用于计算总耗时 context.Properties["RequestStartTime"] = Stopwatch.GetTimestamp(); Console.WriteLine($"📝 GraphQL请求启动:操作名称={operationName}"); } // 监听GraphQL请求完成 public override void RequestCompleted(IRequestContext context) { if (context.Properties.TryGetValue("RequestStartTime", out var startTimeObj) && startTimeObj is long startTime) { var totalMs = Stopwatch.GetElapsedTime(startTime).TotalMilliseconds; var operationName = context.Operation.Name ?? "匿名操作"; Console.WriteLine($"✅ GraphQL请求完成:操作名称={operationName},总耗时={totalMs:F2}ms"); } } // 监听Resolver执行启动 public override void ResolverStarted(IResolverContext context) { var resolverId = $"{context.ParentType.Name}.{context.Field.Name}"; context.Properties["ResolverStartTime"] = Stopwatch.GetTimestamp(); Console.WriteLine($"🔧 Resolver启动:{resolverId}"); } // 监听Resolver执行完成 public override void ResolverCompleted(IResolverContext context) { if (context.Properties.TryGetValue("ResolverStartTime", out var startTimeObj) && startTimeObj is long startTime) { var resolverId = $"{context.ParentType.Name}.{context.Field.Name}"; var elapsedMs = Stopwatch.GetElapsedTime(startTime).TotalMilliseconds; Console.WriteLine($"🔩 Resolver完成:{resolverId},耗时={elapsedMs:F2}ms"); } } }
步骤2:注册诊断监听器到服务
在Startup的ConfigureServices中添加你的监听器:
services.AddGraphQLServer() // 你的其他GraphQL配置(比如添加类型、指令等) .AddDiagnosticEventListener<GraphQLDiagnosticListener>();
后续要输出到AppInsights也很简单,把Console.WriteLine替换成AppInsights的追踪方法即可,完全不需要修改核心逻辑。
方案二:自定义请求执行器(匹配你提到的GraphQL.Net思路)
如果你更倾向于通过自定义执行器来统一处理请求,HotChocolate也允许替换默认的IRequestExecutor实现,在执行前后获取操作名称并计算时长:
步骤1:创建自定义RequestExecutor
using HotChocolate.Execution; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; public class TimedRequestExecutor<T> : DefaultRequestExecutor<T> where T : ISchema { public TimedRequestExecutor(ISchema schema, IRequestExecutorOptions options, IServiceProvider serviceProvider) : base(schema, options, serviceProvider) { } public override async Task<IExecutionResult> ExecuteAsync(IQueryRequest request, CancellationToken cancellationToken = default) { var stopwatch = Stopwatch.StartNew(); var operationName = request.OperationName ?? "匿名操作"; // 执行原始GraphQL请求 var result = await base.ExecuteAsync(request, cancellationToken); stopwatch.Stop(); Console.WriteLine($"📊 GraphQL操作[{operationName}]执行完成,耗时={stopwatch.ElapsedMilliseconds}ms"); return result; } }
步骤2:替换默认执行器
在Startup中注册你的自定义执行器(记得替换YourSchemaType为你实际的Schema类型):
services.AddGraphQLServer() // 其他GraphQL配置 .AddRequestExecutor<TimedRequestExecutor<YourSchemaType>>();
这种方式适合你需要在请求执行前后做统一逻辑处理的场景,但如果要监听每个Resolver的调用细节,还是方案一更合适。
补充:优化HttpRequestInterceptor的用法(如果你坚持用这个)
如果你还是想用AddHttpRequestInterceptor,也不用去访问私有成员——可以通过builder.Build()获取公开的IQueryRequest对象,直接读取OperationName,解析字段也用公开API:
services.AddGraphQLServer() .AddHttpRequestInterceptor(async (context, executor, builder, ct) => { var request = builder.Build(); var operationName = request.OperationName ?? "匿名操作"; // 解析查询中的字段(如果需要) if (request.Document != null) { var operation = request.Document.Definitions .OfType<OperationDefinitionNode>() .FirstOrDefault(o => o.Name?.Value == operationName); if (operation != null) { var fields = operation.SelectionSet.Selections .OfType<FieldNode>() .Select(f => f.Name.Value); Console.WriteLine($"🔍 查询字段:{string.Join(", ", fields)}"); } } // 记录开始时间,后续在中间件中计算时长 context.Items["RequestStartTime"] = Stopwatch.GetTimestamp(); await Task.CompletedTask; }) .UseMiddleware<RequestTimingMiddleware>();
再写一个中间件计算总耗时:
public class RequestTimingMiddleware { private readonly RequestDelegate _next; public RequestTimingMiddleware(RequestDelegate next) => _next = next; public async Task InvokeAsync(HttpContext context) { await _next(context); if (context.Items.TryGetValue("RequestStartTime", out var startTimeObj) && startTimeObj is long startTime) { var totalMs = Stopwatch.GetElapsedTime(startTime).TotalMilliseconds; var operationName = context.Items["OperationName"] as string ?? "匿名操作"; Console.WriteLine($"⏱️ 请求完成:{operationName},总耗时={totalMs:F2}ms"); } } }
不过这种方式比诊断监听器繁琐很多,还是推荐方案一,毕竟是HotChocolate官方设计的扩展方式,更稳定且覆盖场景更全。
内容的提问来源于stack exchange,提问作者Pascal Senn

