Skip to content

Commit 8360265

Browse files
authored
Record data-loader execution time within Apollo Tracing results (#3691)
* Record data-loader execution time within apollo tracing results * Update * Update src/GraphQL.DataLoader.Tests/ApolloTracingTests.cs * Update src/GraphQL.DataLoader.Tests/ApolloTracingTests.cs
1 parent ea9b758 commit 8360265

3 files changed

Lines changed: 87 additions & 2 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using GraphQL.Instrumentation;
2+
using GraphQL.Types;
3+
using Microsoft.Extensions.DependencyInjection;
4+
5+
namespace GraphQL.DataLoader.Tests;
6+
7+
public class ApolloTracingTests
8+
{
9+
// verifies that the time spent executing the data loader is included with the
10+
// resolver duration, as the resolver has not 'completed' until the data loader completes
11+
//
12+
// note that (a) for batch data loaders, the time spent in the data loader is counted for all resolvers
13+
// waiting on that data loader, and (b) any delay caused by the execution engine is counted within the
14+
// resolver execution time
15+
//
16+
// perhaps it would be more ideal to separately record the data loader execution time within the apollo
17+
// tracing output (e.g. 'extensions.tracing.execution.dataloaders'), but this does not currently happen,
18+
// and the execution engine is not aware of which queued data loaders run as a single batch operation or not,
19+
// making implementation difficult
20+
[Fact]
21+
public async Task DataLoaderTimeRecorded()
22+
{
23+
var query = new ObjectGraphType() { Name = "Query" };
24+
query.Field<StringGraphType>("test")
25+
.Resolve(_ => new SimpleDataLoader<string>(async _ =>
26+
{
27+
await Task.Delay(2000);
28+
return "Ok";
29+
}));
30+
31+
var serviceCollection = new ServiceCollection();
32+
serviceCollection.AddGraphQL(b => b
33+
.AddSchema(provider => new Schema(provider) { Query = query })
34+
.AddSystemTextJson()
35+
.UseApolloTracing());
36+
var services = serviceCollection.BuildServiceProvider();
37+
var serializer = services.GetRequiredService<IGraphQLTextSerializer>();
38+
var executer = services.GetRequiredService<IDocumentExecuter>();
39+
var ret = await executer.ExecuteAsync(new()
40+
{
41+
Query = "{test}",
42+
RequestServices = services,
43+
});
44+
var apolloTrace = ret.Extensions["tracing"].ShouldBeOfType<ApolloTrace>();
45+
var resolverData = apolloTrace.Execution.Resolvers.Single();
46+
resolverData.Path.ShouldBe(new object[] { "test" });
47+
resolverData.Duration.ShouldBeGreaterThanOrEqualTo(2000000000L);
48+
}
49+
}

src/GraphQL.DataLoader.Tests/GraphQL.DataLoader.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<ProjectReference Include="..\GraphQL.NewtonsoftJson\GraphQL.NewtonsoftJson.csproj" />
2828
<ProjectReference Include="..\GraphQL.SystemTextJson\GraphQL.SystemTextJson.csproj" />
2929
<ProjectReference Include="..\GraphQL.DataLoader\GraphQL.DataLoader.csproj" />
30+
<ProjectReference Include="..\GraphQL.MicrosoftDI\GraphQL.MicrosoftDI.csproj" />
3031
</ItemGroup>
3132

3233
</Project>

src/GraphQL/Instrumentation/InstrumentFieldsMiddleware.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using GraphQL.DataLoader;
2+
13
namespace GraphQL.Instrumentation
24
{
35
/// <summary>
@@ -25,8 +27,41 @@ public class InstrumentFieldsMiddleware : IFieldMiddleware
2527
{ "path", context.ResponsePath },
2628
};
2729

28-
using (context.Metrics.Subject("field", name, metadata))
29-
return await next(context).ConfigureAwait(false);
30+
var marker = context.Metrics.Subject("field", name, metadata);
31+
var disposeMarker = true;
32+
try
33+
{
34+
var ret = await next(context).ConfigureAwait(false);
35+
if (ret is IDataLoaderResult dataLoaderResult)
36+
{
37+
disposeMarker = false;
38+
return new CompleteDataLoaderResult(dataLoaderResult, marker);
39+
}
40+
return ret;
41+
}
42+
finally
43+
{
44+
if (disposeMarker)
45+
marker.Dispose();
46+
}
47+
}
48+
49+
private class CompleteDataLoaderResult : IDataLoaderResult
50+
{
51+
private readonly IDataLoaderResult _baseDataLoaderResult;
52+
private readonly Metrics.Marker _marker;
53+
54+
public CompleteDataLoaderResult(IDataLoaderResult baseDataLoaderResult, Metrics.Marker marker)
55+
{
56+
_baseDataLoaderResult = baseDataLoaderResult;
57+
_marker = marker;
58+
}
59+
60+
public async Task<object?> GetResultAsync(CancellationToken cancellationToken = default)
61+
{
62+
using (_marker)
63+
return await _baseDataLoaderResult.GetResultAsync(cancellationToken).ConfigureAwait(false);
64+
}
3065
}
3166
}
3267
}

0 commit comments

Comments
 (0)