Skip to content

Commit 83aa6b1

Browse files
Add separate activities for Hub methods (#55439)
1 parent 3117946 commit 83aa6b1

8 files changed

+496
-4
lines changed

src/SignalR/server/Core/src/HubConnectionContext.cs

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ public partial class HubConnectionContext
5959
// Tracks groups that the connection has been added to
6060
internal HashSet<string> GroupNames { get; } = new HashSet<string>();
6161

62+
internal Activity? OriginalActivity { get; set; }
63+
6264
/// <summary>
6365
/// Initializes a new instance of the <see cref="HubConnectionContext"/> class.
6466
/// </summary>

src/SignalR/server/Core/src/HubConnectionHandler.cs

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using System.Linq;
56
using Microsoft.AspNetCore.Connections;
67
using Microsoft.AspNetCore.SignalR.Internal;
@@ -129,7 +130,14 @@ public override async Task OnConnectedAsync(ConnectionContext connection)
129130

130131
Log.ConnectedStarting(_logger);
131132

132-
var connectionContext = new HubConnectionContext(connection, contextOptions, _loggerFactory);
133+
var connectionContext = new HubConnectionContext(connection, contextOptions, _loggerFactory)
134+
{
135+
OriginalActivity = Activity.Current,
136+
};
137+
138+
// Get off the parent span.
139+
// This is likely the Http Request span and we want Hub method invocations to not be collected under a long running span.
140+
Activity.Current = null;
133141

134142
var resolvedSupportedProtocols = (supportedProtocols as IReadOnlyList<string>) ?? supportedProtocols.ToList();
135143
if (!await connectionContext.HandshakeAsync(handshakeTimeout, resolvedSupportedProtocols, _protocolResolver, _userIdProvider, _enableDetailedErrors))

src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs

+72-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal;
1818

1919
internal sealed partial class DefaultHubDispatcher<THub> : HubDispatcher<THub> where THub : Hub
2020
{
21+
private static readonly string _fullHubName = typeof(THub).FullName ?? typeof(THub).Name;
22+
2123
private readonly Dictionary<string, HubMethodDescriptor> _methods = new(StringComparer.OrdinalIgnoreCase);
2224
private readonly Utf8HashLookup _cachedMethodNames = new();
2325
private readonly IServiceScopeFactory _serviceScopeFactory;
@@ -76,11 +78,14 @@ public override async Task OnConnectedAsync(HubConnectionContext connection)
7678

7779
var hubActivator = scope.ServiceProvider.GetRequiredService<IHubActivator<THub>>();
7880
var hub = hubActivator.Create();
81+
Activity? activity = null;
7982
try
8083
{
8184
// OnConnectedAsync won't work with client results (ISingleClientProxy.InvokeAsync)
8285
InitializeHub(hub, connection, invokeAllowed: false);
8386

87+
activity = StartActivity(connection, scope.ServiceProvider, nameof(hub.OnConnectedAsync));
88+
8489
if (_onConnectedMiddleware != null)
8590
{
8691
var context = new HubLifetimeContext(connection.HubCallerContext, scope.ServiceProvider, hub);
@@ -91,8 +96,14 @@ public override async Task OnConnectedAsync(HubConnectionContext connection)
9196
await hub.OnConnectedAsync();
9297
}
9398
}
99+
catch (Exception ex)
100+
{
101+
SetActivityError(activity, ex);
102+
throw;
103+
}
94104
finally
95105
{
106+
activity?.Stop();
96107
hubActivator.Release(hub);
97108
}
98109
}
@@ -103,10 +114,13 @@ public override async Task OnDisconnectedAsync(HubConnectionContext connection,
103114

104115
var hubActivator = scope.ServiceProvider.GetRequiredService<IHubActivator<THub>>();
105116
var hub = hubActivator.Create();
117+
Activity? activity = null;
106118
try
107119
{
108120
InitializeHub(hub, connection);
109121

122+
activity = StartActivity(connection, scope.ServiceProvider, nameof(hub.OnDisconnectedAsync));
123+
110124
if (_onDisconnectedMiddleware != null)
111125
{
112126
var context = new HubLifetimeContext(connection.HubCallerContext, scope.ServiceProvider, hub);
@@ -117,8 +131,14 @@ public override async Task OnDisconnectedAsync(HubConnectionContext connection,
117131
await hub.OnDisconnectedAsync(exception);
118132
}
119133
}
134+
catch (Exception ex)
135+
{
136+
SetActivityError(activity, ex);
137+
throw;
138+
}
120139
finally
121140
{
141+
activity?.Stop();
122142
hubActivator.Release(hub);
123143
}
124144
}
@@ -367,6 +387,10 @@ static async Task ExecuteInvocation(DefaultHubDispatcher<THub> dispatcher,
367387
var logger = dispatcher._logger;
368388
var enableDetailedErrors = dispatcher._enableDetailedErrors;
369389

390+
// Use hubMethodInvocationMessage.Target instead of methodExecutor.MethodInfo.Name
391+
// We want to take HubMethodNameAttribute into account which will be the same as what the invocation target is
392+
var activity = StartActivity(connection, scope.ServiceProvider, hubMethodInvocationMessage.Target);
393+
370394
object? result;
371395
try
372396
{
@@ -375,13 +399,17 @@ static async Task ExecuteInvocation(DefaultHubDispatcher<THub> dispatcher,
375399
}
376400
catch (Exception ex)
377401
{
402+
SetActivityError(activity, ex);
403+
378404
Log.FailedInvokingHubMethod(logger, hubMethodInvocationMessage.Target, ex);
379405
await SendInvocationError(hubMethodInvocationMessage.InvocationId, connection,
380406
ErrorMessageHelper.BuildErrorMessage($"An unexpected error occurred invoking '{hubMethodInvocationMessage.Target}' on the server.", ex, enableDetailedErrors));
381407
return;
382408
}
383409
finally
384410
{
411+
activity?.Stop();
412+
385413
// Stream response handles cleanup in StreamResultsAsync
386414
// And normal invocations handle cleanup below in the finally
387415
if (isStreamCall)
@@ -467,6 +495,8 @@ private async Task StreamAsync(string invocationId, HubConnectionContext connect
467495

468496
streamCts ??= CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted);
469497

498+
var activity = StartActivity(connection, scope.ServiceProvider, hubMethodInvocationMessage.Target);
499+
470500
try
471501
{
472502
if (!connection.ActiveRequestCancellationSources.TryAdd(invocationId, streamCts))
@@ -483,6 +513,8 @@ private async Task StreamAsync(string invocationId, HubConnectionContext connect
483513
}
484514
catch (Exception ex)
485515
{
516+
SetActivityError(activity, ex);
517+
486518
Log.FailedInvokingHubMethod(_logger, hubMethodInvocationMessage.Target, ex);
487519
error = ErrorMessageHelper.BuildErrorMessage($"An unexpected error occurred invoking '{hubMethodInvocationMessage.Target}' on the server.", ex, _enableDetailedErrors);
488520
return;
@@ -509,20 +541,27 @@ private async Task StreamAsync(string invocationId, HubConnectionContext connect
509541
catch (ChannelClosedException ex)
510542
{
511543
// If the channel closes from an exception in the streaming method, grab the innerException for the error from the streaming method
512-
Log.FailedStreaming(_logger, invocationId, descriptor.MethodExecutor.MethodInfo.Name, ex.InnerException ?? ex);
513-
error = ErrorMessageHelper.BuildErrorMessage("An error occurred on the server while streaming results.", ex.InnerException ?? ex, _enableDetailedErrors);
544+
var exception = ex.InnerException ?? ex;
545+
SetActivityError(activity, exception);
546+
547+
Log.FailedStreaming(_logger, invocationId, descriptor.MethodExecutor.MethodInfo.Name, exception);
548+
error = ErrorMessageHelper.BuildErrorMessage("An error occurred on the server while streaming results.", exception, _enableDetailedErrors);
514549
}
515550
catch (Exception ex)
516551
{
517552
// If the streaming method was canceled we don't want to send a HubException message - this is not an error case
518553
if (!(ex is OperationCanceledException && streamCts.IsCancellationRequested))
519554
{
555+
SetActivityError(activity, ex);
556+
520557
Log.FailedStreaming(_logger, invocationId, descriptor.MethodExecutor.MethodInfo.Name, ex);
521558
error = ErrorMessageHelper.BuildErrorMessage("An error occurred on the server while streaming results.", ex, _enableDetailedErrors);
522559
}
523560
}
524561
finally
525562
{
563+
activity?.Stop();
564+
526565
await CleanupInvocation(connection, hubMethodInvocationMessage, hubActivator, hub, scope);
527566

528567
streamCts.Dispose();
@@ -749,4 +788,35 @@ public override IReadOnlyList<Type> GetParameterTypes(string methodName)
749788

750789
return null;
751790
}
791+
792+
// Starts an Activity for a Hub method invocation and sets up all the tags and other state.
793+
// Make sure to call Activity.Stop() once the Hub method completes, and consider calling SetActivityError on exception.
794+
private static Activity? StartActivity(HubConnectionContext connectionContext, IServiceProvider serviceProvider, string methodName)
795+
{
796+
if (serviceProvider.GetService<SignalRActivitySource>() is SignalRActivitySource signalRActivitySource
797+
&& signalRActivitySource.ActivitySource.HasListeners())
798+
{
799+
var requestContext = connectionContext.OriginalActivity?.Context;
800+
801+
return signalRActivitySource.ActivitySource.StartActivity($"{_fullHubName}/{methodName}", ActivityKind.Server, parentId: null,
802+
// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md#server-attributes
803+
tags: [
804+
new("rpc.method", methodName),
805+
new("rpc.system", "signalr"),
806+
new("rpc.service", _fullHubName),
807+
// See https://github.com/dotnet/aspnetcore/blob/027c60168383421750f01e427e4f749d0684bc02/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelMetrics.cs#L308
808+
// And https://github.com/dotnet/aspnetcore/issues/43786
809+
//new("server.address", ...),
810+
],
811+
links: requestContext.HasValue ? [new ActivityLink(requestContext.Value)] : null);
812+
}
813+
814+
return null;
815+
}
816+
817+
private static void SetActivityError(Activity? activity, Exception ex)
818+
{
819+
activity?.SetTag("error.type", ex.GetType().FullName);
820+
activity?.SetStatus(ActivityStatusCode.Error);
821+
}
752822
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
6+
namespace Microsoft.AspNetCore.SignalR.Internal;
7+
8+
// Internal for now so we don't need API review.
9+
// Just a wrapper for the ActivitySource
10+
// don't want to put ActivitySource directly in DI as hosting already does that and it could get overwritten.
11+
internal sealed class SignalRActivitySource
12+
{
13+
public ActivitySource ActivitySource { get; } = new ActivitySource("Microsoft.AspNetCore.SignalR.Server");
14+
}

src/SignalR/server/Core/src/SignalRDependencyInjectionExtensions.cs

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public static ISignalRServerBuilder AddSignalRCore(this IServiceCollection servi
3131
services.TryAddScoped(typeof(IHubActivator<>), typeof(DefaultHubActivator<>));
3232
services.AddAuthorization();
3333

34+
services.TryAddSingleton(new SignalRActivitySource());
35+
3436
var builder = new SignalRServerBuilder(services);
3537
builder.AddJsonProtocol();
3638
return builder;

src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs

+19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using System.Globalization;
56
using System.Runtime.CompilerServices;
67
using System.Text;
@@ -369,6 +370,12 @@ public async IAsyncEnumerable<int> StreamWithClientResult()
369370
var sum = await Clients.Caller.InvokeAsync<int>("Sum", 1, cancellationToken: default);
370371
yield return sum;
371372
}
373+
374+
public void ActivityMethod(TestActivitySource testActivitySource)
375+
{
376+
var activity = testActivitySource.ActivitySource.StartActivity("inner", ActivityKind.Server);
377+
activity.Stop();
378+
}
372379
}
373380

374381
internal class SelfRef
@@ -381,6 +388,11 @@ public SelfRef()
381388
public SelfRef Self { get; set; }
382389
}
383390

391+
public class TestActivitySource
392+
{
393+
public ActivitySource ActivitySource { get; set; }
394+
}
395+
384396
public abstract class TestHub : Hub
385397
{
386398
public override Task OnConnectedAsync()
@@ -709,6 +721,13 @@ public async Task<ChannelReader<string>> CounterChannelAsync(int count)
709721
return CounterChannel(count);
710722
}
711723

724+
[HubMethodName("RenamedCounterChannel")]
725+
public async Task<ChannelReader<string>> CounterChannelAsync2(int count)
726+
{
727+
await Task.Yield();
728+
return CounterChannel(count);
729+
}
730+
712731
public async ValueTask<ChannelReader<string>> CounterChannelValueTaskAsync(int count)
713732
{
714733
await Task.Yield();

0 commit comments

Comments
 (0)