You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在HotChocolate中记录Resolver/操作名称及调用时长?

优雅记录HotChocolate GraphQL操作名称与Resolver调用时长(v11.0.8)

针对你的需求,完全没必要去碰私有成员,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 05:07:52