From 1de86a5c99a1b0cce7d38440e397f2e5e6ad08d2 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:30:49 +0000 Subject: [PATCH 1/4] Give every batch item its own request span A GraphQL over HTTP batch shares a single transport span across items that execute concurrently, so folding each item's operation details into that shared span produced interleaved, meaningless tags. Mark the transport span as a batch when a batch or operation batch request starts, and have the execution listeners fall through to a dedicated request span per item instead of reusing it. --- .../Spans/Http/ExecuteHttpRequestSpan.cs | 9 + .../ActivityExecutionDiagnosticListener.cs | 10 +- .../ActivityServerDiagnosticListener.cs | 20 +- .../ActivityServerDiagnosticListenerTests.cs | 31 + ...ery_Item_Its_Own_Request_Span_Default.snap | 353 ++++++++++++ ..._Its_Own_Request_Span_Default_NET11_0.snap | 353 ++++++++++++ ...ctivityExecutionDiagnosticEventListener.cs | 10 +- .../FusionActivityServerDiagnosticListener.cs | 20 +- ...onActivityServerDiagnosticListenerTests.cs | 41 ++ ...ery_Item_Its_Own_Request_Span_Default.snap | 533 ++++++++++++++++++ ..._Its_Own_Request_Span_Default_NET11_0.snap | 533 ++++++++++++++++++ 11 files changed, 1897 insertions(+), 16 deletions(-) create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics.Core/Spans/Http/ExecuteHttpRequestSpan.cs b/src/HotChocolate/Diagnostics/src/Diagnostics.Core/Spans/Http/ExecuteHttpRequestSpan.cs index a0a227fab65..d47b8df5110 100644 --- a/src/HotChocolate/Diagnostics/src/Diagnostics.Core/Spans/Http/ExecuteHttpRequestSpan.cs +++ b/src/HotChocolate/Diagnostics/src/Diagnostics.Core/Spans/Http/ExecuteHttpRequestSpan.cs @@ -61,6 +61,15 @@ internal sealed class ExecuteHttpRequestSpan( return new ExecuteHttpRequestSpan(activity, httpContext, kind, enricher, options); } + /// + /// 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. + /// + 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); diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityExecutionDiagnosticListener.cs b/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityExecutionDiagnosticListener.cs index a4d945214ba..8b57dc14ee0 100644 --- a/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityExecutionDiagnosticListener.cs +++ b/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityExecutionDiagnosticListener.cs @@ -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() 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() is { IsBatch: false } httpRequestSpan) { httpContextActivity = httpRequestSpan.Activity; } diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityServerDiagnosticListener.cs b/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityServerDiagnosticListener.cs index c81ef74843e..604d900eb8d 100644 --- a/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityServerDiagnosticListener.cs +++ b/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityServerDiagnosticListener.cs @@ -43,8 +43,14 @@ public override void StartSingleRequest(HttpContext context, GraphQLRequest requ public override void StartBatchRequest(HttpContext context, IReadOnlyList batch) { - if (options.IncludeRequestDetails - && context.Features.Get() is { } span) + if (context.Features.Get() is not { } span) + { + return; + } + + span.MarkAsBatch(); + + if (options.IncludeRequestDetails) { span.SetBatchRequestDetails(batch); } @@ -55,8 +61,14 @@ public override void StartOperationBatchRequest( GraphQLRequest request, IReadOnlyList operations) { - if (options.IncludeRequestDetails - && context.Features.Get() is { } span) + if (context.Features.Get() is not { } span) + { + return; + } + + span.MarkAsBatch(); + + if (options.IncludeRequestDetails) { span.SetOperationBatchRequestDetails(request, operations); } diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs index f281240ff33..ca115135690 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs @@ -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() { diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap new file mode 100644 index 00000000000..7c36f7d0ad5 --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap @@ -0,0 +1,353 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP POST", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.http.kind", + "Value": "HttpPost" + }, + { + "Key": "graphql.schema.name", + "Value": "_Default" + }, + { + "Key": "graphql.http.request.type", + "Value": "batch" + }, + { + "Key": "graphql.http.request[0].query.id", + "Value": { + "IsEmpty": false, + "HasValue": true, + "Value": "dbfe187a49b8b785c6471ccb9bb8c5e4" + } + }, + { + "Key": "graphql.http.request[0].query.hash", + "Value": { + "Value": "dbfe187a49b8b785c6471ccb9bb8c5e4", + "AlgorithmName": "md5", + "Format": "Hex", + "IsEmpty": false + } + }, + { + "Key": "graphql.http.request[1].query.id", + "Value": { + "IsEmpty": false, + "HasValue": true, + "Value": "659bc020d413f3107839ff0cbbaf3440" + } + }, + { + "Key": "graphql.http.request[1].query.hash", + "Value": { + "Value": "659bc020d413f3107839ff0cbbaf3440", + "AlgorithmName": "md5", + "Format": "Hex", + "IsEmpty": false + } + } + ], + "event": [], + "activities": [ + { + "OperationName": "Parse HTTP Request", + "DisplayName": "Parse HTTP Request", + "Kind": "Internal", + "Status": "Ok", + "tags": [], + "event": [] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:dbfe187a49b8b785c6471ccb9bb8c5e4" + } + ], + "event": [ + { + "Name": "AddedOperationToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "validate" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:dbfe187a49b8b785c6471ccb9bb8c5e4" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:dbfe187a49b8b785c6471ccb9bb8c5e4" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + } + ], + "event": [] + }, + { + "OperationName": "Query.hero", + "DisplayName": "Query.hero", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "hero" + }, + { + "Key": "graphql.field.path", + "Value": "hero" + }, + { + "Key": "graphql.field.name", + "Value": "hero" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Query.hero" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Query" + } + ], + "event": [] + } + ] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:659bc020d413f3107839ff0cbbaf3440" + } + ], + "event": [ + { + "Name": "AddedOperationToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "validate" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:659bc020d413f3107839ff0cbbaf3440" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:659bc020d413f3107839ff0cbbaf3440" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + } + ], + "event": [] + }, + { + "OperationName": "Query.human", + "DisplayName": "Query.human", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "human" + }, + { + "Key": "graphql.field.path", + "Value": "human" + }, + { + "Key": "graphql.field.name", + "Value": "human" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Query.human" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Query" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap new file mode 100644 index 00000000000..8590ed1873d --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap @@ -0,0 +1,353 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 200 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP POST", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.http.kind", + "Value": "HttpPost" + }, + { + "Key": "graphql.schema.name", + "Value": "_Default" + }, + { + "Key": "graphql.http.request.type", + "Value": "batch" + }, + { + "Key": "graphql.http.request[0].query.id", + "Value": { + "IsEmpty": false, + "HasValue": true, + "Value": "dbfe187a49b8b785c6471ccb9bb8c5e4" + } + }, + { + "Key": "graphql.http.request[0].query.hash", + "Value": { + "Value": "dbfe187a49b8b785c6471ccb9bb8c5e4", + "AlgorithmName": "md5", + "Format": "Hex", + "IsEmpty": false + } + }, + { + "Key": "graphql.http.request[1].query.id", + "Value": { + "IsEmpty": false, + "HasValue": true, + "Value": "659bc020d413f3107839ff0cbbaf3440" + } + }, + { + "Key": "graphql.http.request[1].query.hash", + "Value": { + "Value": "659bc020d413f3107839ff0cbbaf3440", + "AlgorithmName": "md5", + "Format": "Hex", + "IsEmpty": false + } + } + ], + "event": [], + "activities": [ + { + "OperationName": "Parse HTTP Request", + "DisplayName": "Parse HTTP Request", + "Kind": "Internal", + "Status": "Ok", + "tags": [], + "event": [] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:dbfe187a49b8b785c6471ccb9bb8c5e4" + } + ], + "event": [ + { + "Name": "AddedOperationToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "validate" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:dbfe187a49b8b785c6471ccb9bb8c5e4" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:dbfe187a49b8b785c6471ccb9bb8c5e4" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + } + ], + "event": [] + }, + { + "OperationName": "Query.hero", + "DisplayName": "Query.hero", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "hero" + }, + { + "Key": "graphql.field.path", + "Value": "hero" + }, + { + "Key": "graphql.field.name", + "Value": "hero" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Query.hero" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Query" + } + ], + "event": [] + } + ] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:659bc020d413f3107839ff0cbbaf3440" + } + ], + "event": [ + { + "Name": "AddedOperationToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "validate" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:659bc020d413f3107839ff0cbbaf3440" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:659bc020d413f3107839ff0cbbaf3440" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + } + ], + "event": [] + }, + { + "OperationName": "Query.human", + "DisplayName": "Query.human", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "human" + }, + { + "Key": "graphql.field.path", + "Value": "human" + }, + { + "Key": "graphql.field.name", + "Value": "human" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Query.human" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Query" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityExecutionDiagnosticEventListener.cs b/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityExecutionDiagnosticEventListener.cs index 3c8e42d88b7..91ebb177f10 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityExecutionDiagnosticEventListener.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityExecutionDiagnosticEventListener.cs @@ -27,10 +27,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() 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() is { IsBatch: false } httpRequestSpan) { httpContextActivity = httpRequestSpan.Activity; } diff --git a/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityServerDiagnosticListener.cs b/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityServerDiagnosticListener.cs index 6ed4d2e2ce3..0d6576415be 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityServerDiagnosticListener.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityServerDiagnosticListener.cs @@ -44,8 +44,14 @@ public override void StartSingleRequest(HttpContext context, GraphQLRequest requ public override void StartBatchRequest(HttpContext context, IReadOnlyList batch) { - if (options.IncludeRequestDetails - && context.Features.Get() is { } span) + if (context.Features.Get() is not { } span) + { + return; + } + + span.MarkAsBatch(); + + if (options.IncludeRequestDetails) { span.SetBatchRequestDetails(batch); } @@ -56,8 +62,14 @@ public override void StartOperationBatchRequest( GraphQLRequest request, IReadOnlyList operations) { - if (options.IncludeRequestDetails - && context.Features.Get() is { } span) + if (context.Features.Get() is not { } span) + { + return; + } + + span.MarkAsBatch(); + + if (options.IncludeRequestDetails) { span.SetOperationBatchRequestDetails(request, operations); } diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs index 5a087f29163..5f476177373 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; +using HotChocolate.AspNetCore; using HotChocolate.AspNetCore.Subscriptions; using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.Diagnostics; @@ -88,6 +89,46 @@ public async Task Http_Post_Single_Request() } } + [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 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [("a", server)], + configureGatewayBuilder: b => b + .ModifyServerOptions(o => o.Batching = AllowedBatching.All) + .AddInstrumentation()); + + using var client = gateway.CreateClient(); + client.BaseAddress = new Uri("http://localhost:5000"); + + const string batch = + """ + [{"query":"query A { sayHello }"}, + {"query":"query B { greeting(name: \"x\") }"}] + """; + + // 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_PersistedOperationEndpoint_Default() { diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap new file mode 100644 index 00000000000..c21938dd824 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap @@ -0,0 +1,533 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP POST", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.http.kind", + "Value": "HttpPost" + }, + { + "Key": "graphql.schema.name", + "Value": "_Default" + }, + { + "Key": "graphql.http.request.type", + "Value": "batch" + }, + { + "Key": "graphql.http.request[0].query.id", + "Value": { + "IsEmpty": false, + "HasValue": true, + "Value": "722dc4ea59f88d2e746b3502ffa7e2d5" + } + }, + { + "Key": "graphql.http.request[0].query.hash", + "Value": { + "Value": "722dc4ea59f88d2e746b3502ffa7e2d5", + "AlgorithmName": "md5", + "Format": "Hex", + "IsEmpty": false + } + }, + { + "Key": "graphql.http.request[1].query.id", + "Value": { + "IsEmpty": false, + "HasValue": true, + "Value": "906bd3386334ff46441bf331680a58e4" + } + }, + { + "Key": "graphql.http.request[1].query.hash", + "Value": { + "Value": "906bd3386334ff46441bf331680a58e4", + "AlgorithmName": "md5", + "Format": "Hex", + "IsEmpty": false + } + } + ], + "event": [], + "activities": [ + { + "OperationName": "Parse HTTP Request", + "DisplayName": "Parse HTTP Request", + "Kind": "Internal", + "Status": "Ok", + "tags": [], + "event": [] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:722dc4ea59f88d2e746b3502ffa7e2d5" + } + ], + "event": [ + { + "Name": "AddedOperationPlanToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "validate" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:722dc4ea59f88d2e746b3502ffa7e2d5" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:722dc4ea59f88d2e746b3502ffa7e2d5" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Step Execution", + "DisplayName": "GraphQL Step Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "step_execute" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:722dc4ea59f88d2e746b3502ffa7e2d5" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "a39a0e2632168df91bf5ffa9d432107a58e7e788a80af2c6bfe7e0e276b0394b" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "A_722dc4ea_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:1f296352264c925d886fa9d9a6020bcba406ed0a3e617e3ffbc67272df03d408" + } + ], + "event": [], + "activities": [ + { + "OperationName": "System.Net.Http.HttpRequestOut", + "DisplayName": "System.Net.Http.HttpRequestOut", + "Kind": "Client", + "Status": "Unset", + "tags": [ + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "url.full", + "Value": "http://localhost:5000/graphql" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [], + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [] + } + ] + } + ] + } + ] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:906bd3386334ff46441bf331680a58e4" + } + ], + "event": [ + { + "Name": "AddedOperationPlanToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "validate" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:906bd3386334ff46441bf331680a58e4" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:906bd3386334ff46441bf331680a58e4" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Step Execution", + "DisplayName": "GraphQL Step Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "step_execute" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:906bd3386334ff46441bf331680a58e4" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "93a468cff77cf20a1bd77063256532ae27dbc91266ddde838bad3d60bd9c1d8f" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "B_906bd338_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:baae930e2f254c1c548bd48328c129a57ed38176f05ec701268c04952ac8f88d" + } + ], + "event": [], + "activities": [ + { + "OperationName": "System.Net.Http.HttpRequestOut", + "DisplayName": "System.Net.Http.HttpRequestOut", + "Kind": "Client", + "Status": "Unset", + "tags": [ + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "url.full", + "Value": "http://localhost:5000/graphql" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [], + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap new file mode 100644 index 00000000000..0791f9598f5 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap @@ -0,0 +1,533 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 200 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP POST", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.http.kind", + "Value": "HttpPost" + }, + { + "Key": "graphql.schema.name", + "Value": "_Default" + }, + { + "Key": "graphql.http.request.type", + "Value": "batch" + }, + { + "Key": "graphql.http.request[0].query.id", + "Value": { + "IsEmpty": false, + "HasValue": true, + "Value": "722dc4ea59f88d2e746b3502ffa7e2d5" + } + }, + { + "Key": "graphql.http.request[0].query.hash", + "Value": { + "Value": "722dc4ea59f88d2e746b3502ffa7e2d5", + "AlgorithmName": "md5", + "Format": "Hex", + "IsEmpty": false + } + }, + { + "Key": "graphql.http.request[1].query.id", + "Value": { + "IsEmpty": false, + "HasValue": true, + "Value": "906bd3386334ff46441bf331680a58e4" + } + }, + { + "Key": "graphql.http.request[1].query.hash", + "Value": { + "Value": "906bd3386334ff46441bf331680a58e4", + "AlgorithmName": "md5", + "Format": "Hex", + "IsEmpty": false + } + } + ], + "event": [], + "activities": [ + { + "OperationName": "Parse HTTP Request", + "DisplayName": "Parse HTTP Request", + "Kind": "Internal", + "Status": "Ok", + "tags": [], + "event": [] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:722dc4ea59f88d2e746b3502ffa7e2d5" + } + ], + "event": [ + { + "Name": "AddedOperationPlanToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "validate" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:722dc4ea59f88d2e746b3502ffa7e2d5" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:722dc4ea59f88d2e746b3502ffa7e2d5" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Step Execution", + "DisplayName": "GraphQL Step Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "step_execute" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:722dc4ea59f88d2e746b3502ffa7e2d5" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "a39a0e2632168df91bf5ffa9d432107a58e7e788a80af2c6bfe7e0e276b0394b" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "A_722dc4ea_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:1f296352264c925d886fa9d9a6020bcba406ed0a3e617e3ffbc67272df03d408" + } + ], + "event": [], + "activities": [ + { + "OperationName": "System.Net.Http.HttpRequestOut", + "DisplayName": "System.Net.Http.HttpRequestOut", + "Kind": "Client", + "Status": "Unset", + "tags": [ + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "url.full", + "Value": "http://localhost:5000/graphql" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [], + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 200 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [] + } + ] + } + ] + } + ] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:906bd3386334ff46441bf331680a58e4" + } + ], + "event": [ + { + "Name": "AddedOperationPlanToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "validate" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:906bd3386334ff46441bf331680a58e4" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:906bd3386334ff46441bf331680a58e4" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Step Execution", + "DisplayName": "GraphQL Step Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "step_execute" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:906bd3386334ff46441bf331680a58e4" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "93a468cff77cf20a1bd77063256532ae27dbc91266ddde838bad3d60bd9c1d8f" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "B_906bd338_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:baae930e2f254c1c548bd48328c129a57ed38176f05ec701268c04952ac8f88d" + } + ], + "event": [], + "activities": [ + { + "OperationName": "System.Net.Http.HttpRequestOut", + "DisplayName": "System.Net.Http.HttpRequestOut", + "Kind": "Client", + "Status": "Unset", + "tags": [ + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "url.full", + "Value": "http://localhost:5000/graphql" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [], + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 200 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] +} From fd313eb8efcede96fc82700a885cf6bbbffb3a61 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 15 Sep 2026 08:32:11 +0000 Subject: [PATCH 2/4] Fix batch request query id and hash tags serializing as objects --- .../Spans/Http/ExecuteHttpRequestSpan.cs | 4 +-- ...ery_Item_Its_Own_Request_Span_Default.snap | 26 +++---------------- ..._Its_Own_Request_Span_Default_NET11_0.snap | 26 +++---------------- ...ery_Item_Its_Own_Request_Span_Default.snap | 26 +++---------------- ..._Its_Own_Request_Span_Default_NET11_0.snap | 26 +++---------------- 5 files changed, 18 insertions(+), 90 deletions(-) diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics.Core/Spans/Http/ExecuteHttpRequestSpan.cs b/src/HotChocolate/Diagnostics/src/Diagnostics.Core/Spans/Http/ExecuteHttpRequestSpan.cs index d47b8df5110..4392a67ef86 100644 --- a/src/HotChocolate/Diagnostics/src/Diagnostics.Core/Spans/Http/ExecuteHttpRequestSpan.cs +++ b/src/HotChocolate/Diagnostics/src/Diagnostics.Core/Spans/Http/ExecuteHttpRequestSpan.cs @@ -131,13 +131,13 @@ public void SetBatchRequestDetails(IReadOnlyList 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 diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap index 7c36f7d0ad5..ec581acdf0e 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap @@ -64,37 +64,19 @@ }, { "Key": "graphql.http.request[0].query.id", - "Value": { - "IsEmpty": false, - "HasValue": true, - "Value": "dbfe187a49b8b785c6471ccb9bb8c5e4" - } + "Value": "dbfe187a49b8b785c6471ccb9bb8c5e4" }, { "Key": "graphql.http.request[0].query.hash", - "Value": { - "Value": "dbfe187a49b8b785c6471ccb9bb8c5e4", - "AlgorithmName": "md5", - "Format": "Hex", - "IsEmpty": false - } + "Value": "dbfe187a49b8b785c6471ccb9bb8c5e4" }, { "Key": "graphql.http.request[1].query.id", - "Value": { - "IsEmpty": false, - "HasValue": true, - "Value": "659bc020d413f3107839ff0cbbaf3440" - } + "Value": "659bc020d413f3107839ff0cbbaf3440" }, { "Key": "graphql.http.request[1].query.hash", - "Value": { - "Value": "659bc020d413f3107839ff0cbbaf3440", - "AlgorithmName": "md5", - "Format": "Hex", - "IsEmpty": false - } + "Value": "659bc020d413f3107839ff0cbbaf3440" } ], "event": [], diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap index 8590ed1873d..062ad6756dc 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap @@ -64,37 +64,19 @@ }, { "Key": "graphql.http.request[0].query.id", - "Value": { - "IsEmpty": false, - "HasValue": true, - "Value": "dbfe187a49b8b785c6471ccb9bb8c5e4" - } + "Value": "dbfe187a49b8b785c6471ccb9bb8c5e4" }, { "Key": "graphql.http.request[0].query.hash", - "Value": { - "Value": "dbfe187a49b8b785c6471ccb9bb8c5e4", - "AlgorithmName": "md5", - "Format": "Hex", - "IsEmpty": false - } + "Value": "dbfe187a49b8b785c6471ccb9bb8c5e4" }, { "Key": "graphql.http.request[1].query.id", - "Value": { - "IsEmpty": false, - "HasValue": true, - "Value": "659bc020d413f3107839ff0cbbaf3440" - } + "Value": "659bc020d413f3107839ff0cbbaf3440" }, { "Key": "graphql.http.request[1].query.hash", - "Value": { - "Value": "659bc020d413f3107839ff0cbbaf3440", - "AlgorithmName": "md5", - "Format": "Hex", - "IsEmpty": false - } + "Value": "659bc020d413f3107839ff0cbbaf3440" } ], "event": [], diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap index c21938dd824..a07c0f4d755 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap @@ -64,37 +64,19 @@ }, { "Key": "graphql.http.request[0].query.id", - "Value": { - "IsEmpty": false, - "HasValue": true, - "Value": "722dc4ea59f88d2e746b3502ffa7e2d5" - } + "Value": "722dc4ea59f88d2e746b3502ffa7e2d5" }, { "Key": "graphql.http.request[0].query.hash", - "Value": { - "Value": "722dc4ea59f88d2e746b3502ffa7e2d5", - "AlgorithmName": "md5", - "Format": "Hex", - "IsEmpty": false - } + "Value": "722dc4ea59f88d2e746b3502ffa7e2d5" }, { "Key": "graphql.http.request[1].query.id", - "Value": { - "IsEmpty": false, - "HasValue": true, - "Value": "906bd3386334ff46441bf331680a58e4" - } + "Value": "906bd3386334ff46441bf331680a58e4" }, { "Key": "graphql.http.request[1].query.hash", - "Value": { - "Value": "906bd3386334ff46441bf331680a58e4", - "AlgorithmName": "md5", - "Format": "Hex", - "IsEmpty": false - } + "Value": "906bd3386334ff46441bf331680a58e4" } ], "event": [], diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap index 0791f9598f5..a888241702f 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap @@ -64,37 +64,19 @@ }, { "Key": "graphql.http.request[0].query.id", - "Value": { - "IsEmpty": false, - "HasValue": true, - "Value": "722dc4ea59f88d2e746b3502ffa7e2d5" - } + "Value": "722dc4ea59f88d2e746b3502ffa7e2d5" }, { "Key": "graphql.http.request[0].query.hash", - "Value": { - "Value": "722dc4ea59f88d2e746b3502ffa7e2d5", - "AlgorithmName": "md5", - "Format": "Hex", - "IsEmpty": false - } + "Value": "722dc4ea59f88d2e746b3502ffa7e2d5" }, { "Key": "graphql.http.request[1].query.id", - "Value": { - "IsEmpty": false, - "HasValue": true, - "Value": "906bd3386334ff46441bf331680a58e4" - } + "Value": "906bd3386334ff46441bf331680a58e4" }, { "Key": "graphql.http.request[1].query.hash", - "Value": { - "Value": "906bd3386334ff46441bf331680a58e4", - "AlgorithmName": "md5", - "Format": "Hex", - "IsEmpty": false - } + "Value": "906bd3386334ff46441bf331680a58e4" } ], "event": [], From 754b43ae22b9c30abf09c89eb2b97893a712e9e6 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 15 Sep 2026 08:53:18 +0000 Subject: [PATCH 3/4] Add operation batch regression test for per-item request spans The existing coverage only exercised the JSON request batch path. An operation batch (?batchOperations=[A,B]) reaches StartOperationBatchRequest instead, a separate MarkAsBatch call site, so it needs its own default-scope snapshot. Add one for both the HotChocolate and Fusion listeners. --- .../ActivityServerDiagnosticListenerTests.cs | 30 + ...ery_Item_Its_Own_Request_Span_Default.snap | 314 +++++++++++ ..._Its_Own_Request_Span_Default_NET11_0.snap | 314 +++++++++++ ...onActivityServerDiagnosticListenerTests.cs | 39 ++ ...ery_Item_Its_Own_Request_Span_Default.snap | 511 ++++++++++++++++++ ..._Its_Own_Request_Span_Default_NET11_0.snap | 511 ++++++++++++++++++ 6 files changed, 1719 insertions(+) create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs index ca115135690..93c8508c6c9 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs @@ -185,6 +185,36 @@ public async Task Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_ } } + [Fact] + public async Task Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default() + { + using (CaptureActivities(out var activities)) + { + // arrange + // operation 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 request = + """ + {"query":"query A { hero { name } } query B { human(id: \"1000\") { name } }"} + """; + + // act + using var content = new StringContent(request, Encoding.UTF8, "application/json"); + using var response = await client.PostAsync( + "/graphql?batchOperations=[A,B]", + 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() { diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap new file mode 100644 index 00000000000..5977ff5c4dd --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap @@ -0,0 +1,314 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "url.query", + "Value": "?batchOperations=Redacted" + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP POST", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.http.kind", + "Value": "HttpPost" + }, + { + "Key": "graphql.schema.name", + "Value": "_Default" + }, + { + "Key": "graphql.http.request.type", + "Value": "operation_batch" + }, + { + "Key": "graphql.http.request.query.id", + "Value": "7da0832499d436dcd4788afc4e10d219" + }, + { + "Key": "graphql.http.request.query.hash", + "Value": "7da0832499d436dcd4788afc4e10d219" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Parse HTTP Request", + "DisplayName": "Parse HTTP Request", + "Kind": "Internal", + "Status": "Ok", + "tags": [], + "event": [] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:7da0832499d436dcd4788afc4e10d219" + } + ], + "event": [ + { + "Name": "AddedOperationToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "validate" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:7da0832499d436dcd4788afc4e10d219" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:7da0832499d436dcd4788afc4e10d219" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + } + ], + "event": [] + }, + { + "OperationName": "Query.hero", + "DisplayName": "Query.hero", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "hero" + }, + { + "Key": "graphql.field.path", + "Value": "hero" + }, + { + "Key": "graphql.field.name", + "Value": "hero" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Query.hero" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Query" + } + ], + "event": [] + } + ] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:7da0832499d436dcd4788afc4e10d219" + } + ], + "event": [ + { + "Name": "RetrievedDocumentFromCache", + "Tags": [] + }, + { + "Name": "AddedOperationToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:7da0832499d436dcd4788afc4e10d219" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + } + ], + "event": [] + }, + { + "OperationName": "Query.human", + "DisplayName": "Query.human", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "human" + }, + { + "Key": "graphql.field.path", + "Value": "human" + }, + { + "Key": "graphql.field.name", + "Value": "human" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Query.human" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Query" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap new file mode 100644 index 00000000000..c9dede45647 --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap @@ -0,0 +1,314 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "url.query", + "Value": "?batchOperations=Redacted" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 200 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP POST", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.http.kind", + "Value": "HttpPost" + }, + { + "Key": "graphql.schema.name", + "Value": "_Default" + }, + { + "Key": "graphql.http.request.type", + "Value": "operation_batch" + }, + { + "Key": "graphql.http.request.query.id", + "Value": "7da0832499d436dcd4788afc4e10d219" + }, + { + "Key": "graphql.http.request.query.hash", + "Value": "7da0832499d436dcd4788afc4e10d219" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Parse HTTP Request", + "DisplayName": "Parse HTTP Request", + "Kind": "Internal", + "Status": "Ok", + "tags": [], + "event": [] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:7da0832499d436dcd4788afc4e10d219" + } + ], + "event": [ + { + "Name": "AddedOperationToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "validate" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:7da0832499d436dcd4788afc4e10d219" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:7da0832499d436dcd4788afc4e10d219" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + } + ], + "event": [] + }, + { + "OperationName": "Query.hero", + "DisplayName": "Query.hero", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "hero" + }, + { + "Key": "graphql.field.path", + "Value": "hero" + }, + { + "Key": "graphql.field.name", + "Value": "hero" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Query.hero" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Query" + } + ], + "event": [] + } + ] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:7da0832499d436dcd4788afc4e10d219" + } + ], + "event": [ + { + "Name": "RetrievedDocumentFromCache", + "Tags": [] + }, + { + "Name": "AddedOperationToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:7da0832499d436dcd4788afc4e10d219" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + } + ], + "event": [] + }, + { + "OperationName": "Query.human", + "DisplayName": "Query.human", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "human" + }, + { + "Key": "graphql.field.path", + "Value": "human" + }, + { + "Key": "graphql.field.name", + "Value": "human" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Query.human" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Query" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs index 5f476177373..2d3182084e8 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs @@ -129,6 +129,45 @@ public async Task Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_ } } + [Fact] + public async Task Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default() + { + using (CaptureActivities(out var activities)) + { + // arrange + // operation 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 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [("a", server)], + configureGatewayBuilder: b => b + .ModifyServerOptions(o => o.Batching = AllowedBatching.All) + .AddInstrumentation()); + + using var client = gateway.CreateClient(); + client.BaseAddress = new Uri("http://localhost:5000"); + + const string request = + """ + {"query":"query A { sayHello } query B { greeting(name: \"x\") }"} + """; + + // act + using var content = new StringContent(request, Encoding.UTF8, "application/json"); + using var response = await client.PostAsync( + "/graphql?batchOperations=[A,B]", + content, + TestContext.Current.CancellationToken); + await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + // assert + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + [Fact] public async Task Http_Post_PersistedOperationEndpoint_Default() { diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap new file mode 100644 index 00000000000..f4faf6c2cf1 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default.snap @@ -0,0 +1,511 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "url.query", + "Value": "?batchOperations=Redacted" + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP POST", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.http.kind", + "Value": "HttpPost" + }, + { + "Key": "graphql.schema.name", + "Value": "_Default" + }, + { + "Key": "graphql.http.request.type", + "Value": "operation_batch" + }, + { + "Key": "graphql.http.request.query.id", + "Value": "397d1e0a5c74c2617a1768fe1d74398f" + }, + { + "Key": "graphql.http.request.query.hash", + "Value": "397d1e0a5c74c2617a1768fe1d74398f" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Parse HTTP Request", + "DisplayName": "Parse HTTP Request", + "Kind": "Internal", + "Status": "Ok", + "tags": [], + "event": [] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + } + ], + "event": [ + { + "Name": "AddedOperationPlanToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "validate" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Step Execution", + "DisplayName": "GraphQL Step Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "step_execute" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "eefbeb39e428f34e4cafb51dc2962aab9c52f20a624e8b14eaa879356bac923f" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "A_397d1e0a_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:41b2382275bbadb2e46e28b01fb2d329542b2d7180ffbad1bfc64f037b1d7c59" + } + ], + "event": [], + "activities": [ + { + "OperationName": "System.Net.Http.HttpRequestOut", + "DisplayName": "System.Net.Http.HttpRequestOut", + "Kind": "Client", + "Status": "Unset", + "tags": [ + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "url.full", + "Value": "http://localhost:5000/graphql" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [], + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [] + } + ] + } + ] + } + ] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + } + ], + "event": [ + { + "Name": "AddedOperationPlanToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "validate" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Step Execution", + "DisplayName": "GraphQL Step Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "step_execute" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "846735be35f1149bdf8507517b950a7bd3143dc0ccb2362ba624f36ed28d50f5" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "B_397d1e0a_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:e0913d474bf80cd0cf55adeec6c8a117cd795ac6fdb6798ccf5d5287a8429350" + } + ], + "event": [], + "activities": [ + { + "OperationName": "System.Net.Http.HttpRequestOut", + "DisplayName": "System.Net.Http.HttpRequestOut", + "Kind": "Client", + "Status": "Unset", + "tags": [ + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "url.full", + "Value": "http://localhost:5000/graphql" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [], + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap new file mode 100644 index 00000000000..dc9e71ee675 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own_Request_Span_Default_NET11_0.snap @@ -0,0 +1,511 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "url.query", + "Value": "?batchOperations=Redacted" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 200 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP POST", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.http.kind", + "Value": "HttpPost" + }, + { + "Key": "graphql.schema.name", + "Value": "_Default" + }, + { + "Key": "graphql.http.request.type", + "Value": "operation_batch" + }, + { + "Key": "graphql.http.request.query.id", + "Value": "397d1e0a5c74c2617a1768fe1d74398f" + }, + { + "Key": "graphql.http.request.query.hash", + "Value": "397d1e0a5c74c2617a1768fe1d74398f" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Parse HTTP Request", + "DisplayName": "Parse HTTP Request", + "Kind": "Internal", + "Status": "Ok", + "tags": [], + "event": [] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + } + ], + "event": [ + { + "Name": "AddedOperationPlanToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "validate" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Step Execution", + "DisplayName": "GraphQL Step Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "step_execute" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "A" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "eefbeb39e428f34e4cafb51dc2962aab9c52f20a624e8b14eaa879356bac923f" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "A_397d1e0a_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:41b2382275bbadb2e46e28b01fb2d329542b2d7180ffbad1bfc64f037b1d7c59" + } + ], + "event": [], + "activities": [ + { + "OperationName": "System.Net.Http.HttpRequestOut", + "DisplayName": "System.Net.Http.HttpRequestOut", + "Kind": "Client", + "Status": "Unset", + "tags": [ + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "url.full", + "Value": "http://localhost:5000/graphql" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [], + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 200 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [] + } + ] + } + ] + } + ] + }, + { + "OperationName": "GraphQL Operation", + "DisplayName": "query", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "request" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + } + ], + "event": [ + { + "Name": "AddedOperationPlanToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "validate" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "plan" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Step Execution", + "DisplayName": "GraphQL Step Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "step_execute" + }, + { + "Key": "graphql.operation.type", + "Value": "query" + }, + { + "Key": "graphql.operation.name", + "Value": "B" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:397d1e0a5c74c2617a1768fe1d74398f" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "846735be35f1149bdf8507517b950a7bd3143dc0ccb2362ba624f36ed28d50f5" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "B_397d1e0a_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:e0913d474bf80cd0cf55adeec6c8a117cd795ac6fdb6798ccf5d5287a8429350" + } + ], + "event": [], + "activities": [ + { + "OperationName": "System.Net.Http.HttpRequestOut", + "DisplayName": "System.Net.Http.HttpRequestOut", + "Kind": "Client", + "Status": "Unset", + "tags": [ + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "url.full", + "Value": "http://localhost:5000/graphql" + }, + { + "Key": "http.response.status_code", + "Value": 200 + } + ], + "event": [], + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "POST /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "POST" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 200 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] +} From 894de1ae8f9ae24a397a20814697917cc90f22cb Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 15 Sep 2026 08:57:36 +0000 Subject: [PATCH 4/4] Drop explanatory comments from batch span changes The code states the condition directly, so the surrounding prose added nothing. --- .../Diagnostics.Core/Spans/Http/ExecuteHttpRequestSpan.cs | 5 ----- .../Listeners/ActivityExecutionDiagnosticListener.cs | 5 ----- .../ActivityServerDiagnosticListenerTests.cs | 4 ---- .../FusionActivityExecutionDiagnosticEventListener.cs | 5 ----- .../FusionActivityServerDiagnosticListenerTests.cs | 4 ---- 5 files changed, 23 deletions(-) diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics.Core/Spans/Http/ExecuteHttpRequestSpan.cs b/src/HotChocolate/Diagnostics/src/Diagnostics.Core/Spans/Http/ExecuteHttpRequestSpan.cs index 4392a67ef86..b009cedcfbc 100644 --- a/src/HotChocolate/Diagnostics/src/Diagnostics.Core/Spans/Http/ExecuteHttpRequestSpan.cs +++ b/src/HotChocolate/Diagnostics/src/Diagnostics.Core/Spans/Http/ExecuteHttpRequestSpan.cs @@ -61,11 +61,6 @@ internal sealed class ExecuteHttpRequestSpan( return new ExecuteHttpRequestSpan(activity, httpContext, kind, enricher, options); } - /// - /// 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. - /// public bool IsBatch { get; private set; } public void MarkAsBatch() => IsBatch = true; diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityExecutionDiagnosticListener.cs b/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityExecutionDiagnosticListener.cs index 8b57dc14ee0..29d415a72a1 100644 --- a/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityExecutionDiagnosticListener.cs +++ b/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityExecutionDiagnosticListener.cs @@ -31,11 +31,6 @@ public override IDisposable ExecuteRequest(RequestContext context) return EmptyScope; } - // 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() is { IsBatch: false } httpRequestSpan) { httpContextActivity = httpRequestSpan.Activity; diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs index 93c8508c6c9..6ed773e0697 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs @@ -160,8 +160,6 @@ public async Task Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_ 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"); @@ -191,8 +189,6 @@ public async Task Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own using (CaptureActivities(out var activities)) { // arrange - // operation 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"); diff --git a/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityExecutionDiagnosticEventListener.cs b/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityExecutionDiagnosticEventListener.cs index 91ebb177f10..6a79732dd4b 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityExecutionDiagnosticEventListener.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityExecutionDiagnosticEventListener.cs @@ -27,11 +27,6 @@ public override IDisposable ExecuteRequest(RequestContext context) return EmptyScope; } - // 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() is { IsBatch: false } httpRequestSpan) { httpContextActivity = httpRequestSpan.Activity; diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs index 2d3182084e8..607ce19acb3 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs @@ -95,8 +95,6 @@ public async Task Http_Post_BatchRequest_Should_Give_Every_Item_Its_Own_Request_ 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 = CreateSourceSchema( "a", b => b.AddQueryType()); @@ -135,8 +133,6 @@ public async Task Http_Post_OperationBatchRequest_Should_Give_Every_Item_Its_Own using (CaptureActivities(out var activities)) { // arrange - // operation 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 = CreateSourceSchema( "a", b => b.AddQueryType());