Skip to content

Commit 52c55b9

Browse files
BrennanConroyKielekrajkumar-rangaraj
authored
[Instrumentation.AspNetCore] Listen to SignalR server activities (open-telemetry#2539)
Co-authored-by: Piotr Kiełkowicz <[email protected]> Co-authored-by: Rajkumar Rangaraj <[email protected]>
1 parent 70ed217 commit 52c55b9

File tree

9 files changed

+158
-5
lines changed

9 files changed

+158
-5
lines changed

build/Common.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
Refer to https://docs.microsoft.com/en-us/nuget/concepts/package-versioning for semver syntax.
3535
-->
3636
<MinVerPkgVer>[6.0.0,7.0)</MinVerPkgVer>
37+
<MicrosoftAspNetCoreSignalRClientPkgVer>[9.0.0,)</MicrosoftAspNetCoreSignalRClientPkgVer>
3738
<MicrosoftExtensionsHostingAbstractionsPkgVer>[2.1.0,5.0)</MicrosoftExtensionsHostingAbstractionsPkgVer>
3839
<MicrosoftExtensionsConfigurationPkgVer>[9.0.0,)</MicrosoftExtensionsConfigurationPkgVer>
3940
<MicrosoftExtensionsOptionsPkgVer>[9.0.0,)</MicrosoftExtensionsOptionsPkgVer>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.EnableAspNetCoreSignalRSupport.get -> bool
2+
OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.EnableAspNetCoreSignalRSupport.set -> void

src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public static TracerProviderBuilder AddAspNetCoreInstrumentation(
6868
{
6969
deferredTracerProviderBuilder.Configure((sp, builder) =>
7070
{
71-
AddAspNetCoreInstrumentationSources(builder, sp);
71+
AddAspNetCoreInstrumentationSources(builder, name, sp);
7272
});
7373
}
7474

@@ -84,9 +84,12 @@ public static TracerProviderBuilder AddAspNetCoreInstrumentation(
8484
// Note: This is used by unit tests.
8585
internal static TracerProviderBuilder AddAspNetCoreInstrumentation(
8686
this TracerProviderBuilder builder,
87-
HttpInListener listener)
87+
HttpInListener listener,
88+
string? optionsName = null)
8889
{
89-
builder.AddAspNetCoreInstrumentationSources();
90+
optionsName ??= Options.DefaultName;
91+
92+
builder.AddAspNetCoreInstrumentationSources(optionsName);
9093

9194
#pragma warning disable CA2000
9295
return builder.AddInstrumentation(
@@ -96,6 +99,7 @@ internal static TracerProviderBuilder AddAspNetCoreInstrumentation(
9699

97100
private static void AddAspNetCoreInstrumentationSources(
98101
this TracerProviderBuilder builder,
102+
string optionsName,
99103
IServiceProvider? serviceProvider = null)
100104
{
101105
// For .NET7.0 onwards activity will be created using activitySource.
@@ -121,5 +125,16 @@ private static void AddAspNetCoreInstrumentationSources(
121125
builder.AddSource(HttpInListener.ActivitySourceName);
122126
builder.AddLegacySource(HttpInListener.ActivityOperationName); // for the activities created by AspNetCore
123127
}
128+
129+
// SignalR activities first added in .NET 9.0
130+
if (Environment.Version.Major >= 9)
131+
{
132+
var options = serviceProvider?.GetRequiredService<IOptionsMonitor<AspNetCoreTraceInstrumentationOptions>>().Get(optionsName);
133+
if (options is null || options.EnableAspNetCoreSignalRSupport)
134+
{
135+
// https://github.com/dotnet/aspnetcore/blob/6ae3ea387b20f6497b82897d613e9b8a6e31d69c/src/SignalR/server/Core/src/Internal/SignalRServerActivitySource.cs#L13C35-L13C70
136+
builder.AddSource("Microsoft.AspNetCore.SignalR.Server");
137+
}
138+
}
124139
}
125140
}

src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ internal AspNetCoreTraceInstrumentationOptions(IConfiguration configuration)
9393
/// </remarks>
9494
public bool RecordException { get; set; }
9595

96+
/// <summary>
97+
/// Gets or sets a value indicating whether SignalR activities are recorded.
98+
/// </summary>
99+
/// <remarks>
100+
/// Defaults to true.
101+
/// Only applies to .NET 9.0 or greater.
102+
/// </remarks>
103+
public bool EnableAspNetCoreSignalRSupport { get; set; } = true;
104+
96105
/// <summary>
97106
/// Gets or sets a value indicating whether RPC attributes are added to an Activity when using Grpc.AspNetCore.
98107
/// </summary>

src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
* Added support for listening to ASP.NET Core SignalR activities.
6+
Configurable with the
7+
`AspNetCoreTraceInstrumentationOptions.EnableAspNetCoreSignalRSupport`
8+
option which defaults to `true`. Only applies to .NET 9.0 or greater.
9+
510
## 1.11.0
611

712
Released 2025-Jan-27

test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.AspNetCore.Hosting;
88
using Microsoft.AspNetCore.Http;
99
using Microsoft.AspNetCore.Mvc.Testing;
10+
using Microsoft.AspNetCore.SignalR.Client;
1011
using Microsoft.AspNetCore.TestHost;
1112
using Microsoft.Extensions.Configuration;
1213
using Microsoft.Extensions.DependencyInjection;
@@ -1114,6 +1115,104 @@ public async Task ValidateUrlQueryRedaction(string urlQuery, string expectedUrlQ
11141115
Assert.Equal(expectedUrlQuery, activity.GetTagValue(SemanticConventions.AttributeUrlQuery));
11151116
}
11161117

1118+
#if NET9_0_OR_GREATER
1119+
[Fact]
1120+
public async Task SignalRActivitesAreListenedTo()
1121+
{
1122+
var exportedItems = new List<Activity>();
1123+
void ConfigureTestServices(IServiceCollection services)
1124+
{
1125+
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
1126+
.AddAspNetCoreInstrumentation()
1127+
.AddInMemoryExporter(exportedItems)
1128+
.Build();
1129+
}
1130+
1131+
// Arrange
1132+
using (var server = this.factory
1133+
.WithWebHostBuilder(builder =>
1134+
{
1135+
builder.ConfigureTestServices(ConfigureTestServices);
1136+
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
1137+
}))
1138+
{
1139+
await using var client = new HubConnectionBuilder()
1140+
.WithUrl(server.Server.BaseAddress + "testHub", o =>
1141+
{
1142+
o.HttpMessageHandlerFactory = _ => server.Server.CreateHandler();
1143+
o.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.LongPolling;
1144+
}).Build();
1145+
await client.StartAsync();
1146+
1147+
await client.SendAsync("Send", "text");
1148+
1149+
await client.StopAsync();
1150+
}
1151+
1152+
WaitForActivityExport(exportedItems, 10);
1153+
1154+
var hubActivity = exportedItems
1155+
.Where(a => a.DisplayName.StartsWith("TestApp.AspNetCore.TestHub", StringComparison.InvariantCulture));
1156+
1157+
Assert.Equal(3, hubActivity.Count());
1158+
Assert.Collection(
1159+
hubActivity,
1160+
one =>
1161+
{
1162+
Assert.Equal("TestApp.AspNetCore.TestHub/OnConnectedAsync", one.DisplayName);
1163+
},
1164+
two =>
1165+
{
1166+
Assert.Equal("TestApp.AspNetCore.TestHub/Send", two.DisplayName);
1167+
},
1168+
three =>
1169+
{
1170+
Assert.Equal("TestApp.AspNetCore.TestHub/OnDisconnectedAsync", three.DisplayName);
1171+
});
1172+
}
1173+
1174+
[Fact]
1175+
public async Task SignalRActivitesCanBeDisabled()
1176+
{
1177+
var exportedItems = new List<Activity>();
1178+
void ConfigureTestServices(IServiceCollection services)
1179+
{
1180+
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
1181+
.AddAspNetCoreInstrumentation(o => o.EnableAspNetCoreSignalRSupport = false)
1182+
.AddInMemoryExporter(exportedItems)
1183+
.Build();
1184+
}
1185+
1186+
// Arrange
1187+
using (var server = this.factory
1188+
.WithWebHostBuilder(builder =>
1189+
{
1190+
builder.ConfigureTestServices(ConfigureTestServices);
1191+
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
1192+
}))
1193+
{
1194+
await using var client = new HubConnectionBuilder()
1195+
.WithUrl(server.Server.BaseAddress + "testHub", o =>
1196+
{
1197+
o.HttpMessageHandlerFactory = _ => server.Server.CreateHandler();
1198+
o.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.LongPolling;
1199+
}).Build();
1200+
await client.StartAsync();
1201+
1202+
await client.SendAsync("Send", "text");
1203+
1204+
await client.StopAsync();
1205+
}
1206+
1207+
WaitForActivityExport(exportedItems, 8);
1208+
1209+
var hubActivity = exportedItems
1210+
.Where(a => a.DisplayName.StartsWith("TestApp.AspNetCore.TestHub", StringComparison.InvariantCulture));
1211+
1212+
Assert.Empty(hubActivity);
1213+
}
1214+
#endif
1215+
11171216
public void Dispose()
11181217
{
11191218
this.tracerProvider?.Dispose();
@@ -1124,13 +1223,15 @@ private static void WaitForActivityExport(List<Activity> exportedItems, int coun
11241223
// We need to let End callback execute as it is executed AFTER response was returned.
11251224
// In unit tests environment there may be a lot of parallel unit tests executed, so
11261225
// giving some breezing room for the End callback to complete
1127-
Assert.True(SpinWait.SpinUntil(
1226+
Assert.True(
1227+
SpinWait.SpinUntil(
11281228
() =>
11291229
{
11301230
Thread.Sleep(10);
11311231
return exportedItems.Count >= count;
11321232
},
1133-
TimeSpan.FromSeconds(1)));
1233+
TimeSpan.FromSeconds(1)),
1234+
$"Actual: {exportedItems.Count} Expected: {count}");
11341235
}
11351236

11361237
private static void ValidateAspNetCoreActivity(Activity activityToValidate, string expectedHttpPath)

test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<ItemGroup>
1919
<PackageReference Include="OpenTelemetry.Exporter.InMemory" Version="$(OpenTelemetryCoreLatestVersion)" />
2020
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="$(OpenTelemetryCoreLatestVersion)" />
21+
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="$(MicrosoftAspNetCoreSignalRClientPkgVer)" />
2122
</ItemGroup>
2223

2324
<ItemGroup>

test/TestApp.AspNetCore/Program.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public static void Main(string[] args)
2020

2121
builder.Services.AddMvc();
2222

23+
builder.Services.AddSignalR();
24+
2325
builder.Services.AddSingleton<HttpClient>();
2426

2527
builder.Services.AddSingleton(
@@ -43,6 +45,8 @@ public static void Main(string[] args)
4345

4446
app.MapControllers();
4547

48+
app.MapHub<TestHub>("/testHub");
49+
4650
app.UseMiddleware<CallbackMiddleware>();
4751

4852
app.UseMiddleware<ActivityMiddleware>();

test/TestApp.AspNetCore/TestHub.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using Microsoft.AspNetCore.SignalR;
5+
6+
namespace TestApp.AspNetCore;
7+
8+
public class TestHub : Hub
9+
{
10+
public override Task OnConnectedAsync() => base.OnConnectedAsync();
11+
12+
public void Send(string message)
13+
{
14+
}
15+
}

0 commit comments

Comments
 (0)