Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ internal sealed class ExecuteHttpRequestSpan(
return new ExecuteHttpRequestSpan(activity, httpContext, kind, enricher, options);
}

/// <summary>
/// Specifies whether this span covers more than one GraphQL request. The items of a
/// batch execute concurrently, so they must not write their own operation details
/// onto this shared span.
/// </summary>
public bool IsBatch { get; private set; }

public void MarkAsBatch() => IsBatch = true;

public void SetSingleRequestDetails(GraphQLRequest request)
{
Activity.SetTag(GraphQL.Http.Request.Type, GraphQL.Http.Request.Types.Single);
Expand Down Expand Up @@ -122,13 +131,13 @@ public void SetBatchRequestDetails(IReadOnlyList<GraphQLRequest> batch)
if (request.DocumentId is not null
&& (options.RequestDetails & RequestDetails.Id) == RequestDetails.Id)
{
Activity.SetTag(GraphQL.Http.Request.BatchRequest.QueryId(i), request.DocumentId.Value);
Activity.SetTag(GraphQL.Http.Request.BatchRequest.QueryId(i), request.DocumentId.Value.Value);
}

if (request.DocumentHash is not null
&& (options.RequestDetails & RequestDetails.Hash) == RequestDetails.Hash)
{
Activity.SetTag(GraphQL.Http.Request.BatchRequest.QueryHash(i), request.DocumentHash.Value);
Activity.SetTag(GraphQL.Http.Request.BatchRequest.QueryHash(i), request.DocumentHash.Value.Value);
}

if (request.Document is not null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ public override IDisposable ExecuteRequest(RequestContext context)
return EmptyScope;
}

// A GraphQL over HTTP request reuses the transport span as the root request
// span instead of emitting a second one. A transport that has no span of its
// own, like a WebSocket session, falls through to a dedicated request span.
if (httpContext.Features.Get<ExecuteHttpRequestSpan>() is { } httpRequestSpan)
// A single GraphQL over HTTP request reuses the transport span as the root
// request span instead of emitting a second one. A batch shares one transport
// span across concurrently executing items, and a transport without a span of
// its own, like a WebSocket session, has nothing to reuse, so both fall
// through to a dedicated request span.
if (httpContext.Features.Get<ExecuteHttpRequestSpan>() is { IsBatch: false } httpRequestSpan)
{
httpContextActivity = httpRequestSpan.Activity;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,14 @@ public override void StartSingleRequest(HttpContext context, GraphQLRequest requ

public override void StartBatchRequest(HttpContext context, IReadOnlyList<GraphQLRequest> batch)
{
if (options.IncludeRequestDetails
&& context.Features.Get<ExecuteHttpRequestSpan>() is { } span)
if (context.Features.Get<ExecuteHttpRequestSpan>() is not { } span)
{
return;
}

span.MarkAsBatch();

if (options.IncludeRequestDetails)
{
span.SetBatchRequestDetails(batch);
}
Expand All @@ -55,8 +61,14 @@ public override void StartOperationBatchRequest(
GraphQLRequest request,
IReadOnlyList<string> operations)
{
if (options.IncludeRequestDetails
&& context.Features.Get<ExecuteHttpRequestSpan>() is { } span)
if (context.Features.Get<ExecuteHttpRequestSpan>() is not { } span)
{
return;
}

span.MarkAsBatch();
Comment thread
tobias-tengler marked this conversation as resolved.

if (options.IncludeRequestDetails)
{
span.SetOperationBatchRequestDetails(request, operations);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,37 @@ public async Task Http_Get_PersistedOperationEndpoint_GetHeroName_Default()
}
}

[Fact]
public async Task Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default()
{
using (CaptureActivities(out var activities))
{
// arrange
// batch items execute concurrently and share one transport span, so with the
// default scopes they must not fold their operation details into it
using var server = CreateInstrumentedServer();
using var client = server.CreateClient();
client.BaseAddress = new Uri("http://localhost:5000");

const string batch =
"""
[{"query":"query A { hero { name } }"},
{"query":"query B { human(id: \"1000\") { name } }"}]
""";

// act
using var content = new StringContent(batch, Encoding.UTF8, "application/json");
using var response = await client.PostAsync(
"/graphql",
content,
TestContext.Current.CancellationToken);
await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);

// assert
activities.MatchSnapshot(Postfix([NET11_0]));
}
}

[Fact]
public async Task Http_Post_Variables_Are_Not_Automatically_Added_To_Activities()
{
Expand Down
Loading
Loading