From 04b27a8a56043a2a3c83aa0f95c9132f93f1f706 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:27:18 +0000 Subject: [PATCH 1/8] Add tests for WebSocket diagnostics --- .../ActivityServerDiagnosticListenerTests.cs | 251 ++++++++++++++ ...on_Event_Should_Be_Error_When_Timeout.snap | 270 +++++++++++++++ ..._Should_Be_Error_When_Timeout_NET11_0.snap | 270 +++++++++++++++ ...d_Be_Ok_When_Client_Closes_Connection.snap | 248 ++++++++++++++ ...When_Client_Closes_Connection_NET11_0.snap | 248 ++++++++++++++ ...on_Should_Be_Ok_When_Client_Completes.snap | 248 ++++++++++++++ ...d_Be_Ok_When_Client_Completes_NET11_0.snap | 248 ++++++++++++++ ...on_Should_Be_Ok_When_Server_Completes.snap | 248 ++++++++++++++ ...d_Be_Ok_When_Server_Completes_NET11_0.snap | 248 ++++++++++++++ ...onActivityServerDiagnosticListenerTests.cs | 217 ++++++++++++ ...d_Be_Ok_When_Client_Closes_Connection.snap | 313 ++++++++++++++++++ ...When_Client_Closes_Connection_NET11_0.snap | 313 ++++++++++++++++++ ...on_Should_Be_Ok_When_Client_Completes.snap | 313 ++++++++++++++++++ ...d_Be_Ok_When_Client_Completes_NET11_0.snap | 313 ++++++++++++++++++ ...on_Should_Be_Ok_When_Server_Completes.snap | 313 ++++++++++++++++++ ...d_Be_Ok_When_Server_Completes_NET11_0.snap | 313 ++++++++++++++++++ 16 files changed, 4374 insertions(+) create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Event_Should_Be_Error_When_Timeout.snap create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Event_Should_Be_Error_When_Timeout_NET11_0.snap create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection.snap create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection_NET11_0.snap create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes.snap create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_NET11_0.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection_NET11_0.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_NET11_0.snap diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs index d0f0f702df7..0991542c907 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs @@ -1,4 +1,5 @@ using System.Net.Http.Headers; +using System.Net.WebSockets; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.TestHost; @@ -9,6 +10,8 @@ using HotChocolate.Resolvers; using HotChocolate.Subscriptions; using HotChocolate.Transport.Http; +using HotChocolate.Transport.Sockets; +using HotChocolate.Transport.Sockets.Client; using HotChocolate.Types; using static CookieCrumble.TestEnvironment; using static HotChocolate.Diagnostics.ActivityTestHelper; @@ -20,6 +23,7 @@ namespace HotChocolate.Diagnostics; public class ActivityServerDiagnosticListenerTests(TestServerFactory serverFactory) : ServerTestBase(serverFactory) { private static readonly Uri s_url = new("http://localhost:5000/graphql"); + private static readonly Uri s_webSocketUrl = new("ws://localhost:5000/graphql"); [Fact] public async Task Http_Post_SingleRequest_GetHeroName_Default() @@ -744,6 +748,253 @@ public async Task Http_Subscription_Event_Should_Be_Error_When_Timeout() } } + [Fact] + public async Task WebSocket_Subscription_Should_Be_Ok_When_Server_Completes() + { + using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using (CaptureActivities(out var activities)) + { + // arrange + var signal = new HttpSubscriptionSignal(); + using var server = CreateInstrumentedServer( + o => o.Scopes = ActivityScopes.All, + b => b + .AddTypeExtension() + .Services.AddSingleton(signal)); + using var webSocket = await ConnectWebSocketAsync(server, guard.Token); + await using var client = await SocketClient.ConnectAsync(webSocket, guard.Token); + var sender = server.Services.GetRequiredService(); + + var request = new OperationRequest("subscription OnMessageSubscription { onMessage }"); + + using var result = await client.ExecuteAsync(request, guard.Token); + var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); + + // act + // wait until the server subscribed to the topic, push one event, then + // complete the topic so the server ends the operation with a `complete` + // message and the client observes a clean, graceful close + try + { + var moveNext = results.MoveNextAsync().AsTask(); + await signal.Subscribed.Task.WaitAsync(guard.Token); + await sender.SendAsync("OnMessage", "hello", guard.Token); + Assert.True(await moveNext); + await sender.CompleteAsync("OnMessage"); + Assert.False(await results.MoveNextAsync()); + } + finally + { + await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); + } + + // the WebSocket session encloses every span of this trace, so close it + // before the trace is read + await CloseWebSocketAsync(webSocket, guard.Token); + + // assert + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + + [Fact] + public async Task WebSocket_Subscription_Should_Be_Ok_When_Client_Completes() + { + using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using (CaptureActivities(out var activities)) + { + // arrange + var signal = new HttpSubscriptionSignal(); + using var server = CreateInstrumentedServer( + o => o.Scopes = ActivityScopes.All, + b => b + .AddTypeExtension() + .Services.AddSingleton(signal)); + using var webSocket = await ConnectWebSocketAsync(server, guard.Token); + await using var client = await SocketClient.ConnectAsync(webSocket, guard.Token); + var sender = server.Services.GetRequiredService(); + + var request = new OperationRequest("subscription OnMessageSubscription { onMessage }"); + + using var result = await client.ExecuteAsync(request, guard.Token); + var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); + + // receive one event successfully while the subscription is running + var moveNext = results.MoveNextAsync().AsTask(); + await signal.Subscribed.Task.WaitAsync(guard.Token); + await sender.SendAsync("OnMessage", "hello", guard.Token); + Assert.True(await moveNext); + + // act + // stop the subscription from the client (unsubscribe) while the socket + // stays open; disposing the stream sends a `complete` message + await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); + + // the `complete` message is delivered before the close frame, so the server + // stops the operation first and then ends the session + await CloseWebSocketAsync(webSocket, guard.Token); + + // assert + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + + [Fact] + public async Task WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection() + { + using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using (CaptureActivities(out var activities)) + { + // arrange + var signal = new HttpSubscriptionSignal(); + using var server = CreateInstrumentedServer( + o => o.Scopes = ActivityScopes.All, + b => b + .AddTypeExtension() + .Services.AddSingleton(signal)); + using var webSocket = await ConnectWebSocketAsync(server, guard.Token); + await using var client = await SocketClient.ConnectAsync(webSocket, guard.Token); + var sender = server.Services.GetRequiredService(); + + var request = new OperationRequest("subscription OnMessageSubscription { onMessage }"); + + using var result = await client.ExecuteAsync(request, guard.Token); + var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); + + // receive one event successfully while the connection is alive + var moveNext = results.MoveNextAsync().AsTask(); + await signal.Subscribed.Task.WaitAsync(guard.Token); + await sender.SendAsync("OnMessage", "hello", guard.Token); + Assert.True(await moveNext); + + // act + // the subscription is now idle, waiting for the next event. close the + // connection (close the tab) without unsubscribing first, so the server + // has to tear the still-running subscription down with the session. + await CloseWebSocketAsync(webSocket, guard.Token); + await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); + + // assert + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + + [Fact] + public async Task WebSocket_Subscription_Event_Should_Be_Error_When_Timeout() + { + using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using (CaptureActivities(out var activities)) + { + // arrange + // a blocking resolver combined with a tiny per-event timeout forces a + // server-side event timeout (not a client abort): the socket stays open + var signal = new HttpSubscriptionSignal(); + using var server = CreateInstrumentedServer( + o => o.Scopes = ActivityScopes.All, + b => b + .AddTypeExtension() + .ModifyRequestOptions(o => o.ExecutionTimeout = TimeSpan.FromMilliseconds(200)) + .Services.AddSingleton(signal)); + using var webSocket = await ConnectWebSocketAsync(server, guard.Token); + await using var client = await SocketClient.ConnectAsync(webSocket, guard.Token); + var sender = server.Services.GetRequiredService(); + + var request = new OperationRequest( + "subscription OnBlockingMessageSubscription { onBlockingMessage }"); + + using var result = await client.ExecuteAsync(request, guard.Token); + var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); + + // act + // start processing an event that blocks past the per-event timeout; the + // timeout tears the operation down, so the server ends the stream without + // ever delivering a result + var first = results.MoveNextAsync().AsTask(); + await signal.Subscribed.Task.WaitAsync(guard.Token); + await sender.SendAsync("OnBlockingMessage", "hello", guard.Token); + await signal.Entered.Task.WaitAsync(guard.Token); + + Assert.False(await first); + + await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); + await CloseWebSocketAsync(webSocket, guard.Token); + + // assert + // the snapshot records the subscription event span status for a + // server-side event timeout + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + + private static async Task ConnectWebSocketAsync( + TestServer server, + CancellationToken cancellationToken) + { + var webSocketClient = server.CreateWebSocketClient(); + webSocketClient.ConfigureRequest = + r => r.Headers.SecWebSocketProtocol = WellKnownProtocols.GraphQL_Transport_WS; + return await webSocketClient.ConnectAsync(s_webSocketUrl, cancellationToken); + } + + private static async Task CloseWebSocketAsync( + WebSocket webSocket, + CancellationToken cancellationToken) + { + if (webSocket.State is not WebSocketState.Open) + { + // the server already ended the session, so there is nothing left to close + return; + } + + try + { + await webSocket.CloseAsync( + WebSocketCloseStatus.NormalClosure, + "done", + cancellationToken); + } + catch (WebSocketException) + { + // expected: the server may have torn the connection down already + } + catch (OperationCanceledException) + { + // expected: the close handshake was aborted + } + catch (ObjectDisposedException) + { + // expected: the server tore the session down while the close was in flight + } + } + + private static async Task IgnoreSocketTeardownAsync(Task task) + { + try + { + await task; + } + catch (OperationCanceledException) + { + // expected: the streamed read was aborted by the client + } + catch (IOException) + { + // expected: aborting an in-flight read can surface as an I/O failure + } + catch (WebSocketException) + { + // expected: the socket was aborted while a read was in flight + } + catch (SocketClosedException) + { + // expected: the client dropped the connection without a close frame + } + } + private static async Task DrainAsync( IAsyncEnumerator results, CancellationToken cancellationToken) diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Event_Should_Be_Error_When_Timeout.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Event_Should_Be_Error_When_Timeout.snap new file mode 100644 index 00000000000..5caa6a73a68 --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Event_Should_Be_Error_When_Timeout.snap @@ -0,0 +1,270 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "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": 101 + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnBlockingMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:41faa6dbd0de29d0cdf705c602b506bf" + } + ], + "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:41faa6dbd0de29d0cdf705c602b506bf" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Complexity Analysis", + "DisplayName": "GraphQL Complexity Analysis", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.hash", + "Value": "md5:41faa6dbd0de29d0cdf705c602b506bf" + }, + { + "Key": "graphql.operation.fieldCost", + "Value": 10.0 + }, + { + "Key": "graphql.operation.typeCost", + "Value": 1.0 + } + ], + "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:41faa6dbd0de29d0cdf705c602b506bf" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnBlockingMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnBlockingMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:41faa6dbd0de29d0cdf705c602b506bf" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Error", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnBlockingMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:41faa6dbd0de29d0cdf705c602b506bf" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + }, + { + "Key": "error.type", + "Value": "System.OperationCanceledException" + } + ], + "event": [ + { + "Name": "exception", + "Tags": [ + { + "Key": "exception.message", + "Value": "The operation was canceled." + }, + { + "Key": "exception.stacktrace", + "Value": "System.OperationCanceledException: The operation was canceled.\n at System.Threading.CancellationToken.ThrowOperationCanceledException()\n at System.Threading.CancellationToken.ThrowIfCancellationRequested()\n at HotChocolate.Execution.Processing.WorkScheduler.ExecuteInternalAsync(IExecutionTask[] buffer) in WorkScheduler.Execute.cs\n at HotChocolate.Execution.Processing.WorkScheduler.ExecuteAsync1() in WorkScheduler.Execute.cs\n at HotChocolate.Execution.Processing.QueryExecutor.ExecuteNoIncrementalAsync(OperationContext operationContext, IImmutableDictionary`2 scopedContext) in QueryExecutor.cs\n at HotChocolate.Execution.Processing.SubscriptionExecutor.Subscription.OnEvent(Object payload) in SubscriptionExecutor.Subscription.cs" + }, + { + "Key": "exception.type", + "Value": "System.OperationCanceledException" + } + ] + } + ], + "activities": [ + { + "OperationName": "Subscription.onBlockingMessage", + "DisplayName": "Subscription.onBlockingMessage", + "Kind": "Internal", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onBlockingMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onBlockingMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onBlockingMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onBlockingMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Event_Should_Be_Error_When_Timeout_NET11_0.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Event_Should_Be_Error_When_Timeout_NET11_0.snap new file mode 100644 index 00000000000..03fc8c10618 --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Event_Should_Be_Error_When_Timeout_NET11_0.snap @@ -0,0 +1,270 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 101 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnBlockingMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:41faa6dbd0de29d0cdf705c602b506bf" + } + ], + "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:41faa6dbd0de29d0cdf705c602b506bf" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Complexity Analysis", + "DisplayName": "GraphQL Complexity Analysis", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.hash", + "Value": "md5:41faa6dbd0de29d0cdf705c602b506bf" + }, + { + "Key": "graphql.operation.fieldCost", + "Value": 10.0 + }, + { + "Key": "graphql.operation.typeCost", + "Value": 1.0 + } + ], + "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:41faa6dbd0de29d0cdf705c602b506bf" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnBlockingMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnBlockingMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:41faa6dbd0de29d0cdf705c602b506bf" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Error", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnBlockingMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:41faa6dbd0de29d0cdf705c602b506bf" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + }, + { + "Key": "error.type", + "Value": "System.OperationCanceledException" + } + ], + "event": [ + { + "Name": "exception", + "Tags": [ + { + "Key": "exception.message", + "Value": "The operation was canceled." + }, + { + "Key": "exception.stacktrace", + "Value": "System.OperationCanceledException: The operation was canceled.\n at System.Threading.CancellationToken.ThrowOperationCanceledException()\n at System.Threading.CancellationToken.ThrowIfCancellationRequested()\n at HotChocolate.Execution.Processing.WorkScheduler.ExecuteInternalAsync(IExecutionTask[] buffer) in WorkScheduler.Execute.cs\n at HotChocolate.Execution.Processing.WorkScheduler.ExecuteAsync1() in WorkScheduler.Execute.cs\n at HotChocolate.Execution.Processing.QueryExecutor.ExecuteNoIncrementalAsync(OperationContext operationContext, IImmutableDictionary`2 scopedContext) in QueryExecutor.cs\n at HotChocolate.Execution.Processing.SubscriptionExecutor.Subscription.OnEvent(Object payload) in SubscriptionExecutor.Subscription.cs" + }, + { + "Key": "exception.type", + "Value": "System.OperationCanceledException" + } + ] + } + ], + "activities": [ + { + "OperationName": "Subscription.onBlockingMessage", + "DisplayName": "Subscription.onBlockingMessage", + "Kind": "Internal", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onBlockingMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onBlockingMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onBlockingMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onBlockingMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection.snap new file mode 100644 index 00000000000..95bcd1d95d2 --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection.snap @@ -0,0 +1,248 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "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": 101 + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Complexity Analysis", + "DisplayName": "GraphQL Complexity Analysis", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.fieldCost", + "Value": 0.0 + }, + { + "Key": "graphql.operation.typeCost", + "Value": 1.0 + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection_NET11_0.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection_NET11_0.snap new file mode 100644 index 00000000000..cd7dc12365c --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection_NET11_0.snap @@ -0,0 +1,248 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 101 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Complexity Analysis", + "DisplayName": "GraphQL Complexity Analysis", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.fieldCost", + "Value": 0.0 + }, + { + "Key": "graphql.operation.typeCost", + "Value": 1.0 + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap new file mode 100644 index 00000000000..95bcd1d95d2 --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap @@ -0,0 +1,248 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "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": 101 + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Complexity Analysis", + "DisplayName": "GraphQL Complexity Analysis", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.fieldCost", + "Value": 0.0 + }, + { + "Key": "graphql.operation.typeCost", + "Value": 1.0 + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap new file mode 100644 index 00000000000..cd7dc12365c --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap @@ -0,0 +1,248 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 101 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Complexity Analysis", + "DisplayName": "GraphQL Complexity Analysis", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.fieldCost", + "Value": 0.0 + }, + { + "Key": "graphql.operation.typeCost", + "Value": 1.0 + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes.snap new file mode 100644 index 00000000000..95bcd1d95d2 --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes.snap @@ -0,0 +1,248 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "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": 101 + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Complexity Analysis", + "DisplayName": "GraphQL Complexity Analysis", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.fieldCost", + "Value": 0.0 + }, + { + "Key": "graphql.operation.typeCost", + "Value": 1.0 + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_NET11_0.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_NET11_0.snap new file mode 100644 index 00000000000..cd7dc12365c --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_NET11_0.snap @@ -0,0 +1,248 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 101 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Complexity Analysis", + "DisplayName": "GraphQL Complexity Analysis", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.fieldCost", + "Value": 0.0 + }, + { + "Key": "graphql.operation.typeCost", + "Value": 1.0 + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs index b63be71e359..5d3937f30bf 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs @@ -1,3 +1,4 @@ +using System.Net.WebSockets; using System.Runtime.CompilerServices; using System.Text; using HotChocolate.Diagnostics; @@ -6,6 +7,8 @@ using HotChocolate.PersistedOperations; using HotChocolate.Resolvers; using HotChocolate.Transport.Http; +using HotChocolate.Transport.Sockets; +using HotChocolate.Transport.Sockets.Client; using HotChocolate.Types; using HotChocolate.Types.Composite; using HotChocolate.Types.Relay; @@ -21,6 +24,7 @@ namespace HotChocolate.Fusion.Diagnostics; public class FusionActivityServerDiagnosticListenerTests : FusionTestBase { private static readonly Uri s_url = new("http://localhost:5000/graphql"); + private static readonly Uri s_webSocketUrl = new("ws://localhost:5000/graphql"); [Fact] public async Task Http_Post_Single_Request_Default() @@ -748,6 +752,219 @@ public async Task Http_Subscription_Should_Be_Unset_When_Client_Disconnects_Duri } } + [Fact] + public async Task WebSocket_Subscription_Should_Be_Ok_When_Server_Completes() + { + using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b + .AddQueryType() + .AddSubscriptionType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + o.Scopes = FusionActivityScopes.All)); + + using var webSocket = await ConnectWebSocketAsync(gateway, guard.Token); + await using var client = await SocketClient.ConnectAsync(webSocket, guard.Token); + + var request = new OperationRequest("subscription OnMessageSubscription { onMessage }"); + + using var result = await client.ExecuteAsync(request, guard.Token); + var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); + + // act + // the subgraph emits one event then completes its stream, so receive the + // single event and then let the gateway end the operation with a + // `complete` message (no exception, no abort) + try + { + Assert.True(await results.MoveNextAsync()); + Assert.False(await results.MoveNextAsync()); + } + finally + { + await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); + } + + // the WebSocket session encloses every span of this trace, so close it + // before the trace is read + await CloseWebSocketAsync(webSocket, guard.Token); + + // assert + // the snapshot records the gateway request and subscription event span + // status for a graceful close + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + + [Fact] + public async Task WebSocket_Subscription_Should_Be_Ok_When_Client_Completes() + { + using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b + .AddQueryType() + .AddSubscriptionType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + o.Scopes = FusionActivityScopes.All)); + + using var webSocket = await ConnectWebSocketAsync(gateway, guard.Token); + await using var client = await SocketClient.ConnectAsync(webSocket, guard.Token); + + var request = new OperationRequest("subscription OnIdleMessageSubscription { onIdleMessage }"); + + using var result = await client.ExecuteAsync(request, guard.Token); + var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); + + // receive one event successfully while the subscription is running + Assert.True(await results.MoveNextAsync()); + + // act + // stop the subscription from the client (unsubscribe) while the socket + // stays open; disposing the stream sends a `complete` message + await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); + + // the `complete` message is delivered before the close frame, so the + // gateway stops the operation first and then ends the session + await CloseWebSocketAsync(webSocket, guard.Token); + + // assert + // the snapshot records the subscription event span status for a + // client-initiated unsubscribe + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + + [Fact] + public async Task WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection() + { + using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b + .AddQueryType() + .AddSubscriptionType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + o.Scopes = FusionActivityScopes.All)); + + using var webSocket = await ConnectWebSocketAsync(gateway, guard.Token); + await using var client = await SocketClient.ConnectAsync(webSocket, guard.Token); + + var request = new OperationRequest("subscription OnIdleMessageSubscription { onIdleMessage }"); + + using var result = await client.ExecuteAsync(request, guard.Token); + var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); + + // receive one event successfully while the connection is alive + Assert.True(await results.MoveNextAsync()); + + // act + // the subscription is now idle, waiting for the next event. close the + // connection (close the tab) without unsubscribing first, so the gateway + // has to tear the still-running subscription down with the session. + await CloseWebSocketAsync(webSocket, guard.Token); + await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); + + // assert + // the snapshot records the subscription event span status for a client + // that closes the connection while the subscription is idle + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + + private static async Task ConnectWebSocketAsync( + Gateway gateway, + CancellationToken cancellationToken) + { + var webSocketClient = gateway.CreateWebSocketClient(); + webSocketClient.ConfigureRequest = + r => r.Headers.SecWebSocketProtocol = WellKnownProtocols.GraphQL_Transport_WS; + return await webSocketClient.ConnectAsync(s_webSocketUrl, cancellationToken); + } + + private static async Task CloseWebSocketAsync( + WebSocket webSocket, + CancellationToken cancellationToken) + { + if (webSocket.State is not WebSocketState.Open) + { + // the gateway already ended the session, so there is nothing left to close + return; + } + + try + { + await webSocket.CloseAsync( + WebSocketCloseStatus.NormalClosure, + "done", + cancellationToken); + } + catch (WebSocketException) + { + // expected: the gateway may have torn the connection down already + } + catch (OperationCanceledException) + { + // expected: the close handshake was aborted + } + catch (ObjectDisposedException) + { + // expected: the gateway tore the session down while the close was in flight + } + } + + private static async Task IgnoreSocketTeardownAsync(Task task) + { + try + { + await task; + } + catch (OperationCanceledException) + { + // expected: the streamed read was aborted by the client + } + catch (IOException) + { + // expected: aborting an in-flight read can surface as an I/O failure + } + catch (WebSocketException) + { + // expected: the socket was closed while a read was in flight + } + catch (SocketClosedException) + { + // expected: the client dropped the connection without a close frame + } + } + private static async Task IgnoreCancellationAsync(Task task) { try diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection.snap new file mode 100644 index 00000000000..610f071d287 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection.snap @@ -0,0 +1,313 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "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": 101 + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + } + ], + "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:51d9b3bca5722d3209429e79f8ee6435" + } + ], + "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:51d9b3bca5722d3209429e79f8ee6435" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + } + ], + "event": [], + "activities": [ + { + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "8fef749a1b041aca849173d1dd38a6022e142b1ebbf92aeb666766e3a0654610" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "OnIdleMessageSubscription_51d9b3bc_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:6b6ed56d2f50e7e614c9096e8e46d789db0e605355b6baf21ab088bde107ca0e" + } + ], + "event": [] + } + ] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + }, + { + "Key": "graphql.subscription.id", + "Value": "" + } + ], + "event": [] + } + ] + }, + { + "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.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection_NET11_0.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection_NET11_0.snap new file mode 100644 index 00000000000..9dfe166518d --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connection_NET11_0.snap @@ -0,0 +1,313 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 101 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + } + ], + "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:51d9b3bca5722d3209429e79f8ee6435" + } + ], + "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:51d9b3bca5722d3209429e79f8ee6435" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + } + ], + "event": [], + "activities": [ + { + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "8fef749a1b041aca849173d1dd38a6022e142b1ebbf92aeb666766e3a0654610" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "OnIdleMessageSubscription_51d9b3bc_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:6b6ed56d2f50e7e614c9096e8e46d789db0e605355b6baf21ab088bde107ca0e" + } + ], + "event": [] + } + ] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + }, + { + "Key": "graphql.subscription.id", + "Value": "" + } + ], + "event": [] + } + ] + }, + { + "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": [] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap new file mode 100644 index 00000000000..610f071d287 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap @@ -0,0 +1,313 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "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": 101 + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + } + ], + "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:51d9b3bca5722d3209429e79f8ee6435" + } + ], + "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:51d9b3bca5722d3209429e79f8ee6435" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + } + ], + "event": [], + "activities": [ + { + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "8fef749a1b041aca849173d1dd38a6022e142b1ebbf92aeb666766e3a0654610" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "OnIdleMessageSubscription_51d9b3bc_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:6b6ed56d2f50e7e614c9096e8e46d789db0e605355b6baf21ab088bde107ca0e" + } + ], + "event": [] + } + ] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + }, + { + "Key": "graphql.subscription.id", + "Value": "" + } + ], + "event": [] + } + ] + }, + { + "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.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap new file mode 100644 index 00000000000..9dfe166518d --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap @@ -0,0 +1,313 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 101 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + } + ], + "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:51d9b3bca5722d3209429e79f8ee6435" + } + ], + "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:51d9b3bca5722d3209429e79f8ee6435" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + } + ], + "event": [], + "activities": [ + { + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "8fef749a1b041aca849173d1dd38a6022e142b1ebbf92aeb666766e3a0654610" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "OnIdleMessageSubscription_51d9b3bc_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:6b6ed56d2f50e7e614c9096e8e46d789db0e605355b6baf21ab088bde107ca0e" + } + ], + "event": [] + } + ] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnIdleMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + }, + { + "Key": "graphql.subscription.id", + "Value": "" + } + ], + "event": [] + } + ] + }, + { + "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": [] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes.snap new file mode 100644 index 00000000000..1e77d211eae --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes.snap @@ -0,0 +1,313 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "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": 101 + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "bb94346d204b2f13a21dcba79a06b47065115db28b9ba2c04bce67382b50cef0" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "OnMessageSubscription_ef859b98_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:f9abddbb651ad8e118793eb7c4a7c8db7fe43cae95faba710a0ff35b26b17136" + } + ], + "event": [] + } + ] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "" + } + ], + "event": [] + } + ] + }, + { + "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.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_NET11_0.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_NET11_0.snap new file mode 100644 index 00000000000..d7c24ff2863 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_NET11_0.snap @@ -0,0 +1,313 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 101 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "bb94346d204b2f13a21dcba79a06b47065115db28b9ba2c04bce67382b50cef0" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "OnMessageSubscription_ef859b98_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:f9abddbb651ad8e118793eb7c4a7c8db7fe43cae95faba710a0ff35b26b17136" + } + ], + "event": [] + } + ] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "" + } + ], + "event": [] + } + ] + }, + { + "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 f7c5423ee974170d3cc27c4182c77ca67ad355ea Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:27:23 +0000 Subject: [PATCH 2/8] Fix .NET 11 build --- .../Core/test/Types.Tests/Resolvers/ResolverCompilerTests.cs | 2 ++ .../Types.Tests/Types/Descriptors/DescriptorContextTests.cs | 4 ++++ .../src/Utilities/DependencyInjection/EmptyServiceProvider.cs | 4 ++++ .../Utilities/test/Utilities.Tests/MiddlewareCompilerTests.cs | 3 +++ 4 files changed, 13 insertions(+) diff --git a/src/HotChocolate/Core/test/Types.Tests/Resolvers/ResolverCompilerTests.cs b/src/HotChocolate/Core/test/Types.Tests/Resolvers/ResolverCompilerTests.cs index 9b07b50af26..ca722e1deb6 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Resolvers/ResolverCompilerTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Resolvers/ResolverCompilerTests.cs @@ -9,7 +9,9 @@ using HotChocolate.Tests; using HotChocolate.Types; using HotChocolate.Types.Descriptors; +#if !NET11_0_OR_GREATER using HotChocolate.Utilities; +#endif using Moq; namespace HotChocolate.Resolvers; diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Descriptors/DescriptorContextTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Descriptors/DescriptorContextTests.cs index 50174213574..207ee794cdd 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Descriptors/DescriptorContextTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Descriptors/DescriptorContextTests.cs @@ -1,6 +1,10 @@ using HotChocolate.Configuration; using HotChocolate.Features; +#if NET11_0_OR_GREATER +using Microsoft.Extensions.DependencyInjection; +#else using HotChocolate.Utilities; +#endif namespace HotChocolate.Types.Descriptors; diff --git a/src/HotChocolate/Utilities/src/Utilities/DependencyInjection/EmptyServiceProvider.cs b/src/HotChocolate/Utilities/src/Utilities/DependencyInjection/EmptyServiceProvider.cs index 43cf41f0112..642000723f1 100644 --- a/src/HotChocolate/Utilities/src/Utilities/DependencyInjection/EmptyServiceProvider.cs +++ b/src/HotChocolate/Utilities/src/Utilities/DependencyInjection/EmptyServiceProvider.cs @@ -1,3 +1,6 @@ +// .NET 11 ships Microsoft.Extensions.DependencyInjection.EmptyServiceProvider, so callers +// resolve that type on newer targets instead. +#if !NET11_0_OR_GREATER using Microsoft.Extensions.DependencyInjection; namespace HotChocolate.Utilities; @@ -14,3 +17,4 @@ private EmptyServiceProvider() public static EmptyServiceProvider Instance { get; } = new(); } +#endif diff --git a/src/HotChocolate/Utilities/test/Utilities.Tests/MiddlewareCompilerTests.cs b/src/HotChocolate/Utilities/test/Utilities.Tests/MiddlewareCompilerTests.cs index a674806b13e..a62bcdbe460 100644 --- a/src/HotChocolate/Utilities/test/Utilities.Tests/MiddlewareCompilerTests.cs +++ b/src/HotChocolate/Utilities/test/Utilities.Tests/MiddlewareCompilerTests.cs @@ -1,4 +1,7 @@ using System.Linq.Expressions; +#if NET11_0_OR_GREATER +using Microsoft.Extensions.DependencyInjection; +#endif namespace HotChocolate.Utilities; From 771e3b1934d5973ad132188b47bb42f61b85e5a2 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 14 Sep 2026 13:13:47 +0000 Subject: [PATCH 3/8] wip --- .../AggregateServerDiagnosticEventListener.cs | 12 + .../IServerDiagnosticEvents.cs | 17 + .../NoopServerDiagnosticEventListener.cs | 8 + .../ServerDiagnosticEventListener.cs | 9 + .../ApolloSubscriptionProtocolHandler.cs | 7 +- .../GraphQLOverWebSocketProtocolHandler.cs | 5 +- ...rviceCollectionExtensions.Subscriptions.cs | 7 +- .../Diagnostics.Core/ActivityEnricherBase.cs | 12 + .../ActivityServerDiagnosticListener.cs | 7 + .../ActivityServerDiagnosticListenerTests.cs | 247 ++++++++++++++ ...ded_As_Tag_To_Request_And_Event_Spans.snap | 256 ++++++++++++++ ...ag_To_Request_And_Event_Spans_NET11_0.snap | 256 ++++++++++++++ ...ded_As_Tag_To_Request_And_Event_Spans.snap | 256 ++++++++++++++ ...ag_To_Request_And_Event_Spans_NET11_0.snap | 256 ++++++++++++++ ...d_Be_Ok_When_Server_Completes_Default.snap | 156 +++++++++ ...When_Server_Completes_Default_NET11_0.snap | 156 +++++++++ ...FusionServerServiceCollectionExtensions.cs | 6 +- .../FusionActivityServerDiagnosticListener.cs | 7 + ...onActivityServerDiagnosticListenerTests.cs | 164 +++++++++ ...ded_As_Tag_To_Request_And_Event_Spans.snap | 321 ++++++++++++++++++ ...ag_To_Request_And_Event_Spans_NET11_0.snap | 321 ++++++++++++++++++ ...d_Be_Ok_When_Server_Completes_Default.snap | 242 +++++++++++++ ...When_Server_Completes_Default_NET11_0.snap | 242 +++++++++++++ 23 files changed, 2964 insertions(+), 6 deletions(-) create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/AggregateServerDiagnosticEventListener.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/AggregateServerDiagnosticEventListener.cs index 3fd66e60177..2e0a6110b91 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/AggregateServerDiagnosticEventListener.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/AggregateServerDiagnosticEventListener.cs @@ -1,4 +1,6 @@ using Microsoft.AspNetCore.Http; +using HotChocolate.AspNetCore.Subscriptions; +using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.Language; namespace HotChocolate.AspNetCore.Instrumentation; @@ -111,6 +113,16 @@ public IDisposable WebSocketSession(HttpContext context) return new AggregateActivityScope(scopes); } + public void WebSocketConnectionInitialized( + ISocketSession session, + IOperationMessagePayload connectionInitMessage) + { + for (var i = 0; i < _listeners.Length; i++) + { + _listeners[i].WebSocketConnectionInitialized(session, connectionInitMessage); + } + } + public void WebSocketSessionError(HttpContext context, Exception exception) { for (var i = 0; i < _listeners.Length; i++) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/IServerDiagnosticEvents.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/IServerDiagnosticEvents.cs index 781460bcdb1..52ea3bc9006 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/IServerDiagnosticEvents.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/IServerDiagnosticEvents.cs @@ -1,4 +1,6 @@ using Microsoft.AspNetCore.Http; +using HotChocolate.AspNetCore.Subscriptions; +using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.Language; namespace HotChocolate.AspNetCore.Instrumentation; @@ -138,6 +140,21 @@ void StartOperationBatchRequest( /// IDisposable WebSocketSession(HttpContext context); + /// + /// Called within the scope once the client's connection + /// initialization message has been accepted. + /// + /// + /// The GraphQL socket session. + /// + /// + /// The connection initialization message sent by the client. Its payload is only valid + /// for the duration of this call. + /// + void WebSocketConnectionInitialized( + ISocketSession session, + IOperationMessagePayload connectionInitMessage); + /// /// Called within the scope and signals /// that an error occurred that terminated the session. diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/NoopServerDiagnosticEventListener.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/NoopServerDiagnosticEventListener.cs index 2270cb61880..72c9df2d215 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/NoopServerDiagnosticEventListener.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/NoopServerDiagnosticEventListener.cs @@ -1,4 +1,6 @@ using Microsoft.AspNetCore.Http; +using HotChocolate.AspNetCore.Subscriptions; +using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.Language; using static HotChocolate.AspNetCore.Instrumentation.ServerDiagnosticEventListener; @@ -41,6 +43,12 @@ public void ParserErrors(HttpContext context, IReadOnlyList errors) public IDisposable WebSocketSession(HttpContext context) => EmptyScope; + public void WebSocketConnectionInitialized( + ISocketSession session, + IOperationMessagePayload connectionInitMessage) + { + } + public void WebSocketSessionError(HttpContext context, Exception exception) { } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/ServerDiagnosticEventListener.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/ServerDiagnosticEventListener.cs index ee6e9db0de0..5f1772cd182 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/ServerDiagnosticEventListener.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/ServerDiagnosticEventListener.cs @@ -1,4 +1,6 @@ using Microsoft.AspNetCore.Http; +using HotChocolate.AspNetCore.Subscriptions; +using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.Language; namespace HotChocolate.AspNetCore.Instrumentation; @@ -64,6 +66,13 @@ public virtual IDisposable FormatHttpResponse(HttpContext context, OperationResu public virtual IDisposable WebSocketSession(HttpContext context) => EmptyScope; + /// + public virtual void WebSocketConnectionInitialized( + ISocketSession session, + IOperationMessagePayload connectionInitMessage) + { + } + /// public virtual void WebSocketSessionError(HttpContext context, Exception exception) { diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs index 8a5372ff28b..1d54c295e30 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices; using System.Text.Json; using HotChocolate.AspNetCore.Formatters; +using HotChocolate.AspNetCore.Instrumentation; using HotChocolate.Buffers; using HotChocolate.Language; using HotChocolate.Text.Json; @@ -18,13 +19,16 @@ internal sealed class ApolloSubscriptionProtocolHandler : IProtocolHandler { private readonly ISocketSessionInterceptor _interceptor; private readonly IWebSocketPayloadFormatter _formatter; + private readonly IServerDiagnosticEvents _diagnosticEvents; public ApolloSubscriptionProtocolHandler( ISocketSessionInterceptor interceptor, - IWebSocketPayloadFormatter formatter) + IWebSocketPayloadFormatter formatter, + IServerDiagnosticEvents diagnosticEvents) { _interceptor = interceptor; _formatter = formatter; + _diagnosticEvents = diagnosticEvents; } public string Name => GraphQL_WS; @@ -113,6 +117,7 @@ await _interceptor.OnConnectAsync( if (connectionStatus.Accepted) { ((WebSocketConnection)connection).IsConnected = true; + _diagnosticEvents.WebSocketConnectionInitialized(session, operationMessageObj); await SendConnectionAcceptMessage( session, connectionStatus.Extensions, diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs index 373364a99e1..09f029a6bd9 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices; using System.Text.Json; using HotChocolate.AspNetCore.Formatters; +using HotChocolate.AspNetCore.Instrumentation; using HotChocolate.Buffers; using HotChocolate.Language; using HotChocolate.AspNetCore.Utilities; @@ -19,7 +20,8 @@ internal sealed class GraphQLOverWebSocketProtocolHandler( IWebSocketPayloadFormatter formatter, IDocumentCache documentCache, IDocumentHashProvider documentHashProvider, - ParserOptions parserOptions) + ParserOptions parserOptions, + IServerDiagnosticEvents diagnosticEvents) : IGraphQLOverWebSocketProtocolHandler { public string Name => GraphQL_Transport_WS; @@ -128,6 +130,7 @@ await interceptor.OnConnectAsync( if (connectionStatus.Accepted) { ((WebSocketConnection)connection).IsConnected = true; + diagnosticEvents.WebSocketConnectionInitialized(session, operationMessageObj); await SendConnectionAcceptMessage( session, connectionStatus.Extensions, diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Subscriptions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Subscriptions.cs index 95aa367b0d4..cb06a512ce3 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Subscriptions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Subscriptions.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using HotChocolate.AspNetCore; using HotChocolate.AspNetCore.Formatters; +using HotChocolate.AspNetCore.Instrumentation; using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.AspNetCore.Subscriptions.Protocols.Apollo; using HotChocolate.AspNetCore.Subscriptions.Protocols.GraphQLOverWebSocket; @@ -86,7 +87,8 @@ private static IRequestExecutorBuilder AddApolloProtocol( s => s.AddSingleton( sp => new ApolloSubscriptionProtocolHandler( sp.GetRequiredService(), - sp.GetRequiredService()))); + sp.GetRequiredService(), + sp.GetRequiredService()))); private static IRequestExecutorBuilder AddGraphQLOverWebSocketProtocol( this IRequestExecutorBuilder builder) @@ -97,7 +99,8 @@ private static IRequestExecutorBuilder AddGraphQLOverWebSocketProtocol( sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetRequiredService()))); + sp.GetRequiredService(), + sp.GetRequiredService()))); /// /// Adds a custom WebSocket payload formatter to the DI. diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics.Core/ActivityEnricherBase.cs b/src/HotChocolate/Diagnostics/src/Diagnostics.Core/ActivityEnricherBase.cs index 8f276a64d98..02619d2e748 100644 --- a/src/HotChocolate/Diagnostics/src/Diagnostics.Core/ActivityEnricherBase.cs +++ b/src/HotChocolate/Diagnostics/src/Diagnostics.Core/ActivityEnricherBase.cs @@ -1,6 +1,8 @@ using System.Diagnostics; using Microsoft.AspNetCore.Http; using HotChocolate.AspNetCore.Instrumentation; +using HotChocolate.AspNetCore.Subscriptions; +using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.Execution; using HotChocolate.Language; @@ -12,6 +14,16 @@ namespace HotChocolate.Diagnostics; /// public abstract class ActivityEnricherBase { + /// + /// Called once per WebSocket session after the client's connection initialization + /// message has been accepted. The payload of + /// is only valid for the duration of this call. + /// + public virtual void EnrichConnectionInit( + ISocketSession session, + IOperationMessagePayload connectionInitMessage) + { } + public virtual void EnrichExecuteHttpRequest( HttpContext httpContext, HttpRequestKind kind, diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityServerDiagnosticListener.cs b/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityServerDiagnosticListener.cs index b10d244b12a..1b41744ea8a 100644 --- a/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityServerDiagnosticListener.cs +++ b/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityServerDiagnosticListener.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Http; using HotChocolate.AspNetCore.Instrumentation; +using HotChocolate.AspNetCore.Subscriptions; +using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.Execution; using HotChocolate.Language; using static HotChocolate.Diagnostics.HotChocolateActivitySource; @@ -103,6 +105,11 @@ public override void ParserErrors(HttpContext context, IReadOnlyList err } } + public override void WebSocketConnectionInitialized( + ISocketSession session, + IOperationMessagePayload connectionInitMessage) + => enricher.EnrichConnectionInit(session, connectionInitMessage); + public override IDisposable FormatHttpResponse(HttpContext context, OperationResult result) { if (options.SkipFormatHttpResponse) diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs index 0991542c907..9345405ccb7 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs @@ -1,12 +1,18 @@ +using System.Diagnostics; using System.Net.Http.Headers; using System.Net.WebSockets; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; +using HotChocolate.AspNetCore.Instrumentation; +using HotChocolate.AspNetCore.Subscriptions; +using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.AspNetCore.Tests.Utilities; +using HotChocolate.AspNetCore.Tests.Utilities.Subscriptions.Apollo; using HotChocolate.Execution; using HotChocolate.Execution.Configuration; +using HotChocolate.Language; using HotChocolate.Resolvers; using HotChocolate.Subscriptions; using HotChocolate.Transport.Http; @@ -748,6 +754,57 @@ public async Task Http_Subscription_Event_Should_Be_Error_When_Timeout() } } + [Fact] + public async Task WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default() + { + using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using (CaptureActivities(out var activities)) + { + // arrange + var signal = new HttpSubscriptionSignal(); + using var server = CreateInstrumentedServer( + configureBuilder: b => b + .AddTypeExtension() + .Services.AddSingleton(signal)); + using var webSocket = await ConnectWebSocketAsync(server, guard.Token); + await using var client = await SocketClient.ConnectAsync(webSocket, guard.Token); + var sender = server.Services.GetRequiredService(); + + var request = new OperationRequest("subscription OnMessageSubscription { onMessage }"); + + using var result = await client.ExecuteAsync(request, guard.Token); + var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); + + // act + // wait until the server subscribed to the topic, push one event, then + // complete the topic so the server ends the operation with a `complete` + // message and the client observes a clean, graceful close + try + { + var moveNext = results.MoveNextAsync().AsTask(); + await signal.Subscribed.Task.WaitAsync(guard.Token); + await sender.SendAsync("OnMessage", "hello", guard.Token); + Assert.True(await moveNext); + await sender.CompleteAsync("OnMessage"); + Assert.False(await results.MoveNextAsync()); + } + finally + { + await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); + } + + // the WebSocket session encloses every span of this trace, so close it + // before the trace is read + await CloseWebSocketAsync(webSocket, guard.Token); + + // assert + // the default scopes exclude ExecuteRequest, and a WebSocket session has no + // HTTP transport span to fall back to, so the trace carries no request span + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + [Fact] public async Task WebSocket_Subscription_Should_Be_Ok_When_Server_Completes() { @@ -930,6 +987,196 @@ public async Task WebSocket_Subscription_Event_Should_Be_Error_When_Timeout() } } + [Fact] + public async Task WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans() + { + using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using (CaptureActivities(out var activities)) + { + // arrange + // the listener captures the tenant from the connection init payload and stores + // it on the connection, the enricher tags the request and subscription event + // spans with it + var signal = new HttpSubscriptionSignal(); + using var server = CreateInstrumentedServer( + o => o.Scopes = ActivityScopes.All, + b => b + .AddTypeExtension() + .AddApplicationService() + .Services + .AddSingleton(signal) + .AddSingleton()); + using var webSocket = await ConnectWebSocketAsync(server, guard.Token); + var payload = JsonSerializer.SerializeToElement(new { tenant = "acme-42" }); + await using var client = await SocketClient.ConnectAsync(webSocket, payload, guard.Token); + var sender = server.Services.GetRequiredService(); + + var request = new OperationRequest("subscription OnMessageSubscription { onMessage }"); + + using var result = await client.ExecuteAsync(request, guard.Token); + var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); + + // act + // deliver a single event so both the request span and one subscription event + // span are recorded, then close the session + try + { + var moveNext = results.MoveNextAsync().AsTask(); + await signal.Subscribed.Task.WaitAsync(guard.Token); + await sender.SendAsync("OnMessage", "hello", guard.Token); + Assert.True(await moveNext); + await sender.CompleteAsync("OnMessage"); + Assert.False(await results.MoveNextAsync()); + } + finally + { + await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); + } + + await CloseWebSocketAsync(webSocket, guard.Token); + + // assert + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + + [Fact] + public async Task WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans() + { + using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using (CaptureActivities(out var activities)) + { + // arrange + // the same capture over the legacy apollo protocol, which raises the + // connection init diagnostic event from its own protocol handler + var signal = new HttpSubscriptionSignal(); + using var server = CreateInstrumentedServer( + o => o.Scopes = ActivityScopes.All, + b => b + .AddTypeExtension() + .AddApplicationService() + .Services + .AddSingleton(signal) + .AddSingleton()); + var sender = server.Services.GetRequiredService(); + + var webSocketClient = server.CreateWebSocketClient(); + webSocketClient.ConfigureRequest = + r => r.Headers.SecWebSocketProtocol = WellKnownProtocols.GraphQL_WS; + using var webSocket = await webSocketClient.ConnectAsync(s_webSocketUrl, guard.Token); + + await webSocket.SendConnectionInitializeAsync( + new Dictionary { ["tenant"] = "acme-42" }, + guard.Token); + Assert.NotNull(await WaitForApolloMessageAsync(webSocket, "connection_ack", guard.Token)); + + // act + // deliver a single event so both the request span and one subscription event + // span are recorded, then complete the topic so the server ends the operation + await webSocket.SendSubscriptionStartAsync( + "1", + new GraphQLRequest( + Utf8GraphQLParser.Parse("subscription OnMessageSubscription { onMessage }"))); + await signal.Subscribed.Task.WaitAsync(guard.Token); + await sender.SendAsync("OnMessage", "hello", guard.Token); + Assert.NotNull(await WaitForApolloMessageAsync(webSocket, "data", guard.Token)); + + await sender.CompleteAsync("OnMessage"); + Assert.NotNull(await WaitForApolloMessageAsync(webSocket, "complete", guard.Token)); + + await CloseWebSocketAsync(webSocket, guard.Token); + + // assert + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + + private static async Task WaitForApolloMessageAsync( + WebSocket webSocket, + string type, + CancellationToken cancellationToken) + { + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + using var combined = + CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeout.Token); + + try + { + while (!combined.Token.IsCancellationRequested) + { + var message = await webSocket.ReceiveServerMessageAsync(combined.Token); + + if (message is null) + { + await Task.Delay(5, combined.Token); + continue; + } + + if (message.RootElement.GetProperty("type").GetString() == type) + { + return message; + } + + message.Dispose(); + } + } + catch (OperationCanceledException) + { + // expected: no matching message arrived within the timeout + } + + return null; + } + + public sealed record TenantFeature(string Tenant); + + public sealed class TenantActivityEnricher(InstrumentationOptions options) + : ActivityEnricher(options) + { + /// + /// Stores the tenant on the connection so it outlives the connection + /// initialization message, which is only valid during this call. + /// + public override void EnrichConnectionInit( + ISocketSession session, + IOperationMessagePayload connectionInitMessage) + { + if (connectionInitMessage.Payload is { ValueKind: JsonValueKind.Object } payload + && payload.TryGetProperty("tenant", out var tenant) + && tenant.ValueKind is JsonValueKind.String) + { + session.Connection.Features.Set(new TenantFeature(tenant.GetString()!)); + } + } + + public override void EnrichExecuteRequest(RequestContext context, Activity activity) + { + base.EnrichExecuteRequest(context, activity); + SetTenantTag(context, activity); + } + + public override void EnrichOnSubscriptionEvent( + RequestContext context, + ulong subscriptionId, + Activity activity) + { + base.EnrichOnSubscriptionEvent(context, subscriptionId, activity); + SetTenantTag(context, activity); + } + + private static void SetTenantTag(RequestContext context, Activity activity) + { + if (context.ContextData.TryGetValue(nameof(ISocketSession), out var value) + && value is ISocketSession session + && session.Connection.Features.Get() is { } tenant) + { + activity.SetTag("test.tenant", tenant.Tenant); + } + } + } + private static async Task ConnectWebSocketAsync( TestServer server, CancellationToken cancellationToken) diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap new file mode 100644 index 00000000000..a82c92e88be --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap @@ -0,0 +1,256 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "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": 101 + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Complexity Analysis", + "DisplayName": "GraphQL Complexity Analysis", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.fieldCost", + "Value": 0.0 + }, + { + "Key": "graphql.operation.typeCost", + "Value": 1.0 + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap new file mode 100644 index 00000000000..03affcdb64f --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap @@ -0,0 +1,256 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 101 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Complexity Analysis", + "DisplayName": "GraphQL Complexity Analysis", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.fieldCost", + "Value": 0.0 + }, + { + "Key": "graphql.operation.typeCost", + "Value": 1.0 + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap new file mode 100644 index 00000000000..a82c92e88be --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap @@ -0,0 +1,256 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "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": 101 + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Complexity Analysis", + "DisplayName": "GraphQL Complexity Analysis", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.fieldCost", + "Value": 0.0 + }, + { + "Key": "graphql.operation.typeCost", + "Value": 1.0 + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap new file mode 100644 index 00000000000..03affcdb64f --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap @@ -0,0 +1,256 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 101 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Complexity Analysis", + "DisplayName": "GraphQL Complexity Analysis", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.fieldCost", + "Value": 0.0 + }, + { + "Key": "graphql.operation.typeCost", + "Value": 1.0 + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap new file mode 100644 index 00000000000..f864156a25d --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap @@ -0,0 +1,156 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "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": 101 + } + ], + "event": [], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap new file mode 100644 index 00000000000..6e0e1be3a73 --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap @@ -0,0 +1,156 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 101 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion/src/Fusion.AspNetCore/DependencyInjection/FusionServerServiceCollectionExtensions.cs b/src/HotChocolate/Fusion/src/Fusion.AspNetCore/DependencyInjection/FusionServerServiceCollectionExtensions.cs index db4f19a125f..644c1374500 100644 --- a/src/HotChocolate/Fusion/src/Fusion.AspNetCore/DependencyInjection/FusionServerServiceCollectionExtensions.cs +++ b/src/HotChocolate/Fusion/src/Fusion.AspNetCore/DependencyInjection/FusionServerServiceCollectionExtensions.cs @@ -123,7 +123,8 @@ private static IFusionGatewayBuilder AddApolloProtocol( (_, s) => s.AddSingleton( sp => new ApolloSubscriptionProtocolHandler( sp.GetRequiredService(), - sp.GetRequiredService()))); + sp.GetRequiredService(), + sp.GetRequiredService()))); private static IFusionGatewayBuilder AddGraphQLOverWebSocketProtocol( this IFusionGatewayBuilder builder) @@ -134,5 +135,6 @@ private static IFusionGatewayBuilder AddGraphQLOverWebSocketProtocol( sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetRequiredService()))); + sp.GetRequiredService(), + sp.GetRequiredService()))); } diff --git a/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityServerDiagnosticListener.cs b/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityServerDiagnosticListener.cs index 2bfb286ed56..7d05ded5374 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityServerDiagnosticListener.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityServerDiagnosticListener.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Http; using HotChocolate.AspNetCore.Instrumentation; +using HotChocolate.AspNetCore.Subscriptions; +using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.Diagnostics; using HotChocolate.Execution; using HotChocolate.Language; @@ -104,6 +106,11 @@ public override void ParserErrors(HttpContext context, IReadOnlyList err } } + public override void WebSocketConnectionInitialized( + ISocketSession session, + IOperationMessagePayload connectionInitMessage) + => enricher.EnrichConnectionInit(session, connectionInitMessage); + public override IDisposable FormatHttpResponse(HttpContext context, OperationResult result) { if (options.SkipFormatHttpResponse) diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs index 5d3937f30bf..ef6c215cde0 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs @@ -1,8 +1,15 @@ +using System.Diagnostics; using System.Net.WebSockets; using System.Runtime.CompilerServices; using System.Text; +using System.Text.Json; +using HotChocolate.AspNetCore.Instrumentation; +using HotChocolate.AspNetCore.Subscriptions; +using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.Diagnostics; using HotChocolate.Execution; +using HotChocolate.Fusion.Execution; +using HotChocolate.Fusion.Execution.Nodes; using HotChocolate.Language; using HotChocolate.PersistedOperations; using HotChocolate.Resolvers; @@ -752,6 +759,59 @@ public async Task Http_Subscription_Should_Be_Unset_When_Client_Disconnects_Duri } } + [Fact] + public async Task WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default() + { + using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b + .AddQueryType() + .AddSubscriptionType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation()); + + using var webSocket = await ConnectWebSocketAsync(gateway, guard.Token); + await using var client = await SocketClient.ConnectAsync(webSocket, guard.Token); + + var request = new OperationRequest("subscription OnMessageSubscription { onMessage }"); + + using var result = await client.ExecuteAsync(request, guard.Token); + var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); + + // act + // the subgraph emits one event then completes its stream, so receive the + // single event and then let the gateway end the operation with a + // `complete` message (no exception, no abort) + try + { + Assert.True(await results.MoveNextAsync()); + Assert.False(await results.MoveNextAsync()); + } + finally + { + await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); + } + + // the WebSocket session encloses every span of this trace, so close it + // before the trace is read + await CloseWebSocketAsync(webSocket, guard.Token); + + // assert + // the default scopes exclude ExecuteRequest, and a WebSocket session has no + // HTTP transport span to fall back to, so the trace carries no request span + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + [Fact] public async Task WebSocket_Subscription_Should_Be_Ok_When_Server_Completes() { @@ -900,6 +960,110 @@ public async Task WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connect } } + [Fact] + public async Task WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans() + { + using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using (CaptureActivities(out var activities)) + { + // arrange + // the listener captures the tenant from the connection init payload and stores + // it on the connection, the enricher tags the request and subscription event + // spans with it + using var server1 = CreateSourceSchema( + "a", + b => b + .AddQueryType() + .AddSubscriptionType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b + .AddInstrumentation(o => o.Scopes = FusionActivityScopes.All) + .AddApplicationService() + .Services.AddSingleton()); + + using var webSocket = await ConnectWebSocketAsync(gateway, guard.Token); + var payload = JsonSerializer.SerializeToElement(new { tenant = "acme-42" }); + await using var client = await SocketClient.ConnectAsync(webSocket, payload, guard.Token); + + var request = new OperationRequest("subscription OnMessageSubscription { onMessage }"); + + using var result = await client.ExecuteAsync(request, guard.Token); + var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); + + // act + // the subgraph emits one event then completes its stream, so both the request + // span and one subscription event span are recorded + try + { + Assert.True(await results.MoveNextAsync()); + Assert.False(await results.MoveNextAsync()); + } + finally + { + await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); + } + + await CloseWebSocketAsync(webSocket, guard.Token); + + // assert + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + + public sealed record TenantFeature(string Tenant); + + public sealed class TenantActivityEnricher(InstrumentationOptions options) + : FusionActivityEnricher(options) + { + /// + /// Stores the tenant on the connection so it outlives the connection + /// initialization message, which is only valid during this call. + /// + public override void EnrichConnectionInit( + ISocketSession session, + IOperationMessagePayload connectionInitMessage) + { + if (connectionInitMessage.Payload is { ValueKind: JsonValueKind.Object } payload + && payload.TryGetProperty("tenant", out var tenant) + && tenant.ValueKind is JsonValueKind.String) + { + session.Connection.Features.Set(new TenantFeature(tenant.GetString()!)); + } + } + + public override void EnrichExecuteRequest(RequestContext context, Activity activity) + { + base.EnrichExecuteRequest(context, activity); + SetTenantTag(context, activity); + } + + public override void EnrichOnSubscriptionEvent( + OperationPlanContext context, + ExecutionNode node, + string schemaName, + ulong subscriptionId, + Activity activity) + { + base.EnrichOnSubscriptionEvent(context, node, schemaName, subscriptionId, activity); + SetTenantTag(context.RequestContext, activity); + } + + private static void SetTenantTag(RequestContext context, Activity activity) + { + if (context.ContextData.TryGetValue(nameof(ISocketSession), out var value) + && value is ISocketSession session + && session.Connection.Features.Get() is { } tenant) + { + activity.SetTag("test.tenant", tenant.Tenant); + } + } + } + private static async Task ConnectWebSocketAsync( Gateway gateway, CancellationToken cancellationToken) diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap new file mode 100644 index 00000000000..e03f14a6a0a --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap @@ -0,0 +1,321 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "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": 101 + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "bb94346d204b2f13a21dcba79a06b47065115db28b9ba2c04bce67382b50cef0" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "OnMessageSubscription_ef859b98_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:f9abddbb651ad8e118793eb7c4a7c8db7fe43cae95faba710a0ff35b26b17136" + } + ], + "event": [] + } + ] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "event": [] + } + ] + }, + { + "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.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap new file mode 100644 index 00000000000..f3c18f13722 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap @@ -0,0 +1,321 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 101 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "bb94346d204b2f13a21dcba79a06b47065115db28b9ba2c04bce67382b50cef0" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "OnMessageSubscription_ef859b98_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:f9abddbb651ad8e118793eb7c4a7c8db7fe43cae95faba710a0ff35b26b17136" + } + ], + "event": [] + } + ] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "event": [] + } + ] + }, + { + "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": [] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap new file mode 100644 index 00000000000..cc89cb2dd7e --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap @@ -0,0 +1,242 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "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": 101 + } + ], + "event": [], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "bb94346d204b2f13a21dcba79a06b47065115db28b9ba2c04bce67382b50cef0" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "OnMessageSubscription_ef859b98_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:f9abddbb651ad8e118793eb7c4a7c8db7fe43cae95faba710a0ff35b26b17136" + } + ], + "event": [] + }, + { + "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 Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "" + } + ], + "event": [] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap new file mode 100644 index 00000000000..89d240d3e4e --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap @@ -0,0 +1,242 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 101 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "bb94346d204b2f13a21dcba79a06b47065115db28b9ba2c04bce67382b50cef0" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "OnMessageSubscription_ef859b98_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:f9abddbb651ad8e118793eb7c4a7c8db7fe43cae95faba710a0ff35b26b17136" + } + ], + "event": [] + }, + { + "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 Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "" + } + ], + "event": [] + } + ] + } + ] +} From c71aa6944feacb942ebd62a34df4daa045d5d785 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:06:32 +0000 Subject: [PATCH 4/8] wip --- .../ActivityServerDiagnosticListenerTests.cs | 100 +++++- ...ded_As_Tag_To_Request_And_Event_Spans.snap | 256 ++++++++++++++ ...ag_To_Request_And_Event_Spans_NET11_0.snap | 256 ++++++++++++++ ...onActivityServerDiagnosticListenerTests.cs | 97 +++++- ...ded_As_Tag_To_Request_And_Event_Spans.snap | 321 ++++++++++++++++++ ...ag_To_Request_And_Event_Spans_NET11_0.snap | 321 ++++++++++++++++++ 6 files changed, 1339 insertions(+), 12 deletions(-) create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap create mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap create mode 100644 src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs index 9345405ccb7..dcd2b207dc6 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs @@ -3,6 +3,7 @@ using System.Net.WebSockets; using System.Text; using System.Text.Json; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using HotChocolate.AspNetCore.Instrumentation; @@ -987,6 +988,61 @@ public async Task WebSocket_Subscription_Event_Should_Be_Error_When_Timeout() } } + [Fact] + public async Task WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans() + { + using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using (CaptureActivities(out var activities)) + { + // arrange + // clients that are not browsers can send the tenant as a handshake header + // instead of in the connection init payload, the same enricher reads both + var signal = new HttpSubscriptionSignal(); + using var server = CreateInstrumentedServer( + o => o.Scopes = ActivityScopes.All, + b => b + .AddTypeExtension() + .AddApplicationService() + .Services + .AddSingleton(signal) + .AddSingleton()); + using var webSocket = await ConnectWebSocketAsync( + server, + guard.Token, + r => r.Headers[TenantHeaderName] = "acme-42"); + await using var client = await SocketClient.ConnectAsync(webSocket, guard.Token); + var sender = server.Services.GetRequiredService(); + + var request = new OperationRequest("subscription OnMessageSubscription { onMessage }"); + + using var result = await client.ExecuteAsync(request, guard.Token); + var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); + + // act + // deliver a single event so both the request span and one subscription event + // span are recorded, then close the session + try + { + var moveNext = results.MoveNextAsync().AsTask(); + await signal.Subscribed.Task.WaitAsync(guard.Token); + await sender.SendAsync("OnMessage", "hello", guard.Token); + Assert.True(await moveNext); + await sender.CompleteAsync("OnMessage"); + Assert.False(await results.MoveNextAsync()); + } + finally + { + await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); + } + + await CloseWebSocketAsync(webSocket, guard.Token); + + // assert + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + [Fact] public async Task WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans() { @@ -1130,15 +1186,13 @@ await webSocket.SendSubscriptionStartAsync( return null; } + private const string TenantHeaderName = "X-Tenant"; + public sealed record TenantFeature(string Tenant); public sealed class TenantActivityEnricher(InstrumentationOptions options) : ActivityEnricher(options) { - /// - /// Stores the tenant on the connection so it outlives the connection - /// initialization message, which is only valid during this call. - /// public override void EnrichConnectionInit( ISocketSession session, IOperationMessagePayload connectionInitMessage) @@ -1167,23 +1221,48 @@ public override void EnrichOnSubscriptionEvent( } private static void SetTenantTag(RequestContext context, Activity activity) + { + if (ResolveTenant(context) is { } tenant) + { + activity.SetTag("test.tenant", tenant); + } + } + + /// + /// Resolves the tenant from the connection initialization payload, which browser + /// clients have to use because they cannot set handshake headers, and otherwise + /// falls back to the handshake header. + /// + private static string? ResolveTenant(RequestContext context) { if (context.ContextData.TryGetValue(nameof(ISocketSession), out var value) && value is ISocketSession session && session.Connection.Features.Get() is { } tenant) { - activity.SetTag("test.tenant", tenant.Tenant); + return tenant.Tenant; + } + + if (context.Features.Get() is { } httpContext + && httpContext.Request.Headers.TryGetValue(TenantHeaderName, out var header)) + { + return header.ToString(); } + + return null; } } private static async Task ConnectWebSocketAsync( TestServer server, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + Action? configureRequest = null) { var webSocketClient = server.CreateWebSocketClient(); - webSocketClient.ConfigureRequest = - r => r.Headers.SecWebSocketProtocol = WellKnownProtocols.GraphQL_Transport_WS; + webSocketClient.ConfigureRequest = r => + { + r.Headers.SecWebSocketProtocol = WellKnownProtocols.GraphQL_Transport_WS; + configureRequest?.Invoke(r); + }; return await webSocketClient.ConnectAsync(s_webSocketUrl, cancellationToken); } @@ -1208,6 +1287,11 @@ await webSocket.CloseAsync( { // expected: the server may have torn the connection down already } + catch (IOException) + { + // expected: the state check above races the server tearing the session down, + // so the close can still find the connection already gone + } catch (OperationCanceledException) { // expected: the close handshake was aborted diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap new file mode 100644 index 00000000000..a82c92e88be --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap @@ -0,0 +1,256 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "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": 101 + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Complexity Analysis", + "DisplayName": "GraphQL Complexity Analysis", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.fieldCost", + "Value": 0.0 + }, + { + "Key": "graphql.operation.typeCost", + "Value": 1.0 + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap new file mode 100644 index 00000000000..03affcdb64f --- /dev/null +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap @@ -0,0 +1,256 @@ +{ + "source": { + "name": "HotChocolate.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 101 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Complexity Analysis", + "DisplayName": "GraphQL Complexity Analysis", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.fieldCost", + "Value": 0.0 + }, + { + "Key": "graphql.operation.typeCost", + "Value": 1.0 + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs index ef6c215cde0..659cc671d01 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs @@ -20,6 +20,7 @@ using HotChocolate.Types.Composite; using HotChocolate.Types.Relay; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using static CookieCrumble.TestEnvironment; using static HotChocolate.Fusion.Diagnostics.ActivityTestHelper; @@ -960,6 +961,62 @@ public async Task WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connect } } + [Fact] + public async Task WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans() + { + using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using (CaptureActivities(out var activities)) + { + // arrange + // clients that are not browsers can send the tenant as a handshake header + // instead of in the connection init payload, the same enricher reads both + using var server1 = CreateSourceSchema( + "a", + b => b + .AddQueryType() + .AddSubscriptionType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b + .AddInstrumentation(o => o.Scopes = FusionActivityScopes.All) + .AddApplicationService() + .Services.AddSingleton()); + + using var webSocket = await ConnectWebSocketAsync( + gateway, + guard.Token, + r => r.Headers[TenantHeaderName] = "acme-42"); + await using var client = await SocketClient.ConnectAsync(webSocket, guard.Token); + + var request = new OperationRequest("subscription OnMessageSubscription { onMessage }"); + + using var result = await client.ExecuteAsync(request, guard.Token); + var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); + + // act + // the subgraph emits one event then completes its stream, so both the request + // span and one subscription event span are recorded + try + { + Assert.True(await results.MoveNextAsync()); + Assert.False(await results.MoveNextAsync()); + } + finally + { + await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); + } + + await CloseWebSocketAsync(webSocket, guard.Token); + + // assert + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + [Fact] public async Task WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans() { @@ -1015,6 +1072,8 @@ public async Task WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Req } } + private const string TenantHeaderName = "X-Tenant"; + public sealed record TenantFeature(string Tenant); public sealed class TenantActivityEnricher(InstrumentationOptions options) @@ -1054,23 +1113,48 @@ public override void EnrichOnSubscriptionEvent( } private static void SetTenantTag(RequestContext context, Activity activity) + { + if (ResolveTenant(context) is { } tenant) + { + activity.SetTag("test.tenant", tenant); + } + } + + /// + /// Resolves the tenant from the connection initialization payload, which browser + /// clients have to use because they cannot set handshake headers, and otherwise + /// falls back to the handshake header. + /// + private static string? ResolveTenant(RequestContext context) { if (context.ContextData.TryGetValue(nameof(ISocketSession), out var value) && value is ISocketSession session && session.Connection.Features.Get() is { } tenant) { - activity.SetTag("test.tenant", tenant.Tenant); + return tenant.Tenant; + } + + if (context.Features.Get() is { } httpContext + && httpContext.Request.Headers.TryGetValue(TenantHeaderName, out var header)) + { + return header.ToString(); } + + return null; } } private static async Task ConnectWebSocketAsync( Gateway gateway, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + Action? configureRequest = null) { var webSocketClient = gateway.CreateWebSocketClient(); - webSocketClient.ConfigureRequest = - r => r.Headers.SecWebSocketProtocol = WellKnownProtocols.GraphQL_Transport_WS; + webSocketClient.ConfigureRequest = r => + { + r.Headers.SecWebSocketProtocol = WellKnownProtocols.GraphQL_Transport_WS; + configureRequest?.Invoke(r); + }; return await webSocketClient.ConnectAsync(s_webSocketUrl, cancellationToken); } @@ -1095,6 +1179,11 @@ await webSocket.CloseAsync( { // expected: the gateway may have torn the connection down already } + catch (IOException) + { + // expected: the state check above races the gateway tearing the session down, + // so the close can still find the connection already gone + } catch (OperationCanceledException) { // expected: the close handshake was aborted diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap new file mode 100644 index 00000000000..e03f14a6a0a --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap @@ -0,0 +1,321 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "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": 101 + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "bb94346d204b2f13a21dcba79a06b47065115db28b9ba2c04bce67382b50cef0" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "OnMessageSubscription_ef859b98_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:f9abddbb651ad8e118793eb7c4a7c8db7fe43cae95faba710a0ff35b26b17136" + } + ], + "event": [] + } + ] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "event": [] + } + ] + }, + { + "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.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap new file mode 100644 index 00000000000..f3c18f13722 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap @@ -0,0 +1,321 @@ +{ + "source": { + "name": "HotChocolate.Fusion.Diagnostics" + }, + "activities": [ + { + "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "DisplayName": "GET /graphql/{**slug}", + "Kind": "Server", + "Status": "Unset", + "tags": [ + { + "Key": "server.address", + "Value": "localhost" + }, + { + "Key": "server.port", + "Value": 5000 + }, + { + "Key": "http.request.method", + "Value": "GET" + }, + { + "Key": "url.scheme", + "Value": "http" + }, + { + "Key": "url.path", + "Value": "/graphql" + }, + { + "Key": "network.protocol.version", + "Value": "1.1" + }, + { + "Key": "http.response.status_code", + "Value": 101 + }, + { + "Key": "http.route", + "Value": "/graphql/{**slug}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Execution", + "DisplayName": "GraphQL Operation Execution", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "execute" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [], + "activities": [ + { + "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": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.step.id", + "Value": "1" + }, + { + "Key": "graphql.operation.step.kind", + "Value": "operation" + }, + { + "Key": "graphql.operation.step.plan.id", + "Value": "bb94346d204b2f13a21dcba79a06b47065115db28b9ba2c04bce67382b50cef0" + }, + { + "Key": "graphql.source_schema.name", + "Value": "a" + }, + { + "Key": "graphql.source_schema.operation.name", + "Value": "OnMessageSubscription_ef859b98_1" + }, + { + "Key": "graphql.source_schema.operation.hash", + "Value": "sha256:f9abddbb651ad8e118793eb7c4a7c8db7fe43cae95faba710a0ff35b26b17136" + } + ], + "event": [] + } + ] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "" + }, + { + "Key": "test.tenant", + "Value": "acme-42" + } + ], + "event": [] + } + ] + }, + { + "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 802e93f370b53e3bfa9e82df6358003b9e85deb2 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:29:01 +0000 Subject: [PATCH 5/8] Fix missing graphql span --- .../ActivityExecutionDiagnosticListener.cs | 15 +- ...d_Be_Ok_When_Server_Completes_Default.snap | 160 +++++++++++------- ...When_Server_Completes_Default_NET11_0.snap | 160 +++++++++++------- ...ctivityExecutionDiagnosticEventListener.cs | 15 +- ...d_Be_Ok_When_Server_Completes_Default.snap | 144 ++++++++++------ ...When_Server_Completes_Default_NET11_0.snap | 144 ++++++++++------ 6 files changed, 410 insertions(+), 228 deletions(-) diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityExecutionDiagnosticListener.cs b/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityExecutionDiagnosticListener.cs index e464a81128b..a4d945214ba 100644 --- a/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityExecutionDiagnosticListener.cs +++ b/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityExecutionDiagnosticListener.cs @@ -25,15 +25,18 @@ public override IDisposable ExecuteRequest(RequestContext context) if (options.SkipExecuteRequest) { - if (!options.SkipExecuteHttpRequest - && context.Features.TryGet(out var httpContext) - && httpContext.Features.Get() is { } httpRequestSpan) + if (options.SkipExecuteHttpRequest + || !context.Features.TryGet(out var httpContext)) { - httpContextActivity = httpRequestSpan.Activity; + return EmptyScope; } - else + + // 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) { - return EmptyScope; + httpContextActivity = httpRequestSpan.Activity; } } diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap index f864156a25d..a360db43ff4 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap @@ -45,56 +45,22 @@ "event": [], "activities": [ { - "OperationName": "GraphQL Document Validation", - "DisplayName": "GraphQL Document Validation", + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", "Kind": "Internal", "Status": "Ok", "tags": [ { "Key": "graphql.processing.type", - "Value": "validate" + "Value": "request" }, { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - } - ], - "event": [] - }, - { - "OperationName": "GraphQL Operation Planning", - "DisplayName": "GraphQL Operation Planning", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.processing.type", - "Value": "plan" + "Key": "server.address", + "Value": "localhost" }, { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - }, - { - "Key": "graphql.operation.type", - "Value": "subscription" - }, - { - "Key": "graphql.operation.name", - "Value": "OnMessageSubscription" - } - ], - "event": [] - }, - { - "OperationName": "GraphQL Subscription Event", - "DisplayName": "GraphQL Subscription Event", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.processing.type", - "Value": "subscription_event" + "Key": "server.port", + "Value": 5000 }, { "Key": "graphql.operation.type", @@ -107,46 +73,124 @@ { "Key": "graphql.document.hash", "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [ + { + "Name": "AddedOperationToCache", + "Tags": [] }, { - "Key": "graphql.subscription.id", - "Value": "1" + "Name": "AddedDocumentToCache", + "Tags": [] } ], - "event": [], "activities": [ { - "OperationName": "Subscription.onMessage", - "DisplayName": "Subscription.onMessage", + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", "Kind": "Internal", "Status": "Ok", "tags": [ { "Key": "graphql.processing.type", - "Value": "resolve" + "Value": "validate" }, { - "Key": "graphql.field.alias", - "Value": "onMessage" - }, + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ { - "Key": "graphql.field.path", - "Value": "onMessage" + "Key": "graphql.processing.type", + "Value": "plan" }, { - "Key": "graphql.field.name", - "Value": "onMessage" + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" }, { - "Key": "graphql.field.schema_coordinate", - "Value": "Subscription.onMessage" + "Key": "graphql.operation.type", + "Value": "subscription" }, { - "Key": "graphql.field.parent_type", - "Value": "Subscription" + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" } ], "event": [] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] } ] } diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap index 6e0e1be3a73..a5a0a47fc94 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap @@ -45,56 +45,22 @@ "event": [], "activities": [ { - "OperationName": "GraphQL Document Validation", - "DisplayName": "GraphQL Document Validation", + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", "Kind": "Internal", "Status": "Ok", "tags": [ { "Key": "graphql.processing.type", - "Value": "validate" + "Value": "request" }, { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - } - ], - "event": [] - }, - { - "OperationName": "GraphQL Operation Planning", - "DisplayName": "GraphQL Operation Planning", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.processing.type", - "Value": "plan" + "Key": "server.address", + "Value": "localhost" }, { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - }, - { - "Key": "graphql.operation.type", - "Value": "subscription" - }, - { - "Key": "graphql.operation.name", - "Value": "OnMessageSubscription" - } - ], - "event": [] - }, - { - "OperationName": "GraphQL Subscription Event", - "DisplayName": "GraphQL Subscription Event", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.processing.type", - "Value": "subscription_event" + "Key": "server.port", + "Value": 5000 }, { "Key": "graphql.operation.type", @@ -107,46 +73,124 @@ { "Key": "graphql.document.hash", "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [ + { + "Name": "AddedOperationToCache", + "Tags": [] }, { - "Key": "graphql.subscription.id", - "Value": "1" + "Name": "AddedDocumentToCache", + "Tags": [] } ], - "event": [], "activities": [ { - "OperationName": "Subscription.onMessage", - "DisplayName": "Subscription.onMessage", + "OperationName": "GraphQL Document Validation", + "DisplayName": "GraphQL Document Validation", "Kind": "Internal", "Status": "Ok", "tags": [ { "Key": "graphql.processing.type", - "Value": "resolve" + "Value": "validate" }, { - "Key": "graphql.field.alias", - "Value": "onMessage" - }, + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Operation Planning", + "DisplayName": "GraphQL Operation Planning", + "Kind": "Internal", + "Status": "Ok", + "tags": [ { - "Key": "graphql.field.path", - "Value": "onMessage" + "Key": "graphql.processing.type", + "Value": "plan" }, { - "Key": "graphql.field.name", - "Value": "onMessage" + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" }, { - "Key": "graphql.field.schema_coordinate", - "Value": "Subscription.onMessage" + "Key": "graphql.operation.type", + "Value": "subscription" }, { - "Key": "graphql.field.parent_type", - "Value": "Subscription" + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" } ], "event": [] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "1" + } + ], + "event": [], + "activities": [ + { + "OperationName": "Subscription.onMessage", + "DisplayName": "Subscription.onMessage", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "resolve" + }, + { + "Key": "graphql.field.alias", + "Value": "onMessage" + }, + { + "Key": "graphql.field.path", + "Value": "onMessage" + }, + { + "Key": "graphql.field.name", + "Value": "onMessage" + }, + { + "Key": "graphql.field.schema_coordinate", + "Value": "Subscription.onMessage" + }, + { + "Key": "graphql.field.parent_type", + "Value": "Subscription" + } + ], + "event": [] + } + ] } ] } diff --git a/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityExecutionDiagnosticEventListener.cs b/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityExecutionDiagnosticEventListener.cs index 15ed77be47f..3c8e42d88b7 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityExecutionDiagnosticEventListener.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityExecutionDiagnosticEventListener.cs @@ -21,15 +21,18 @@ public override IDisposable ExecuteRequest(RequestContext context) if (options.SkipExecuteRequest) { - if (!options.SkipExecuteHttpRequest - && context.Features.TryGet(out var httpContext) - && httpContext.Features.Get() is { } httpRequestSpan) + if (options.SkipExecuteHttpRequest + || !context.Features.TryGet(out var httpContext)) { - httpContextActivity = httpRequestSpan.Activity; + return EmptyScope; } - else + + // 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) { - return EmptyScope; + httpContextActivity = httpRequestSpan.Activity; } } diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap index cc89cb2dd7e..3b4398e3218 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default.snap @@ -45,35 +45,22 @@ "event": [], "activities": [ { - "OperationName": "GraphQL Document Validation", - "DisplayName": "GraphQL Document Validation", + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", "Kind": "Internal", "Status": "Ok", "tags": [ { "Key": "graphql.processing.type", - "Value": "validate" + "Value": "request" }, { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - } - ], - "event": [] - }, - { - "OperationName": "GraphQL Operation Planning", - "DisplayName": "GraphQL Operation Planning", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.processing.type", - "Value": "plan" + "Key": "server.address", + "Value": "localhost" }, { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + "Key": "server.port", + "Value": 5000 }, { "Key": "graphql.operation.type", @@ -82,9 +69,95 @@ { "Key": "graphql.operation.name", "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" } ], - "event": [] + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "" + } + ], + "event": [] + } + ] }, { "OperationName": "GraphQL Step Execution", @@ -206,35 +279,6 @@ "event": [] } ] - }, - { - "OperationName": "GraphQL Subscription Event", - "DisplayName": "GraphQL Subscription Event", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.processing.type", - "Value": "subscription_event" - }, - { - "Key": "graphql.operation.type", - "Value": "subscription" - }, - { - "Key": "graphql.operation.name", - "Value": "OnMessageSubscription" - }, - { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - }, - { - "Key": "graphql.subscription.id", - "Value": "" - } - ], - "event": [] } ] } diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap index 89d240d3e4e..e6e5ffeacc2 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Default_NET11_0.snap @@ -45,35 +45,22 @@ "event": [], "activities": [ { - "OperationName": "GraphQL Document Validation", - "DisplayName": "GraphQL Document Validation", + "OperationName": "GraphQL Operation", + "DisplayName": "subscription", "Kind": "Internal", "Status": "Ok", "tags": [ { "Key": "graphql.processing.type", - "Value": "validate" + "Value": "request" }, { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - } - ], - "event": [] - }, - { - "OperationName": "GraphQL Operation Planning", - "DisplayName": "GraphQL Operation Planning", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.processing.type", - "Value": "plan" + "Key": "server.address", + "Value": "localhost" }, { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + "Key": "server.port", + "Value": 5000 }, { "Key": "graphql.operation.type", @@ -82,9 +69,95 @@ { "Key": "graphql.operation.name", "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" } ], - "event": [] + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + } + ], + "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:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + } + ], + "event": [] + }, + { + "OperationName": "GraphQL Subscription Event", + "DisplayName": "GraphQL Subscription Event", + "Kind": "Internal", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.processing.type", + "Value": "subscription_event" + }, + { + "Key": "graphql.operation.type", + "Value": "subscription" + }, + { + "Key": "graphql.operation.name", + "Value": "OnMessageSubscription" + }, + { + "Key": "graphql.document.hash", + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "graphql.subscription.id", + "Value": "" + } + ], + "event": [] + } + ] }, { "OperationName": "GraphQL Step Execution", @@ -206,35 +279,6 @@ "event": [] } ] - }, - { - "OperationName": "GraphQL Subscription Event", - "DisplayName": "GraphQL Subscription Event", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.processing.type", - "Value": "subscription_event" - }, - { - "Key": "graphql.operation.type", - "Value": "subscription" - }, - { - "Key": "graphql.operation.name", - "Value": "OnMessageSubscription" - }, - { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - }, - { - "Key": "graphql.subscription.id", - "Value": "" - } - ], - "event": [] } ] } From 389bc803e52b7b590500d74017e62a93ac506e0f Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:57:42 +0000 Subject: [PATCH 6/8] Cleanup --- .../Diagnostics.Core/ActivityEnricherBase.cs | 7 +- .../ActivityServerDiagnosticListener.cs | 2 +- .../ActivityServerDiagnosticListenerTests.cs | 119 +-------- ...on_Should_Be_Ok_When_Client_Completes.snap | 248 ------------------ ...d_Be_Ok_When_Client_Completes_NET11_0.snap | 248 ------------------ .../FusionActivityServerDiagnosticListener.cs | 2 +- ...onActivityServerDiagnosticListenerTests.cs | 230 ++++++++-------- ...ed_As_Tag_To_Request_And_Event_Spans.snap} | 36 ++- ...g_To_Request_And_Event_Spans_NET11_0.snap} | 36 ++- .../migration/migrate-from-16-6-to-16-7.md | 22 +- .../migrating/migrate-from-16-6-to-16-7.md | 22 +- 11 files changed, 226 insertions(+), 746 deletions(-) delete mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap delete mode 100644 src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap rename src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/{FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap => FusionActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap} (88%) rename src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/{FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap => FusionActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap} (88%) diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics.Core/ActivityEnricherBase.cs b/src/HotChocolate/Diagnostics/src/Diagnostics.Core/ActivityEnricherBase.cs index 02619d2e748..bb2e1e49a97 100644 --- a/src/HotChocolate/Diagnostics/src/Diagnostics.Core/ActivityEnricherBase.cs +++ b/src/HotChocolate/Diagnostics/src/Diagnostics.Core/ActivityEnricherBase.cs @@ -14,12 +14,7 @@ namespace HotChocolate.Diagnostics; /// public abstract class ActivityEnricherBase { - /// - /// Called once per WebSocket session after the client's connection initialization - /// message has been accepted. The payload of - /// is only valid for the duration of this call. - /// - public virtual void EnrichConnectionInit( + public virtual void OnWebSocketConnectionInitialized( ISocketSession session, IOperationMessagePayload connectionInitMessage) { } diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityServerDiagnosticListener.cs b/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityServerDiagnosticListener.cs index 1b41744ea8a..c81ef74843e 100644 --- a/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityServerDiagnosticListener.cs +++ b/src/HotChocolate/Diagnostics/src/Diagnostics/Listeners/ActivityServerDiagnosticListener.cs @@ -108,7 +108,7 @@ public override void ParserErrors(HttpContext context, IReadOnlyList err public override void WebSocketConnectionInitialized( ISocketSession session, IOperationMessagePayload connectionInitMessage) - => enricher.EnrichConnectionInit(session, connectionInitMessage); + => enricher.OnWebSocketConnectionInitialized(session, connectionInitMessage); public override IDisposable FormatHttpResponse(HttpContext context, OperationResult result) { diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs index dcd2b207dc6..ff454dc92a4 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs @@ -778,9 +778,6 @@ public async Task WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Defa var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); // act - // wait until the server subscribed to the topic, push one event, then - // complete the topic so the server ends the operation with a `complete` - // message and the client observes a clean, graceful close try { var moveNext = results.MoveNextAsync().AsTask(); @@ -795,13 +792,10 @@ public async Task WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Defa await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); } - // the WebSocket session encloses every span of this trace, so close it - // before the trace is read + // the session encloses every span, so close it before the trace is read await CloseWebSocketAsync(webSocket, guard.Token); // assert - // the default scopes exclude ExecuteRequest, and a WebSocket session has no - // HTTP transport span to fall back to, so the trace carries no request span activities.MatchSnapshot(Postfix([NET11_0])); } } @@ -830,9 +824,6 @@ public async Task WebSocket_Subscription_Should_Be_Ok_When_Server_Completes() var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); // act - // wait until the server subscribed to the topic, push one event, then - // complete the topic so the server ends the operation with a `complete` - // message and the client observes a clean, graceful close try { var moveNext = results.MoveNextAsync().AsTask(); @@ -847,51 +838,7 @@ public async Task WebSocket_Subscription_Should_Be_Ok_When_Server_Completes() await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); } - // the WebSocket session encloses every span of this trace, so close it - // before the trace is read - await CloseWebSocketAsync(webSocket, guard.Token); - - // assert - activities.MatchSnapshot(Postfix([NET11_0])); - } - } - - [Fact] - public async Task WebSocket_Subscription_Should_Be_Ok_When_Client_Completes() - { - using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - - using (CaptureActivities(out var activities)) - { - // arrange - var signal = new HttpSubscriptionSignal(); - using var server = CreateInstrumentedServer( - o => o.Scopes = ActivityScopes.All, - b => b - .AddTypeExtension() - .Services.AddSingleton(signal)); - using var webSocket = await ConnectWebSocketAsync(server, guard.Token); - await using var client = await SocketClient.ConnectAsync(webSocket, guard.Token); - var sender = server.Services.GetRequiredService(); - - var request = new OperationRequest("subscription OnMessageSubscription { onMessage }"); - - using var result = await client.ExecuteAsync(request, guard.Token); - var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); - - // receive one event successfully while the subscription is running - var moveNext = results.MoveNextAsync().AsTask(); - await signal.Subscribed.Task.WaitAsync(guard.Token); - await sender.SendAsync("OnMessage", "hello", guard.Token); - Assert.True(await moveNext); - - // act - // stop the subscription from the client (unsubscribe) while the socket - // stays open; disposing the stream sends a `complete` message - await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); - - // the `complete` message is delivered before the close frame, so the server - // stops the operation first and then ends the session + // the session encloses every span, so close it before the trace is read await CloseWebSocketAsync(webSocket, guard.Token); // assert @@ -922,16 +869,14 @@ public async Task WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connect using var result = await client.ExecuteAsync(request, guard.Token); var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); - // receive one event successfully while the connection is alive var moveNext = results.MoveNextAsync().AsTask(); await signal.Subscribed.Task.WaitAsync(guard.Token); await sender.SendAsync("OnMessage", "hello", guard.Token); Assert.True(await moveNext); // act - // the subscription is now idle, waiting for the next event. close the - // connection (close the tab) without unsubscribing first, so the server - // has to tear the still-running subscription down with the session. + // close the connection without unsubscribing, so the server has to tear the + // still-running subscription down with the session await CloseWebSocketAsync(webSocket, guard.Token); await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); @@ -948,8 +893,7 @@ public async Task WebSocket_Subscription_Event_Should_Be_Error_When_Timeout() using (CaptureActivities(out var activities)) { // arrange - // a blocking resolver combined with a tiny per-event timeout forces a - // server-side event timeout (not a client abort): the socket stays open + // a blocking resolver plus a tiny timeout forces a server-side event timeout var signal = new HttpSubscriptionSignal(); using var server = CreateInstrumentedServer( o => o.Scopes = ActivityScopes.All, @@ -968,9 +912,7 @@ public async Task WebSocket_Subscription_Event_Should_Be_Error_When_Timeout() var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); // act - // start processing an event that blocks past the per-event timeout; the - // timeout tears the operation down, so the server ends the stream without - // ever delivering a result + // the timeout tears the operation down, so no result is ever delivered var first = results.MoveNextAsync().AsTask(); await signal.Subscribed.Task.WaitAsync(guard.Token); await sender.SendAsync("OnBlockingMessage", "hello", guard.Token); @@ -982,8 +924,6 @@ public async Task WebSocket_Subscription_Event_Should_Be_Error_When_Timeout() await CloseWebSocketAsync(webSocket, guard.Token); // assert - // the snapshot records the subscription event span status for a - // server-side event timeout activities.MatchSnapshot(Postfix([NET11_0])); } } @@ -996,8 +936,7 @@ public async Task WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_S using (CaptureActivities(out var activities)) { // arrange - // clients that are not browsers can send the tenant as a handshake header - // instead of in the connection init payload, the same enricher reads both + // non-browser clients can send the tenant as a handshake header instead var signal = new HttpSubscriptionSignal(); using var server = CreateInstrumentedServer( o => o.Scopes = ActivityScopes.All, @@ -1020,8 +959,6 @@ public async Task WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_S var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); // act - // deliver a single event so both the request span and one subscription event - // span are recorded, then close the session try { var moveNext = results.MoveNextAsync().AsTask(); @@ -1051,9 +988,6 @@ public async Task WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Req using (CaptureActivities(out var activities)) { // arrange - // the listener captures the tenant from the connection init payload and stores - // it on the connection, the enricher tags the request and subscription event - // spans with it var signal = new HttpSubscriptionSignal(); using var server = CreateInstrumentedServer( o => o.Scopes = ActivityScopes.All, @@ -1074,8 +1008,6 @@ public async Task WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Req var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); // act - // deliver a single event so both the request span and one subscription event - // span are recorded, then close the session try { var moveNext = results.MoveNextAsync().AsTask(); @@ -1105,8 +1037,7 @@ public async Task WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag using (CaptureActivities(out var activities)) { // arrange - // the same capture over the legacy apollo protocol, which raises the - // connection init diagnostic event from its own protocol handler + // the legacy apollo protocol raises the event from its own protocol handler var signal = new HttpSubscriptionSignal(); using var server = CreateInstrumentedServer( o => o.Scopes = ActivityScopes.All, @@ -1129,8 +1060,6 @@ await webSocket.SendConnectionInitializeAsync( Assert.NotNull(await WaitForApolloMessageAsync(webSocket, "connection_ack", guard.Token)); // act - // deliver a single event so both the request span and one subscription event - // span are recorded, then complete the topic so the server ends the operation await webSocket.SendSubscriptionStartAsync( "1", new GraphQLRequest( @@ -1193,7 +1122,7 @@ public sealed record TenantFeature(string Tenant); public sealed class TenantActivityEnricher(InstrumentationOptions options) : ActivityEnricher(options) { - public override void EnrichConnectionInit( + public override void OnWebSocketConnectionInitialized( ISocketSession session, IOperationMessagePayload connectionInitMessage) { @@ -1228,11 +1157,6 @@ private static void SetTenantTag(RequestContext context, Activity activity) } } - /// - /// Resolves the tenant from the connection initialization payload, which browser - /// clients have to use because they cannot set handshake headers, and otherwise - /// falls back to the handshake header. - /// private static string? ResolveTenant(RequestContext context) { if (context.ContextData.TryGetValue(nameof(ISocketSession), out var value) @@ -1270,35 +1194,16 @@ private static async Task CloseWebSocketAsync( WebSocket webSocket, CancellationToken cancellationToken) { - if (webSocket.State is not WebSocketState.Open) - { - // the server already ended the session, so there is nothing left to close - return; - } - try { - await webSocket.CloseAsync( + await webSocket.CloseOutputAsync( WebSocketCloseStatus.NormalClosure, "done", cancellationToken); } - catch (WebSocketException) - { - // expected: the server may have torn the connection down already - } - catch (IOException) - { - // expected: the state check above races the server tearing the session down, - // so the close can still find the connection already gone - } - catch (OperationCanceledException) - { - // expected: the close handshake was aborted - } - catch (ObjectDisposedException) + catch (Exception ex) when (ex is WebSocketException or IOException or ObjectDisposedException) { - // expected: the server tore the session down while the close was in flight + // the server can end the session before the close frame is sent } } diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap deleted file mode 100644 index 95bcd1d95d2..00000000000 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap +++ /dev/null @@ -1,248 +0,0 @@ -{ - "source": { - "name": "HotChocolate.Diagnostics" - }, - "activities": [ - { - "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", - "DisplayName": "GET /graphql/{**slug}", - "Kind": "Server", - "Status": "Unset", - "tags": [ - { - "Key": "server.address", - "Value": "localhost" - }, - { - "Key": "server.port", - "Value": 5000 - }, - { - "Key": "http.request.method", - "Value": "GET" - }, - { - "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": 101 - } - ], - "event": [], - "activities": [ - { - "OperationName": "GraphQL Operation", - "DisplayName": "subscription", - "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": "subscription" - }, - { - "Key": "graphql.operation.name", - "Value": "OnMessageSubscription" - }, - { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - } - ], - "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:ef859b98c9d8c17038c8fd9aeecdb1e2" - } - ], - "event": [] - }, - { - "OperationName": "GraphQL Complexity Analysis", - "DisplayName": "GraphQL Complexity Analysis", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - }, - { - "Key": "graphql.operation.fieldCost", - "Value": 0.0 - }, - { - "Key": "graphql.operation.typeCost", - "Value": 1.0 - } - ], - "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:ef859b98c9d8c17038c8fd9aeecdb1e2" - }, - { - "Key": "graphql.operation.type", - "Value": "subscription" - }, - { - "Key": "graphql.operation.name", - "Value": "OnMessageSubscription" - } - ], - "event": [] - }, - { - "OperationName": "GraphQL Operation Execution", - "DisplayName": "GraphQL Operation Execution", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.processing.type", - "Value": "execute" - }, - { - "Key": "graphql.operation.type", - "Value": "subscription" - }, - { - "Key": "graphql.operation.name", - "Value": "OnMessageSubscription" - }, - { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - } - ], - "event": [], - "activities": [ - { - "OperationName": "GraphQL Subscription Event", - "DisplayName": "GraphQL Subscription Event", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.processing.type", - "Value": "subscription_event" - }, - { - "Key": "graphql.operation.type", - "Value": "subscription" - }, - { - "Key": "graphql.operation.name", - "Value": "OnMessageSubscription" - }, - { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - }, - { - "Key": "graphql.subscription.id", - "Value": "1" - } - ], - "event": [], - "activities": [ - { - "OperationName": "Subscription.onMessage", - "DisplayName": "Subscription.onMessage", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.processing.type", - "Value": "resolve" - }, - { - "Key": "graphql.field.alias", - "Value": "onMessage" - }, - { - "Key": "graphql.field.path", - "Value": "onMessage" - }, - { - "Key": "graphql.field.name", - "Value": "onMessage" - }, - { - "Key": "graphql.field.schema_coordinate", - "Value": "Subscription.onMessage" - }, - { - "Key": "graphql.field.parent_type", - "Value": "Subscription" - } - ], - "event": [] - } - ] - } - ] - } - ] - } - ] - } - ] -} diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap deleted file mode 100644 index cd7dc12365c..00000000000 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap +++ /dev/null @@ -1,248 +0,0 @@ -{ - "source": { - "name": "HotChocolate.Diagnostics" - }, - "activities": [ - { - "OperationName": "Microsoft.AspNetCore.Hosting.HttpRequestIn", - "DisplayName": "GET /graphql/{**slug}", - "Kind": "Server", - "Status": "Unset", - "tags": [ - { - "Key": "server.address", - "Value": "localhost" - }, - { - "Key": "server.port", - "Value": 5000 - }, - { - "Key": "http.request.method", - "Value": "GET" - }, - { - "Key": "url.scheme", - "Value": "http" - }, - { - "Key": "url.path", - "Value": "/graphql" - }, - { - "Key": "network.protocol.version", - "Value": "1.1" - }, - { - "Key": "http.response.status_code", - "Value": 101 - }, - { - "Key": "http.route", - "Value": "/graphql/{**slug}" - } - ], - "event": [], - "activities": [ - { - "OperationName": "GraphQL Operation", - "DisplayName": "subscription", - "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": "subscription" - }, - { - "Key": "graphql.operation.name", - "Value": "OnMessageSubscription" - }, - { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - } - ], - "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:ef859b98c9d8c17038c8fd9aeecdb1e2" - } - ], - "event": [] - }, - { - "OperationName": "GraphQL Complexity Analysis", - "DisplayName": "GraphQL Complexity Analysis", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - }, - { - "Key": "graphql.operation.fieldCost", - "Value": 0.0 - }, - { - "Key": "graphql.operation.typeCost", - "Value": 1.0 - } - ], - "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:ef859b98c9d8c17038c8fd9aeecdb1e2" - }, - { - "Key": "graphql.operation.type", - "Value": "subscription" - }, - { - "Key": "graphql.operation.name", - "Value": "OnMessageSubscription" - } - ], - "event": [] - }, - { - "OperationName": "GraphQL Operation Execution", - "DisplayName": "GraphQL Operation Execution", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.processing.type", - "Value": "execute" - }, - { - "Key": "graphql.operation.type", - "Value": "subscription" - }, - { - "Key": "graphql.operation.name", - "Value": "OnMessageSubscription" - }, - { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - } - ], - "event": [], - "activities": [ - { - "OperationName": "GraphQL Subscription Event", - "DisplayName": "GraphQL Subscription Event", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.processing.type", - "Value": "subscription_event" - }, - { - "Key": "graphql.operation.type", - "Value": "subscription" - }, - { - "Key": "graphql.operation.name", - "Value": "OnMessageSubscription" - }, - { - "Key": "graphql.document.hash", - "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" - }, - { - "Key": "graphql.subscription.id", - "Value": "1" - } - ], - "event": [], - "activities": [ - { - "OperationName": "Subscription.onMessage", - "DisplayName": "Subscription.onMessage", - "Kind": "Internal", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.processing.type", - "Value": "resolve" - }, - { - "Key": "graphql.field.alias", - "Value": "onMessage" - }, - { - "Key": "graphql.field.path", - "Value": "onMessage" - }, - { - "Key": "graphql.field.name", - "Value": "onMessage" - }, - { - "Key": "graphql.field.schema_coordinate", - "Value": "Subscription.onMessage" - }, - { - "Key": "graphql.field.parent_type", - "Value": "Subscription" - } - ], - "event": [] - } - ] - } - ] - } - ] - } - ] - } - ] -} diff --git a/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityServerDiagnosticListener.cs b/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityServerDiagnosticListener.cs index 7d05ded5374..6ed4d2e2ce3 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityServerDiagnosticListener.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Diagnostics/Listeners/FusionActivityServerDiagnosticListener.cs @@ -109,7 +109,7 @@ public override void ParserErrors(HttpContext context, IReadOnlyList err public override void WebSocketConnectionInitialized( ISocketSession session, IOperationMessagePayload connectionInitMessage) - => enricher.EnrichConnectionInit(session, connectionInitMessage); + => enricher.OnWebSocketConnectionInitialized(session, connectionInitMessage); public override IDisposable FormatHttpResponse(HttpContext context, OperationResult result) { diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs index 659cc671d01..c745c10889a 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs @@ -789,9 +789,7 @@ public async Task WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Defa var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); // act - // the subgraph emits one event then completes its stream, so receive the - // single event and then let the gateway end the operation with a - // `complete` message (no exception, no abort) + // the subgraph emits one event then completes its stream try { Assert.True(await results.MoveNextAsync()); @@ -802,13 +800,10 @@ public async Task WebSocket_Subscription_Should_Be_Ok_When_Server_Completes_Defa await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); } - // the WebSocket session encloses every span of this trace, so close it - // before the trace is read + // the session encloses every span, so close it before the trace is read await CloseWebSocketAsync(webSocket, guard.Token); // assert - // the default scopes exclude ExecuteRequest, and a WebSocket session has no - // HTTP transport span to fall back to, so the trace carries no request span activities.MatchSnapshot(Postfix([NET11_0])); } } @@ -843,9 +838,7 @@ public async Task WebSocket_Subscription_Should_Be_Ok_When_Server_Completes() var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); // act - // the subgraph emits one event then completes its stream, so receive the - // single event and then let the gateway end the operation with a - // `complete` message (no exception, no abort) + // the subgraph emits one event then completes its stream try { Assert.True(await results.MoveNextAsync()); @@ -856,61 +849,10 @@ public async Task WebSocket_Subscription_Should_Be_Ok_When_Server_Completes() await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); } - // the WebSocket session encloses every span of this trace, so close it - // before the trace is read + // the session encloses every span, so close it before the trace is read await CloseWebSocketAsync(webSocket, guard.Token); // assert - // the snapshot records the gateway request and subscription event span - // status for a graceful close - activities.MatchSnapshot(Postfix([NET11_0])); - } - } - - [Fact] - public async Task WebSocket_Subscription_Should_Be_Ok_When_Client_Completes() - { - using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - - using (CaptureActivities(out var activities)) - { - // arrange - using var server1 = CreateSourceSchema( - "a", - b => b - .AddQueryType() - .AddSubscriptionType()); - - using var gateway = await CreateCompositeSchemaAsync( - [ - ("a", server1) - ], - configureGatewayBuilder: b => b.AddInstrumentation(o => - o.Scopes = FusionActivityScopes.All)); - - using var webSocket = await ConnectWebSocketAsync(gateway, guard.Token); - await using var client = await SocketClient.ConnectAsync(webSocket, guard.Token); - - var request = new OperationRequest("subscription OnIdleMessageSubscription { onIdleMessage }"); - - using var result = await client.ExecuteAsync(request, guard.Token); - var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); - - // receive one event successfully while the subscription is running - Assert.True(await results.MoveNextAsync()); - - // act - // stop the subscription from the client (unsubscribe) while the socket - // stays open; disposing the stream sends a `complete` message - await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); - - // the `complete` message is delivered before the close frame, so the - // gateway stops the operation first and then ends the session - await CloseWebSocketAsync(webSocket, guard.Token); - - // assert - // the snapshot records the subscription event span status for a - // client-initiated unsubscribe activities.MatchSnapshot(Postfix([NET11_0])); } } @@ -944,19 +886,15 @@ public async Task WebSocket_Subscription_Should_Be_Ok_When_Client_Closes_Connect using var result = await client.ExecuteAsync(request, guard.Token); var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); - // receive one event successfully while the connection is alive Assert.True(await results.MoveNextAsync()); // act - // the subscription is now idle, waiting for the next event. close the - // connection (close the tab) without unsubscribing first, so the gateway - // has to tear the still-running subscription down with the session. + // close the connection without unsubscribing, so the gateway has to tear the + // still-running subscription down with the session await CloseWebSocketAsync(webSocket, guard.Token); await IgnoreSocketTeardownAsync(results.DisposeAsync().AsTask()); // assert - // the snapshot records the subscription event span status for a client - // that closes the connection while the subscription is idle activities.MatchSnapshot(Postfix([NET11_0])); } } @@ -969,8 +907,7 @@ public async Task WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_S using (CaptureActivities(out var activities)) { // arrange - // clients that are not browsers can send the tenant as a handshake header - // instead of in the connection init payload, the same enricher reads both + // non-browser clients can send the tenant as a handshake header instead using var server1 = CreateSourceSchema( "a", b => b @@ -998,8 +935,7 @@ public async Task WebSocket_Header_Should_Be_Added_As_Tag_To_Request_And_Event_S var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); // act - // the subgraph emits one event then completes its stream, so both the request - // span and one subscription event span are recorded + // the subgraph emits one event then completes its stream try { Assert.True(await results.MoveNextAsync()); @@ -1025,9 +961,6 @@ public async Task WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Req using (CaptureActivities(out var activities)) { // arrange - // the listener captures the tenant from the connection init payload and stores - // it on the connection, the enricher tags the request and subscription event - // spans with it using var server1 = CreateSourceSchema( "a", b => b @@ -1053,8 +986,7 @@ public async Task WebSocket_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Req var results = result.ReadResultsAsync().GetAsyncEnumerator(guard.Token); // act - // the subgraph emits one event then completes its stream, so both the request - // span and one subscription event span are recorded + // the subgraph emits one event then completes its stream try { Assert.True(await results.MoveNextAsync()); @@ -1083,7 +1015,7 @@ public sealed class TenantActivityEnricher(InstrumentationOptions options) /// Stores the tenant on the connection so it outlives the connection /// initialization message, which is only valid during this call. /// - public override void EnrichConnectionInit( + public override void OnWebSocketConnectionInitialized( ISocketSession session, IOperationMessagePayload connectionInitMessage) { @@ -1120,11 +1052,6 @@ private static void SetTenantTag(RequestContext context, Activity activity) } } - /// - /// Resolves the tenant from the connection initialization payload, which browser - /// clients have to use because they cannot set handshake headers, and otherwise - /// falls back to the handshake header. - /// private static string? ResolveTenant(RequestContext context) { if (context.ContextData.TryGetValue(nameof(ISocketSession), out var value) @@ -1144,6 +1071,118 @@ private static void SetTenantTag(RequestContext context, Activity activity) } } + [Fact] + public async Task WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans() + { + using var guard = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using (CaptureActivities(out var activities)) + { + // arrange + // the legacy apollo protocol raises the event from its own protocol handler + using var server1 = CreateSourceSchema( + "a", + b => b + .AddQueryType() + .AddSubscriptionType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b + .AddInstrumentation(o => o.Scopes = FusionActivityScopes.All) + .AddApplicationService() + .Services.AddSingleton()); + + var webSocketClient = gateway.CreateWebSocketClient(); + webSocketClient.ConfigureRequest = + r => r.Headers.SecWebSocketProtocol = WellKnownProtocols.GraphQL_WS; + using var webSocket = await webSocketClient.ConnectAsync(s_webSocketUrl, guard.Token); + + await SendApolloMessageAsync( + webSocket, + """{"type":"connection_init","payload":{"tenant":"acme-42"}}""", + guard.Token); + Assert.NotNull(await WaitForApolloMessageAsync(webSocket, "connection_ack", guard.Token)); + + // act + // the subgraph emits one event then completes its stream + await SendApolloMessageAsync( + webSocket, + """ + {"type":"start","id":"1","payload":{"query":"subscription OnMessageSubscription { onMessage }"}} + """, + guard.Token); + Assert.NotNull(await WaitForApolloMessageAsync(webSocket, "data", guard.Token)); + Assert.NotNull(await WaitForApolloMessageAsync(webSocket, "complete", guard.Token)); + + await CloseWebSocketAsync(webSocket, guard.Token); + + // assert + activities.MatchSnapshot(Postfix([NET11_0])); + } + } + + private static async Task SendApolloMessageAsync( + WebSocket webSocket, + string message, + CancellationToken cancellationToken) + => await webSocket.SendAsync( + Encoding.UTF8.GetBytes(message), + WebSocketMessageType.Text, + true, + cancellationToken); + + private static async Task WaitForApolloMessageAsync( + WebSocket webSocket, + string type, + CancellationToken cancellationToken) + { + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + using var combined = + CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeout.Token); + var buffer = new byte[4096]; + + try + { + while (!combined.Token.IsCancellationRequested) + { + await using var stream = new MemoryStream(); + WebSocketReceiveResult result; + + do + { + result = await webSocket.ReceiveAsync( + new ArraySegment(buffer), + combined.Token); + stream.Write(buffer, 0, result.Count); + } + while (!result.EndOfMessage); + + if (stream.Length == 0) + { + continue; + } + + var message = JsonDocument.Parse(stream.ToArray()); + + if (message.RootElement.GetProperty("type").GetString() == type) + { + return message; + } + + message.Dispose(); + } + } + catch (OperationCanceledException) + { + // expected: no matching message arrived within the timeout + } + + return null; + } + private static async Task ConnectWebSocketAsync( Gateway gateway, CancellationToken cancellationToken, @@ -1162,35 +1201,16 @@ private static async Task CloseWebSocketAsync( WebSocket webSocket, CancellationToken cancellationToken) { - if (webSocket.State is not WebSocketState.Open) - { - // the gateway already ended the session, so there is nothing left to close - return; - } - try { - await webSocket.CloseAsync( + await webSocket.CloseOutputAsync( WebSocketCloseStatus.NormalClosure, "done", cancellationToken); } - catch (WebSocketException) - { - // expected: the gateway may have torn the connection down already - } - catch (IOException) - { - // expected: the state check above races the gateway tearing the session down, - // so the close can still find the connection already gone - } - catch (OperationCanceledException) - { - // expected: the close handshake was aborted - } - catch (ObjectDisposedException) + catch (Exception ex) when (ex is WebSocketException or IOException or ObjectDisposedException) { - // expected: the gateway tore the session down while the close was in flight + // the gateway can end the session before the close frame is sent } } diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap similarity index 88% rename from src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap rename to src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap index 610f071d287..e03f14a6a0a 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes.snap +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans.snap @@ -68,11 +68,15 @@ }, { "Key": "graphql.operation.name", - "Value": "OnIdleMessageSubscription" + "Value": "OnMessageSubscription" }, { "Key": "graphql.document.hash", - "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "test.tenant", + "Value": "acme-42" } ], "event": [ @@ -98,7 +102,7 @@ }, { "Key": "graphql.document.hash", - "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" } ], "event": [] @@ -115,7 +119,7 @@ }, { "Key": "graphql.document.hash", - "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" }, { "Key": "graphql.operation.type", @@ -123,7 +127,7 @@ }, { "Key": "graphql.operation.name", - "Value": "OnIdleMessageSubscription" + "Value": "OnMessageSubscription" } ], "event": [] @@ -144,11 +148,11 @@ }, { "Key": "graphql.operation.name", - "Value": "OnIdleMessageSubscription" + "Value": "OnMessageSubscription" }, { "Key": "graphql.document.hash", - "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" } ], "event": [], @@ -169,11 +173,11 @@ }, { "Key": "graphql.operation.name", - "Value": "OnIdleMessageSubscription" + "Value": "OnMessageSubscription" }, { "Key": "graphql.document.hash", - "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" }, { "Key": "graphql.operation.step.id", @@ -185,7 +189,7 @@ }, { "Key": "graphql.operation.step.plan.id", - "Value": "8fef749a1b041aca849173d1dd38a6022e142b1ebbf92aeb666766e3a0654610" + "Value": "bb94346d204b2f13a21dcba79a06b47065115db28b9ba2c04bce67382b50cef0" }, { "Key": "graphql.source_schema.name", @@ -193,11 +197,11 @@ }, { "Key": "graphql.source_schema.operation.name", - "Value": "OnIdleMessageSubscription_51d9b3bc_1" + "Value": "OnMessageSubscription_ef859b98_1" }, { "Key": "graphql.source_schema.operation.hash", - "Value": "sha256:6b6ed56d2f50e7e614c9096e8e46d789db0e605355b6baf21ab088bde107ca0e" + "Value": "sha256:f9abddbb651ad8e118793eb7c4a7c8db7fe43cae95faba710a0ff35b26b17136" } ], "event": [] @@ -220,15 +224,19 @@ }, { "Key": "graphql.operation.name", - "Value": "OnIdleMessageSubscription" + "Value": "OnMessageSubscription" }, { "Key": "graphql.document.hash", - "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" }, { "Key": "graphql.subscription.id", "Value": "" + }, + { + "Key": "test.tenant", + "Value": "acme-42" } ], "event": [] diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap similarity index 88% rename from src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap rename to src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap index 9dfe166518d..f3c18f13722 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Subscription_Should_Be_Ok_When_Client_Completes_NET11_0.snap +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/__snapshots__/FusionActivityServerDiagnosticListenerTests.WebSocket_Apollo_ConnectionInit_Payload_Should_Be_Added_As_Tag_To_Request_And_Event_Spans_NET11_0.snap @@ -68,11 +68,15 @@ }, { "Key": "graphql.operation.name", - "Value": "OnIdleMessageSubscription" + "Value": "OnMessageSubscription" }, { "Key": "graphql.document.hash", - "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" + }, + { + "Key": "test.tenant", + "Value": "acme-42" } ], "event": [ @@ -98,7 +102,7 @@ }, { "Key": "graphql.document.hash", - "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" } ], "event": [] @@ -115,7 +119,7 @@ }, { "Key": "graphql.document.hash", - "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" }, { "Key": "graphql.operation.type", @@ -123,7 +127,7 @@ }, { "Key": "graphql.operation.name", - "Value": "OnIdleMessageSubscription" + "Value": "OnMessageSubscription" } ], "event": [] @@ -144,11 +148,11 @@ }, { "Key": "graphql.operation.name", - "Value": "OnIdleMessageSubscription" + "Value": "OnMessageSubscription" }, { "Key": "graphql.document.hash", - "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" } ], "event": [], @@ -169,11 +173,11 @@ }, { "Key": "graphql.operation.name", - "Value": "OnIdleMessageSubscription" + "Value": "OnMessageSubscription" }, { "Key": "graphql.document.hash", - "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" }, { "Key": "graphql.operation.step.id", @@ -185,7 +189,7 @@ }, { "Key": "graphql.operation.step.plan.id", - "Value": "8fef749a1b041aca849173d1dd38a6022e142b1ebbf92aeb666766e3a0654610" + "Value": "bb94346d204b2f13a21dcba79a06b47065115db28b9ba2c04bce67382b50cef0" }, { "Key": "graphql.source_schema.name", @@ -193,11 +197,11 @@ }, { "Key": "graphql.source_schema.operation.name", - "Value": "OnIdleMessageSubscription_51d9b3bc_1" + "Value": "OnMessageSubscription_ef859b98_1" }, { "Key": "graphql.source_schema.operation.hash", - "Value": "sha256:6b6ed56d2f50e7e614c9096e8e46d789db0e605355b6baf21ab088bde107ca0e" + "Value": "sha256:f9abddbb651ad8e118793eb7c4a7c8db7fe43cae95faba710a0ff35b26b17136" } ], "event": [] @@ -220,15 +224,19 @@ }, { "Key": "graphql.operation.name", - "Value": "OnIdleMessageSubscription" + "Value": "OnMessageSubscription" }, { "Key": "graphql.document.hash", - "Value": "md5:51d9b3bca5722d3209429e79f8ee6435" + "Value": "md5:ef859b98c9d8c17038c8fd9aeecdb1e2" }, { "Key": "graphql.subscription.id", "Value": "" + }, + { + "Key": "test.tenant", + "Value": "acme-42" } ], "event": [] diff --git a/website/content/docs/fusion/migration/migrate-from-16-6-to-16-7.md b/website/content/docs/fusion/migration/migrate-from-16-6-to-16-7.md index 892365ee9a0..7ed684805ff 100644 --- a/website/content/docs/fusion/migration/migrate-from-16-6-to-16-7.md +++ b/website/content/docs/fusion/migration/migrate-from-16-6-to-16-7.md @@ -1,10 +1,30 @@ --- title: Migrate Hot Chocolate Fusion from 16.6 to 16.7 -description: "Migration guide for Hot Chocolate Fusion v16.6 to v16.7: replace raw condition masks with ConditionFlags and configure wide operation limits." +description: "Migration guide for Hot Chocolate Fusion v16.6 to v16.7: implement the new WebSocket connection initialization diagnostic event, replace raw condition masks with ConditionFlags, and configure wide operation limits." --- Update every Hot Chocolate Fusion package in the application to version 16.7 before applying these changes. +# Breaking changes + +Things that have been removed or had a change in behavior that may cause your code not to compile or lead to unexpected behavior at runtime if not addressed. + +## IServerDiagnosticEvents gained a WebSocket connection initialization event + +`IServerDiagnosticEvents` has a new `WebSocketConnectionInitialized` member. The gateway raises it once per WebSocket session, for both the `graphql-transport-ws` and the legacy `graphql-ws` protocol, after the client's connection initialization message has been accepted. + +Listeners that derive from `ServerDiagnosticEventListener` need no change, because the base class provides a virtual no-op. Types that implement `IServerDiagnosticEvents` directly have to implement the new member: + +```csharp +public void WebSocketConnectionInitialized( + ISocketSession session, + IOperationMessagePayload connectionInitMessage) +{ +} +``` + +The payload of `connectionInitMessage` is only valid for the duration of the call. Read out any value that is needed later inside the callback, for example onto `ISocketConnection.Features`. + # Deprecations ## Raw condition masks replaced by ConditionFlags diff --git a/website/content/docs/hotchocolate/migrating/migrate-from-16-6-to-16-7.md b/website/content/docs/hotchocolate/migrating/migrate-from-16-6-to-16-7.md index 0e5694a34cc..f8a3ae84420 100644 --- a/website/content/docs/hotchocolate/migrating/migrate-from-16-6-to-16-7.md +++ b/website/content/docs/hotchocolate/migrating/migrate-from-16-6-to-16-7.md @@ -1,11 +1,31 @@ --- title: Migrate Hot Chocolate from 16.6 to 16.7 metaTitle: "Hot Chocolate 16.7 Migration Guide" -description: "Migration guide for Hot Chocolate v16.6 to v16.7: replace raw condition masks with ConditionFlags and configure wide operation limits." +description: "Migration guide for Hot Chocolate v16.6 to v16.7: implement the new WebSocket connection initialization diagnostic event, replace raw condition masks with ConditionFlags, and configure wide operation limits." --- Update every `HotChocolate.*` package in the application to version 16.7 before applying these changes. +# Breaking changes + +Things that have been removed or had a change in behavior that may cause your code not to compile or lead to unexpected behavior at runtime if not addressed. + +## IServerDiagnosticEvents gained a WebSocket connection initialization event + +`IServerDiagnosticEvents` has a new `WebSocketConnectionInitialized` member. It is raised once per WebSocket session, for both the `graphql-transport-ws` and the legacy `graphql-ws` protocol, after the client's connection initialization message has been accepted. + +Listeners that derive from `ServerDiagnosticEventListener` need no change, because the base class provides a virtual no-op. Types that implement `IServerDiagnosticEvents` directly have to implement the new member: + +```csharp +public void WebSocketConnectionInitialized( + ISocketSession session, + IOperationMessagePayload connectionInitMessage) +{ +} +``` + +The payload of `connectionInitMessage` is only valid for the duration of the call. Read out any value that is needed later inside the callback, for example onto `ISocketConnection.Features`. + # Deprecations ## Raw condition masks replaced by ConditionFlags From 7b3c63f5119921384d51ba8d06786f45527ee55f Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:42:59 +0000 Subject: [PATCH 7/8] Fix build --- .../Types/Types/Descriptors/Conventions/DescriptorContext.cs | 1 + .../Core/test/Types.Tests/Resolvers/ResolverCompilerTests.cs | 3 +-- .../Types.Tests/Types/Descriptors/DescriptorContextTests.cs | 4 ---- .../src/Utilities/DependencyInjection/EmptyServiceProvider.cs | 4 ---- .../Utilities/test/Utilities.Tests/MiddlewareCompilerTests.cs | 3 --- 5 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DescriptorContext.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DescriptorContext.cs index 8cb7f493d17..bcdf72682da 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DescriptorContext.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DescriptorContext.cs @@ -6,6 +6,7 @@ using HotChocolate.Resolvers; using HotChocolate.Types.Relay; using HotChocolate.Utilities; +using EmptyServiceProvider = HotChocolate.Utilities.EmptyServiceProvider; using ThrowHelper = HotChocolate.Utilities.ThrowHelper; namespace HotChocolate.Types.Descriptors; diff --git a/src/HotChocolate/Core/test/Types.Tests/Resolvers/ResolverCompilerTests.cs b/src/HotChocolate/Core/test/Types.Tests/Resolvers/ResolverCompilerTests.cs index ca722e1deb6..7c000f80411 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Resolvers/ResolverCompilerTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Resolvers/ResolverCompilerTests.cs @@ -9,10 +9,9 @@ using HotChocolate.Tests; using HotChocolate.Types; using HotChocolate.Types.Descriptors; -#if !NET11_0_OR_GREATER using HotChocolate.Utilities; -#endif using Moq; +using EmptyServiceProvider = HotChocolate.Utilities.EmptyServiceProvider; namespace HotChocolate.Resolvers; diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Descriptors/DescriptorContextTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Descriptors/DescriptorContextTests.cs index 207ee794cdd..50174213574 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Descriptors/DescriptorContextTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Descriptors/DescriptorContextTests.cs @@ -1,10 +1,6 @@ using HotChocolate.Configuration; using HotChocolate.Features; -#if NET11_0_OR_GREATER -using Microsoft.Extensions.DependencyInjection; -#else using HotChocolate.Utilities; -#endif namespace HotChocolate.Types.Descriptors; diff --git a/src/HotChocolate/Utilities/src/Utilities/DependencyInjection/EmptyServiceProvider.cs b/src/HotChocolate/Utilities/src/Utilities/DependencyInjection/EmptyServiceProvider.cs index 642000723f1..43cf41f0112 100644 --- a/src/HotChocolate/Utilities/src/Utilities/DependencyInjection/EmptyServiceProvider.cs +++ b/src/HotChocolate/Utilities/src/Utilities/DependencyInjection/EmptyServiceProvider.cs @@ -1,6 +1,3 @@ -// .NET 11 ships Microsoft.Extensions.DependencyInjection.EmptyServiceProvider, so callers -// resolve that type on newer targets instead. -#if !NET11_0_OR_GREATER using Microsoft.Extensions.DependencyInjection; namespace HotChocolate.Utilities; @@ -17,4 +14,3 @@ private EmptyServiceProvider() public static EmptyServiceProvider Instance { get; } = new(); } -#endif diff --git a/src/HotChocolate/Utilities/test/Utilities.Tests/MiddlewareCompilerTests.cs b/src/HotChocolate/Utilities/test/Utilities.Tests/MiddlewareCompilerTests.cs index a62bcdbe460..a674806b13e 100644 --- a/src/HotChocolate/Utilities/test/Utilities.Tests/MiddlewareCompilerTests.cs +++ b/src/HotChocolate/Utilities/test/Utilities.Tests/MiddlewareCompilerTests.cs @@ -1,7 +1,4 @@ using System.Linq.Expressions; -#if NET11_0_OR_GREATER -using Microsoft.Extensions.DependencyInjection; -#endif namespace HotChocolate.Utilities; From a540f71662e3eb846b11c187adda864155f3b10f Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:43:47 +0000 Subject: [PATCH 8/8] Fix build --- .../Core/test/Types.Tests/Resolvers/ResolverCompilerTests.cs | 1 - .../Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs | 1 - .../FusionActivityServerDiagnosticListenerTests.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/src/HotChocolate/Core/test/Types.Tests/Resolvers/ResolverCompilerTests.cs b/src/HotChocolate/Core/test/Types.Tests/Resolvers/ResolverCompilerTests.cs index 7c000f80411..c67c7cb5715 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Resolvers/ResolverCompilerTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Resolvers/ResolverCompilerTests.cs @@ -9,7 +9,6 @@ using HotChocolate.Tests; using HotChocolate.Types; using HotChocolate.Types.Descriptors; -using HotChocolate.Utilities; using Moq; using EmptyServiceProvider = HotChocolate.Utilities.EmptyServiceProvider; diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs index 5a723c41c5d..f281240ff33 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityServerDiagnosticListenerTests.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; -using HotChocolate.AspNetCore.Instrumentation; using HotChocolate.AspNetCore.Subscriptions; using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.AspNetCore.Tests.Utilities; diff --git a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs index c745c10889a..5a087f29163 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/FusionActivityServerDiagnosticListenerTests.cs @@ -3,7 +3,6 @@ using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; -using HotChocolate.AspNetCore.Instrumentation; using HotChocolate.AspNetCore.Subscriptions; using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.Diagnostics;