diff --git a/src/SignalR/server/Core/src/HubConnectionContext.cs b/src/SignalR/server/Core/src/HubConnectionContext.cs index 6812b6515149..e6872bed1f99 100644 --- a/src/SignalR/server/Core/src/HubConnectionContext.cs +++ b/src/SignalR/server/Core/src/HubConnectionContext.cs @@ -59,6 +59,8 @@ public partial class HubConnectionContext // Tracks groups that the connection has been added to internal HashSet GroupNames { get; } = new HashSet(); + internal Activity? OriginalActivity { get; set; } + /// /// Initializes a new instance of the class. /// diff --git a/src/SignalR/server/Core/src/HubConnectionHandler.cs b/src/SignalR/server/Core/src/HubConnectionHandler.cs index 32df42a00b37..342cbedb8dae 100644 --- a/src/SignalR/server/Core/src/HubConnectionHandler.cs +++ b/src/SignalR/server/Core/src/HubConnectionHandler.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.SignalR.Internal; @@ -129,7 +130,14 @@ public override async Task OnConnectedAsync(ConnectionContext connection) Log.ConnectedStarting(_logger); - var connectionContext = new HubConnectionContext(connection, contextOptions, _loggerFactory); + var connectionContext = new HubConnectionContext(connection, contextOptions, _loggerFactory) + { + OriginalActivity = Activity.Current, + }; + + // Get off the parent span. + // This is likely the Http Request span and we want Hub method invocations to not be collected under a long running span. + Activity.Current = null; var resolvedSupportedProtocols = (supportedProtocols as IReadOnlyList) ?? supportedProtocols.ToList(); if (!await connectionContext.HandshakeAsync(handshakeTimeout, resolvedSupportedProtocols, _protocolResolver, _userIdProvider, _enableDetailedErrors)) diff --git a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs index 62973db14d2f..b2e54590e2e2 100644 --- a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs +++ b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs @@ -18,6 +18,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal; internal sealed partial class DefaultHubDispatcher : HubDispatcher where THub : Hub { + private static readonly string _fullHubName = typeof(THub).FullName ?? typeof(THub).Name; + private readonly Dictionary _methods = new(StringComparer.OrdinalIgnoreCase); private readonly Utf8HashLookup _cachedMethodNames = new(); private readonly IServiceScopeFactory _serviceScopeFactory; @@ -76,11 +78,14 @@ public override async Task OnConnectedAsync(HubConnectionContext connection) var hubActivator = scope.ServiceProvider.GetRequiredService>(); var hub = hubActivator.Create(); + Activity? activity = null; try { // OnConnectedAsync won't work with client results (ISingleClientProxy.InvokeAsync) InitializeHub(hub, connection, invokeAllowed: false); + activity = StartActivity(connection, scope.ServiceProvider, nameof(hub.OnConnectedAsync)); + if (_onConnectedMiddleware != null) { var context = new HubLifetimeContext(connection.HubCallerContext, scope.ServiceProvider, hub); @@ -91,8 +96,14 @@ public override async Task OnConnectedAsync(HubConnectionContext connection) await hub.OnConnectedAsync(); } } + catch (Exception ex) + { + SetActivityError(activity, ex); + throw; + } finally { + activity?.Stop(); hubActivator.Release(hub); } } @@ -103,10 +114,13 @@ public override async Task OnDisconnectedAsync(HubConnectionContext connection, var hubActivator = scope.ServiceProvider.GetRequiredService>(); var hub = hubActivator.Create(); + Activity? activity = null; try { InitializeHub(hub, connection); + activity = StartActivity(connection, scope.ServiceProvider, nameof(hub.OnDisconnectedAsync)); + if (_onDisconnectedMiddleware != null) { var context = new HubLifetimeContext(connection.HubCallerContext, scope.ServiceProvider, hub); @@ -117,8 +131,14 @@ public override async Task OnDisconnectedAsync(HubConnectionContext connection, await hub.OnDisconnectedAsync(exception); } } + catch (Exception ex) + { + SetActivityError(activity, ex); + throw; + } finally { + activity?.Stop(); hubActivator.Release(hub); } } @@ -367,6 +387,10 @@ static async Task ExecuteInvocation(DefaultHubDispatcher dispatcher, var logger = dispatcher._logger; var enableDetailedErrors = dispatcher._enableDetailedErrors; + // Use hubMethodInvocationMessage.Target instead of methodExecutor.MethodInfo.Name + // We want to take HubMethodNameAttribute into account which will be the same as what the invocation target is + var activity = StartActivity(connection, scope.ServiceProvider, hubMethodInvocationMessage.Target); + object? result; try { @@ -375,6 +399,8 @@ static async Task ExecuteInvocation(DefaultHubDispatcher dispatcher, } catch (Exception ex) { + SetActivityError(activity, ex); + Log.FailedInvokingHubMethod(logger, hubMethodInvocationMessage.Target, ex); await SendInvocationError(hubMethodInvocationMessage.InvocationId, connection, ErrorMessageHelper.BuildErrorMessage($"An unexpected error occurred invoking '{hubMethodInvocationMessage.Target}' on the server.", ex, enableDetailedErrors)); @@ -382,6 +408,8 @@ await SendInvocationError(hubMethodInvocationMessage.InvocationId, connection, } finally { + activity?.Stop(); + // Stream response handles cleanup in StreamResultsAsync // And normal invocations handle cleanup below in the finally if (isStreamCall) @@ -467,6 +495,8 @@ private async Task StreamAsync(string invocationId, HubConnectionContext connect streamCts ??= CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted); + var activity = StartActivity(connection, scope.ServiceProvider, hubMethodInvocationMessage.Target); + try { if (!connection.ActiveRequestCancellationSources.TryAdd(invocationId, streamCts)) @@ -483,6 +513,8 @@ private async Task StreamAsync(string invocationId, HubConnectionContext connect } catch (Exception ex) { + SetActivityError(activity, ex); + Log.FailedInvokingHubMethod(_logger, hubMethodInvocationMessage.Target, ex); error = ErrorMessageHelper.BuildErrorMessage($"An unexpected error occurred invoking '{hubMethodInvocationMessage.Target}' on the server.", ex, _enableDetailedErrors); return; @@ -509,20 +541,27 @@ private async Task StreamAsync(string invocationId, HubConnectionContext connect catch (ChannelClosedException ex) { // If the channel closes from an exception in the streaming method, grab the innerException for the error from the streaming method - Log.FailedStreaming(_logger, invocationId, descriptor.MethodExecutor.MethodInfo.Name, ex.InnerException ?? ex); - error = ErrorMessageHelper.BuildErrorMessage("An error occurred on the server while streaming results.", ex.InnerException ?? ex, _enableDetailedErrors); + var exception = ex.InnerException ?? ex; + SetActivityError(activity, exception); + + Log.FailedStreaming(_logger, invocationId, descriptor.MethodExecutor.MethodInfo.Name, exception); + error = ErrorMessageHelper.BuildErrorMessage("An error occurred on the server while streaming results.", exception, _enableDetailedErrors); } catch (Exception ex) { // If the streaming method was canceled we don't want to send a HubException message - this is not an error case if (!(ex is OperationCanceledException && streamCts.IsCancellationRequested)) { + SetActivityError(activity, ex); + Log.FailedStreaming(_logger, invocationId, descriptor.MethodExecutor.MethodInfo.Name, ex); error = ErrorMessageHelper.BuildErrorMessage("An error occurred on the server while streaming results.", ex, _enableDetailedErrors); } } finally { + activity?.Stop(); + await CleanupInvocation(connection, hubMethodInvocationMessage, hubActivator, hub, scope); streamCts.Dispose(); @@ -749,4 +788,35 @@ public override IReadOnlyList GetParameterTypes(string methodName) return null; } + + // Starts an Activity for a Hub method invocation and sets up all the tags and other state. + // Make sure to call Activity.Stop() once the Hub method completes, and consider calling SetActivityError on exception. + private static Activity? StartActivity(HubConnectionContext connectionContext, IServiceProvider serviceProvider, string methodName) + { + if (serviceProvider.GetService() is SignalRActivitySource signalRActivitySource + && signalRActivitySource.ActivitySource.HasListeners()) + { + var requestContext = connectionContext.OriginalActivity?.Context; + + return signalRActivitySource.ActivitySource.StartActivity($"{_fullHubName}/{methodName}", ActivityKind.Server, parentId: null, + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md#server-attributes + tags: [ + new("rpc.method", methodName), + new("rpc.system", "signalr"), + new("rpc.service", _fullHubName), + // See https://github.com/dotnet/aspnetcore/blob/027c60168383421750f01e427e4f749d0684bc02/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelMetrics.cs#L308 + // And https://github.com/dotnet/aspnetcore/issues/43786 + //new("server.address", ...), + ], + links: requestContext.HasValue ? [new ActivityLink(requestContext.Value)] : null); + } + + return null; + } + + private static void SetActivityError(Activity? activity, Exception ex) + { + activity?.SetTag("error.type", ex.GetType().FullName); + activity?.SetStatus(ActivityStatusCode.Error); + } } diff --git a/src/SignalR/server/Core/src/Internal/SignalRActivitySource.cs b/src/SignalR/server/Core/src/Internal/SignalRActivitySource.cs new file mode 100644 index 000000000000..d65522521e4a --- /dev/null +++ b/src/SignalR/server/Core/src/Internal/SignalRActivitySource.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Microsoft.AspNetCore.SignalR.Internal; + +// Internal for now so we don't need API review. +// Just a wrapper for the ActivitySource +// don't want to put ActivitySource directly in DI as hosting already does that and it could get overwritten. +internal sealed class SignalRActivitySource +{ + public ActivitySource ActivitySource { get; } = new ActivitySource("Microsoft.AspNetCore.SignalR.Server"); +} diff --git a/src/SignalR/server/Core/src/SignalRDependencyInjectionExtensions.cs b/src/SignalR/server/Core/src/SignalRDependencyInjectionExtensions.cs index 9c519ed60bbc..317e11a63ff3 100644 --- a/src/SignalR/server/Core/src/SignalRDependencyInjectionExtensions.cs +++ b/src/SignalR/server/Core/src/SignalRDependencyInjectionExtensions.cs @@ -31,6 +31,8 @@ public static ISignalRServerBuilder AddSignalRCore(this IServiceCollection servi services.TryAddScoped(typeof(IHubActivator<>), typeof(DefaultHubActivator<>)); services.AddAuthorization(); + services.TryAddSingleton(new SignalRActivitySource()); + var builder = new SignalRServerBuilder(services); builder.AddJsonProtocol(); return builder; diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs index 7e1855a1dd23..091ffabaad9f 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; using System.Text; @@ -369,6 +370,12 @@ public async IAsyncEnumerable StreamWithClientResult() var sum = await Clients.Caller.InvokeAsync("Sum", 1, cancellationToken: default); yield return sum; } + + public void ActivityMethod(TestActivitySource testActivitySource) + { + var activity = testActivitySource.ActivitySource.StartActivity("inner", ActivityKind.Server); + activity.Stop(); + } } internal class SelfRef @@ -381,6 +388,11 @@ public SelfRef() public SelfRef Self { get; set; } } +public class TestActivitySource +{ + public ActivitySource ActivitySource { get; set; } +} + public abstract class TestHub : Hub { public override Task OnConnectedAsync() @@ -709,6 +721,13 @@ public async Task> CounterChannelAsync(int count) return CounterChannel(count); } + [HubMethodName("RenamedCounterChannel")] + public async Task> CounterChannelAsync2(int count) + { + await Task.Yield(); + return CounterChannel(count); + } + public async ValueTask> CounterChannelValueTaskAsync(int count) { await Task.Yield(); diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.Activity.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.Activity.cs new file mode 100644 index 000000000000..d5d175d0f479 --- /dev/null +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.Activity.cs @@ -0,0 +1,377 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.AspNetCore.InternalTesting; +using Microsoft.AspNetCore.SignalR.Internal; +using Microsoft.AspNetCore.SignalR.Protocol; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Testing; +using Moq; + +namespace Microsoft.AspNetCore.SignalR.Tests; + +public partial class HubConnectionHandlerTests +{ + [Fact] + public async Task HubMethodInvokesCreateActivities() + { + using (StartVerifiableLog()) + { + var activities = new List(); + var testSource = new ActivitySource("test_source"); + var hubMethodTestSource = new TestActivitySource() { ActivitySource = new ActivitySource("test_custom") }; + + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(builder => + { + builder.AddSingleton(hubMethodTestSource); + + // Provided by hosting layer normally + builder.AddSingleton(testSource); + }, LoggerFactory); + var signalrSource = serviceProvider.GetRequiredService().ActivitySource; + + using var listener = new ActivityListener + { + ShouldListenTo = activitySource => (ReferenceEquals(activitySource, testSource)) + || ReferenceEquals(activitySource, hubMethodTestSource.ActivitySource) || ReferenceEquals(activitySource, signalrSource), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activities.Add + }; + ActivitySource.AddActivityListener(listener); + + var mockHttpRequestActivity = new Activity("HttpRequest"); + mockHttpRequestActivity.Start(); + Activity.Current = mockHttpRequestActivity; + + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + + var activity = Assert.Single(activities); + AssertHubMethodActivity(activity, nameof(MethodHub.OnConnectedAsync), mockHttpRequestActivity); + + await client.SendInvocationAsync(nameof(MethodHub.Echo), "test").DefaultTimeout(); + + var completionMessage = Assert.IsType(await client.ReadAsync().DefaultTimeout()); + var res = (string)completionMessage.Result; + Assert.Equal("test", res); + + Assert.Equal(2, activities.Count); + AssertHubMethodActivity(activities[1], nameof(MethodHub.Echo), mockHttpRequestActivity); + + await client.SendInvocationAsync("RenamedMethod").DefaultTimeout(); + Assert.IsType(await client.ReadAsync().DefaultTimeout()); + + Assert.Equal(3, activities.Count); + AssertHubMethodActivity(activities[2], "RenamedMethod", mockHttpRequestActivity); + + await client.SendInvocationAsync(nameof(MethodHub.ActivityMethod)).DefaultTimeout(); + Assert.IsType(await client.ReadAsync().DefaultTimeout()); + + Assert.Equal(5, activities.Count); + AssertHubMethodActivity(activities[3], nameof(MethodHub.ActivityMethod), mockHttpRequestActivity); + Assert.NotNull(activities[4].Parent); + Assert.Equal("inner", activities[4].OperationName); + Assert.Equal(activities[3], activities[4].Parent); + + client.Dispose(); + + await connectionHandlerTask; + } + + Assert.Equal(6, activities.Count); + AssertHubMethodActivity(activities[5], nameof(MethodHub.OnDisconnectedAsync), mockHttpRequestActivity); + } + } + + [Fact] + public async Task StreamingHubMethodCreatesActivities() + { + using (StartVerifiableLog()) + { + var activities = new List(); + var testSource = new ActivitySource("test_source"); + + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(builder => + { + // Provided by hosting layer normally + builder.AddSingleton(testSource); + }, LoggerFactory); + var signalrSource = serviceProvider.GetRequiredService().ActivitySource; + + using var listener = new ActivityListener + { + ShouldListenTo = activitySource => (ReferenceEquals(activitySource, testSource)) + || ReferenceEquals(activitySource, signalrSource), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activities.Add + }; + ActivitySource.AddActivityListener(listener); + + var mockHttpRequestActivity = new Activity("HttpRequest"); + mockHttpRequestActivity.Start(); + Activity.Current = mockHttpRequestActivity; + + var connectionHandler = serviceProvider.GetService>(); + Mock invocationBinder = new Mock(); + invocationBinder.Setup(b => b.GetStreamItemType(It.IsAny())).Returns(typeof(int)); + + using (var client = new TestClient(invocationBinder: invocationBinder.Object)) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + + var activity = Assert.Single(activities); + AssertHubMethodActivity(activity, nameof(StreamingHub.OnConnectedAsync), mockHttpRequestActivity); + + _ = await client.StreamAsync(nameof(StreamingHub.CounterChannel), 3).DefaultTimeout(); + + Assert.Equal(2, activities.Count); + AssertHubMethodActivity(activities[1], nameof(StreamingHub.CounterChannel), mockHttpRequestActivity); + + _ = await client.StreamAsync("RenamedCounterChannel", 3).DefaultTimeout(); + + Assert.Equal(3, activities.Count); + AssertHubMethodActivity(activities[2], "RenamedCounterChannel", mockHttpRequestActivity); + + client.Dispose(); + + await connectionHandlerTask; + } + + Assert.Equal(4, activities.Count); + AssertHubMethodActivity(activities[3], nameof(StreamingHub.OnDisconnectedAsync), mockHttpRequestActivity); + } + } + + [Fact] + public async Task ExceptionInOnConnectedAsyncSetsActivityErrorState() + { + bool ExpectedErrors(WriteContext writeContext) + { + return writeContext.LoggerName == "Microsoft.AspNetCore.SignalR.HubConnectionHandler" && + writeContext.EventId.Name == "ErrorDispatchingHubEvent"; + } + + using (StartVerifiableLog(ExpectedErrors)) + { + var activities = new List(); + var testSource = new ActivitySource("test_source"); + + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(builder => + { + // Provided by hosting layer normally + builder.AddSingleton(testSource); + }, LoggerFactory); + var signalrSource = serviceProvider.GetRequiredService().ActivitySource; + + using var listener = new ActivityListener + { + ShouldListenTo = activitySource => (ReferenceEquals(activitySource, testSource)) + || ReferenceEquals(activitySource, signalrSource), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activities.Add + }; + ActivitySource.AddActivityListener(listener); + + var mockHttpRequestActivity = new Activity("HttpRequest"); + mockHttpRequestActivity.Start(); + Activity.Current = mockHttpRequestActivity; + + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + + var activity = Assert.Single(activities); + AssertHubMethodActivity(activity, nameof(OnConnectedThrowsHub.OnConnectedAsync), + mockHttpRequestActivity, exceptionType: typeof(InvalidOperationException)); + } + } + } + + [Fact] + public async Task ExceptionInOnDisconnectedAsyncSetsActivityErrorState() + { + bool ExpectedErrors(WriteContext writeContext) + { + return writeContext.LoggerName == "Microsoft.AspNetCore.SignalR.HubConnectionHandler" && + writeContext.EventId.Name == "ErrorDispatchingHubEvent"; + } + + using (StartVerifiableLog(ExpectedErrors)) + { + var activities = new List(); + var testSource = new ActivitySource("test_source"); + + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(builder => + { + // Provided by hosting layer normally + builder.AddSingleton(testSource); + }, LoggerFactory); + var signalrSource = serviceProvider.GetRequiredService().ActivitySource; + + using var listener = new ActivityListener + { + ShouldListenTo = activitySource => (ReferenceEquals(activitySource, testSource)) + || ReferenceEquals(activitySource, signalrSource), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activities.Add + }; + ActivitySource.AddActivityListener(listener); + + var mockHttpRequestActivity = new Activity("HttpRequest"); + mockHttpRequestActivity.Start(); + Activity.Current = mockHttpRequestActivity; + + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + client.Dispose(); + + await connectionHandlerTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + + Assert.Equal(2, activities.Count); + var activity = activities[1]; + AssertHubMethodActivity(activity, nameof(OnDisconnectedThrowsHub.OnDisconnectedAsync), + mockHttpRequestActivity, exceptionType: typeof(InvalidOperationException)); + } + } + + [Fact] + public async Task ExceptionInStreamingMethodSetsActivityErrorState() + { + bool ExpectedErrors(WriteContext writeContext) + { + return writeContext.LoggerName == "Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher" && + writeContext.EventId.Name == "FailedStreaming"; + } + + using (StartVerifiableLog(ExpectedErrors)) + { + var activities = new List(); + var testSource = new ActivitySource("test_source"); + + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(builder => + { + // Provided by hosting layer normally + builder.AddSingleton(testSource); + }, LoggerFactory); + var signalrSource = serviceProvider.GetRequiredService().ActivitySource; + + using var listener = new ActivityListener + { + ShouldListenTo = activitySource => (ReferenceEquals(activitySource, testSource)) + || ReferenceEquals(activitySource, signalrSource), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activities.Add + }; + ActivitySource.AddActivityListener(listener); + + var mockHttpRequestActivity = new Activity("HttpRequest"); + mockHttpRequestActivity.Start(); + Activity.Current = mockHttpRequestActivity; + + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + + _ = await client.StreamAsync(nameof(StreamingHub.ExceptionAsyncEnumerable)).DefaultTimeout(); + + Assert.Equal(2, activities.Count); + var activity = activities[1]; + AssertHubMethodActivity(activity, nameof(StreamingHub.ExceptionAsyncEnumerable), + mockHttpRequestActivity, exceptionType: typeof(Exception)); + } + } + } + + [Fact] + public async Task ExceptionInHubMethodSetsActivityErrorState() + { + bool ExpectedErrors(WriteContext writeContext) + { + return writeContext.LoggerName == "Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher" && + writeContext.EventId.Name == "FailedInvokingHubMethod"; + } + + using (StartVerifiableLog(ExpectedErrors)) + { + var activities = new List(); + var testSource = new ActivitySource("test_source"); + + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(builder => + { + // Provided by hosting layer normally + builder.AddSingleton(testSource); + }, LoggerFactory); + var signalrSource = serviceProvider.GetRequiredService().ActivitySource; + + using var listener = new ActivityListener + { + ShouldListenTo = activitySource => (ReferenceEquals(activitySource, testSource)) + || ReferenceEquals(activitySource, signalrSource), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activities.Add + }; + ActivitySource.AddActivityListener(listener); + + var mockHttpRequestActivity = new Activity("HttpRequest"); + mockHttpRequestActivity.Start(); + Activity.Current = mockHttpRequestActivity; + + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + + _ = await client.InvokeAsync(nameof(MethodHub.MethodThatThrows)).DefaultTimeout(); + + Assert.Equal(2, activities.Count); + var activity = activities[1]; + AssertHubMethodActivity(activity, nameof(MethodHub.MethodThatThrows), + mockHttpRequestActivity, exceptionType: typeof(InvalidOperationException)); + } + } + } + + private static void AssertHubMethodActivity(Activity activity, string methodName, Activity httpActivity, Type exceptionType = null) + { + Assert.Null(activity.Parent); + Assert.True(activity.IsStopped); + Assert.Equal($"{typeof(THub).FullName}/{methodName}", activity.OperationName); + + var tags = activity.Tags.ToArray(); + if (exceptionType is not null) + { + Assert.Equal(ActivityStatusCode.Error, activity.Status); + Assert.Equal(4, tags.Length); + Assert.Equal("error.type", tags[3].Key); + Assert.Equal(exceptionType.FullName, tags[3].Value); + } + else + { + Assert.Equal(ActivityStatusCode.Unset, activity.Status); + Assert.Equal(3, tags.Length); + } + + Assert.Equal("rpc.method", tags[0].Key); + Assert.Equal(methodName, tags[0].Value); + Assert.Equal("rpc.system", tags[1].Key); + Assert.Equal("signalr", tags[1].Value); + Assert.Equal("rpc.service", tags[2].Key); + Assert.Equal(typeof(THub).FullName, tags[2].Value); + + // Linked to original http request span + Assert.Equal(httpActivity.SpanId, Assert.Single(activity.Links).Context.SpanId); + } +} diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs index 3e759b3389ab..ba620cb8b899 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs @@ -17,9 +17,9 @@ using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Connections.Features; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Protocol; -using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing;