diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelMetrics.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelMetrics.cs
index 8b87922f90a8..931e22669a9f 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelMetrics.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelMetrics.cs
@@ -4,8 +4,6 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Metrics;
-using System.Net;
-using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Security.Authentication;
using Microsoft.AspNetCore.Connections;
@@ -321,43 +319,7 @@ private void TlsHandshakeStopCore(ConnectionMetricsContext metricsContext, long
private static void InitializeConnectionTags(ref TagList tags, in ConnectionMetricsContext metricsContext)
{
- var localEndpoint = metricsContext.ConnectionContext.LocalEndPoint;
- if (localEndpoint is IPEndPoint localIPEndPoint)
- {
- tags.Add("server.address", localIPEndPoint.Address.ToString());
- tags.Add("server.port", localIPEndPoint.Port);
-
- switch (localIPEndPoint.Address.AddressFamily)
- {
- case AddressFamily.InterNetwork:
- tags.Add("network.type", "ipv4");
- break;
- case AddressFamily.InterNetworkV6:
- tags.Add("network.type", "ipv6");
- break;
- }
-
- // There isn't an easy way to detect whether QUIC is the underlying transport.
- // This code assumes that a multiplexed connection is QUIC.
- // Improve in the future if there are additional multiplexed connection types.
- var transport = metricsContext.ConnectionContext is not MultiplexedConnectionContext ? "tcp" : "udp";
- tags.Add("network.transport", transport);
- }
- else if (localEndpoint is UnixDomainSocketEndPoint udsEndPoint)
- {
- tags.Add("server.address", udsEndPoint.ToString());
- tags.Add("network.transport", "unix");
- }
- else if (localEndpoint is NamedPipeEndPoint namedPipeEndPoint)
- {
- tags.Add("server.address", namedPipeEndPoint.ToString());
- tags.Add("network.transport", "pipe");
- }
- else if (localEndpoint != null)
- {
- tags.Add("server.address", localEndpoint.ToString());
- tags.Add("network.transport", localEndpoint.AddressFamily.ToString());
- }
+ ConnectionEndpointTags.AddConnectionEndpointTags(ref tags, metricsContext.ConnectionContext);
}
public ConnectionMetricsContext CreateContext(BaseConnectionContext connection)
diff --git a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj
index 9199ae066c0a..8ce64dcf342f 100644
--- a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj
+++ b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj
@@ -37,6 +37,7 @@
+
diff --git a/src/Servers/Kestrel/Core/test/TransportConnectionFeatureCollectionTests.cs b/src/Servers/Kestrel/Core/test/TransportConnectionFeatureCollectionTests.cs
new file mode 100644
index 000000000000..d93126cc5049
--- /dev/null
+++ b/src/Servers/Kestrel/Core/test/TransportConnectionFeatureCollectionTests.cs
@@ -0,0 +1,103 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Connections.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
+using Microsoft.AspNetCore.InternalTesting;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests;
+
+public class TransportConnectionFeatureCollectionTests
+{
+ [Fact]
+ public void IConnectionEndPointFeature_IsAvailableInFeatureCollection()
+ {
+ var serviceContext = new TestServiceContext();
+ var connection = new Mock { CallBase = true }.Object;
+ var transportConnectionManager = new TransportConnectionManager(serviceContext.ConnectionManager);
+ var kestrelConnection = CreateKestrelConnection(serviceContext, connection, transportConnectionManager);
+
+ var endpointFeature = kestrelConnection.TransportConnection.Features.Get();
+
+ Assert.NotNull(endpointFeature);
+ }
+
+ [Fact]
+ public void IConnectionEndPointFeature_ReturnsCorrectLocalEndPoint()
+ {
+ var serviceContext = new TestServiceContext();
+ var connection = new Mock { CallBase = true }.Object;
+ var expectedLocalEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
+ connection.LocalEndPoint = expectedLocalEndPoint;
+ var transportConnectionManager = new TransportConnectionManager(serviceContext.ConnectionManager);
+ var kestrelConnection = CreateKestrelConnection(serviceContext, connection, transportConnectionManager);
+
+ var endpointFeature = kestrelConnection.TransportConnection.Features.Get();
+
+ Assert.NotNull(endpointFeature);
+ Assert.Equal(expectedLocalEndPoint, endpointFeature.LocalEndPoint);
+ }
+
+ [Fact]
+ public void IConnectionEndPointFeature_ReturnsCorrectRemoteEndPoint()
+ {
+ var serviceContext = new TestServiceContext();
+ var connection = new Mock { CallBase = true }.Object;
+ var expectedRemoteEndPoint = new IPEndPoint(IPAddress.Parse("192.168.1.100"), 54321);
+ connection.RemoteEndPoint = expectedRemoteEndPoint;
+ var transportConnectionManager = new TransportConnectionManager(serviceContext.ConnectionManager);
+ var kestrelConnection = CreateKestrelConnection(serviceContext, connection, transportConnectionManager);
+
+ var endpointFeature = kestrelConnection.TransportConnection.Features.Get();
+
+ Assert.NotNull(endpointFeature);
+ Assert.Equal(expectedRemoteEndPoint, endpointFeature.RemoteEndPoint);
+ }
+
+ [Fact]
+ public void IConnectionEndPointFeature_AllowsSettingLocalEndPoint()
+ {
+ var serviceContext = new TestServiceContext();
+ var connection = new Mock { CallBase = true }.Object;
+ var transportConnectionManager = new TransportConnectionManager(serviceContext.ConnectionManager);
+ var kestrelConnection = CreateKestrelConnection(serviceContext, connection, transportConnectionManager);
+ var newLocalEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9090);
+
+ var endpointFeature = kestrelConnection.TransportConnection.Features.Get();
+ endpointFeature.LocalEndPoint = newLocalEndPoint;
+
+ Assert.Equal(newLocalEndPoint, kestrelConnection.TransportConnection.LocalEndPoint);
+ Assert.Equal(newLocalEndPoint, endpointFeature.LocalEndPoint);
+ }
+
+ [Fact]
+ public void IConnectionEndPointFeature_AllowsSettingRemoteEndPoint()
+ {
+ var serviceContext = new TestServiceContext();
+ var connection = new Mock { CallBase = true }.Object;
+ var transportConnectionManager = new TransportConnectionManager(serviceContext.ConnectionManager);
+ var kestrelConnection = CreateKestrelConnection(serviceContext, connection, transportConnectionManager);
+ var newRemoteEndPoint = new IPEndPoint(IPAddress.Parse("10.0.0.1"), 12345);
+
+ var endpointFeature = kestrelConnection.TransportConnection.Features.Get();
+ endpointFeature.RemoteEndPoint = newRemoteEndPoint;
+
+ Assert.Equal(newRemoteEndPoint, kestrelConnection.TransportConnection.RemoteEndPoint);
+ Assert.Equal(newRemoteEndPoint, endpointFeature.RemoteEndPoint);
+ }
+
+ private static KestrelConnection CreateKestrelConnection(TestServiceContext serviceContext, DefaultConnectionContext connection, TransportConnectionManager transportConnectionManager, Func connectionDelegate = null)
+ {
+ connectionDelegate ??= _ => Task.CompletedTask;
+
+ return new KestrelConnection(
+ id: 0, serviceContext, transportConnectionManager, connectionDelegate, connection, serviceContext.Log, TestContextFactory.CreateMetricsContext(connection));
+ }
+}
\ No newline at end of file
diff --git a/src/Servers/Kestrel/shared/TransportConnection.FeatureCollection.cs b/src/Servers/Kestrel/shared/TransportConnection.FeatureCollection.cs
index 6aedabe43c5d..39f3c610b790 100644
--- a/src/Servers/Kestrel/shared/TransportConnection.FeatureCollection.cs
+++ b/src/Servers/Kestrel/shared/TransportConnection.FeatureCollection.cs
@@ -3,6 +3,7 @@
using System.Buffers;
using System.IO.Pipelines;
+using System.Net;
using Microsoft.AspNetCore.Connections.Features;
#nullable enable
@@ -37,4 +38,16 @@ CancellationToken IConnectionLifetimeFeature.ConnectionClosed
}
void IConnectionLifetimeFeature.Abort() => Abort(new ConnectionAbortedException("The connection was aborted by the application via IConnectionLifetimeFeature.Abort()."));
+
+ EndPoint? IConnectionEndPointFeature.LocalEndPoint
+ {
+ get => LocalEndPoint;
+ set => LocalEndPoint = value;
+ }
+
+ EndPoint? IConnectionEndPointFeature.RemoteEndPoint
+ {
+ get => RemoteEndPoint;
+ set => RemoteEndPoint = value;
+ }
}
diff --git a/src/Servers/Kestrel/shared/TransportConnection.Generated.cs b/src/Servers/Kestrel/shared/TransportConnection.Generated.cs
index 303811649ac4..dcdeeea9d636 100644
--- a/src/Servers/Kestrel/shared/TransportConnection.Generated.cs
+++ b/src/Servers/Kestrel/shared/TransportConnection.Generated.cs
@@ -18,7 +18,8 @@ internal partial class TransportConnection : IFeatureCollection,
IConnectionTransportFeature,
IConnectionItemsFeature,
IMemoryPoolFeature,
- IConnectionLifetimeFeature
+ IConnectionLifetimeFeature,
+ IConnectionEndPointFeature
{
// Implemented features
internal protected IConnectionIdFeature? _currentIConnectionIdFeature;
@@ -26,6 +27,7 @@ internal partial class TransportConnection : IFeatureCollection,
internal protected IConnectionItemsFeature? _currentIConnectionItemsFeature;
internal protected IMemoryPoolFeature? _currentIMemoryPoolFeature;
internal protected IConnectionLifetimeFeature? _currentIConnectionLifetimeFeature;
+ internal protected IConnectionEndPointFeature? _currentIConnectionEndPointFeature;
// Other reserved feature slots
internal protected IPersistentStateFeature? _currentIPersistentStateFeature;
@@ -48,6 +50,7 @@ private void FastReset()
_currentIConnectionItemsFeature = this;
_currentIMemoryPoolFeature = this;
_currentIConnectionLifetimeFeature = this;
+ _currentIConnectionEndPointFeature = this;
_currentIPersistentStateFeature = null;
_currentIConnectionSocketFeature = null;
@@ -180,6 +183,10 @@ private void ExtraFeatureSet(Type key, object? value)
{
feature = _currentIConnectionMetricsTagsFeature;
}
+ else if (key == typeof(IConnectionEndPointFeature))
+ {
+ feature = _currentIConnectionEndPointFeature;
+ }
else if (MaybeExtra != null)
{
feature = ExtraFeatureGet(key);
@@ -244,6 +251,10 @@ private void ExtraFeatureSet(Type key, object? value)
{
_currentIConnectionMetricsTagsFeature = (IConnectionMetricsTagsFeature?)value;
}
+ else if (key == typeof(IConnectionEndPointFeature))
+ {
+ _currentIConnectionEndPointFeature = (IConnectionEndPointFeature?)value;
+ }
else
{
ExtraFeatureSet(key, value);
@@ -310,6 +321,10 @@ private void ExtraFeatureSet(Type key, object? value)
{
feature = Unsafe.As(ref _currentIConnectionMetricsTagsFeature);
}
+ else if (typeof(TFeature) == typeof(IConnectionEndPointFeature))
+ {
+ feature = Unsafe.As(ref _currentIConnectionEndPointFeature);
+ }
else if (MaybeExtra != null)
{
feature = (TFeature?)(ExtraFeatureGet(typeof(TFeature)));
@@ -382,6 +397,10 @@ private void ExtraFeatureSet(Type key, object? value)
{
_currentIConnectionMetricsTagsFeature = Unsafe.As(ref feature);
}
+ else if (typeof(TFeature) == typeof(IConnectionEndPointFeature))
+ {
+ _currentIConnectionEndPointFeature = Unsafe.As(ref feature);
+ }
else
{
ExtraFeatureSet(typeof(TFeature), feature);
@@ -442,6 +461,10 @@ private IEnumerable> FastEnumerable()
{
yield return new KeyValuePair(typeof(IConnectionMetricsTagsFeature), _currentIConnectionMetricsTagsFeature);
}
+ if (_currentIConnectionEndPointFeature != null)
+ {
+ yield return new KeyValuePair(typeof(IConnectionEndPointFeature), _currentIConnectionEndPointFeature);
+ }
if (MaybeExtra != null)
{
diff --git a/src/Servers/Kestrel/shared/TransportMultiplexedConnection.FeatureCollection.cs b/src/Servers/Kestrel/shared/TransportMultiplexedConnection.FeatureCollection.cs
index a4727c0885b0..72feb040b846 100644
--- a/src/Servers/Kestrel/shared/TransportMultiplexedConnection.FeatureCollection.cs
+++ b/src/Servers/Kestrel/shared/TransportMultiplexedConnection.FeatureCollection.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers;
+using System.Net;
using Microsoft.AspNetCore.Connections.Features;
namespace Microsoft.AspNetCore.Connections;
@@ -28,4 +29,16 @@ CancellationToken IConnectionLifetimeFeature.ConnectionClosed
}
void IConnectionLifetimeFeature.Abort() => Abort(new ConnectionAbortedException("The connection was aborted by the application via IConnectionLifetimeFeature.Abort()."));
+
+ EndPoint? IConnectionEndPointFeature.LocalEndPoint
+ {
+ get => LocalEndPoint;
+ set => LocalEndPoint = value;
+ }
+
+ EndPoint? IConnectionEndPointFeature.RemoteEndPoint
+ {
+ get => RemoteEndPoint;
+ set => RemoteEndPoint = value;
+ }
}
diff --git a/src/Servers/Kestrel/shared/TransportMultiplexedConnection.Generated.cs b/src/Servers/Kestrel/shared/TransportMultiplexedConnection.Generated.cs
index 60f5689a0f1c..22b95f65ef14 100644
--- a/src/Servers/Kestrel/shared/TransportMultiplexedConnection.Generated.cs
+++ b/src/Servers/Kestrel/shared/TransportMultiplexedConnection.Generated.cs
@@ -17,13 +17,15 @@ internal partial class TransportMultiplexedConnection : IFeatureCollection,
IConnectionIdFeature,
IConnectionItemsFeature,
IMemoryPoolFeature,
- IConnectionLifetimeFeature
+ IConnectionLifetimeFeature,
+ IConnectionEndPointFeature
{
// Implemented features
internal protected IConnectionIdFeature? _currentIConnectionIdFeature;
internal protected IConnectionItemsFeature? _currentIConnectionItemsFeature;
internal protected IMemoryPoolFeature? _currentIMemoryPoolFeature;
internal protected IConnectionLifetimeFeature? _currentIConnectionLifetimeFeature;
+ internal protected IConnectionEndPointFeature? _currentIConnectionEndPointFeature;
// Other reserved feature slots
internal protected IConnectionTransportFeature? _currentIConnectionTransportFeature;
@@ -40,6 +42,7 @@ private void FastReset()
_currentIConnectionItemsFeature = this;
_currentIMemoryPoolFeature = this;
_currentIConnectionLifetimeFeature = this;
+ _currentIConnectionEndPointFeature = this;
_currentIConnectionTransportFeature = null;
_currentIProtocolErrorCodeFeature = null;
@@ -135,6 +138,10 @@ private void ExtraFeatureSet(Type key, object? value)
{
feature = _currentIConnectionLifetimeFeature;
}
+ else if (key == typeof(IConnectionEndPointFeature))
+ {
+ feature = _currentIConnectionEndPointFeature;
+ }
else if (key == typeof(IProtocolErrorCodeFeature))
{
feature = _currentIProtocolErrorCodeFeature;
@@ -175,6 +182,10 @@ private void ExtraFeatureSet(Type key, object? value)
{
_currentIConnectionLifetimeFeature = (IConnectionLifetimeFeature?)value;
}
+ else if (key == typeof(IConnectionEndPointFeature))
+ {
+ _currentIConnectionEndPointFeature = (IConnectionEndPointFeature?)value;
+ }
else if (key == typeof(IProtocolErrorCodeFeature))
{
_currentIProtocolErrorCodeFeature = (IProtocolErrorCodeFeature?)value;
@@ -217,6 +228,10 @@ private void ExtraFeatureSet(Type key, object? value)
{
feature = Unsafe.As(ref _currentIConnectionLifetimeFeature);
}
+ else if (typeof(TFeature) == typeof(IConnectionEndPointFeature))
+ {
+ feature = Unsafe.As(ref _currentIConnectionEndPointFeature);
+ }
else if (typeof(TFeature) == typeof(IProtocolErrorCodeFeature))
{
feature = Unsafe.As(ref _currentIProtocolErrorCodeFeature);
@@ -260,6 +275,10 @@ private void ExtraFeatureSet(Type key, object? value)
{
_currentIConnectionLifetimeFeature = Unsafe.As(ref feature);
}
+ else if (typeof(TFeature) == typeof(IConnectionEndPointFeature))
+ {
+ _currentIConnectionEndPointFeature = Unsafe.As(ref feature);
+ }
else if (typeof(TFeature) == typeof(IProtocolErrorCodeFeature))
{
_currentIProtocolErrorCodeFeature = Unsafe.As(ref feature);
@@ -296,6 +315,10 @@ private IEnumerable> FastEnumerable()
{
yield return new KeyValuePair(typeof(IConnectionLifetimeFeature), _currentIConnectionLifetimeFeature);
}
+ if (_currentIConnectionEndPointFeature != null)
+ {
+ yield return new KeyValuePair(typeof(IConnectionEndPointFeature), _currentIConnectionEndPointFeature);
+ }
if (_currentIProtocolErrorCodeFeature != null)
{
yield return new KeyValuePair(typeof(IProtocolErrorCodeFeature), _currentIProtocolErrorCodeFeature);
diff --git a/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs b/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs
index cae7e34924c4..a2c9e921a4b4 100644
--- a/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs
+++ b/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs
@@ -24,7 +24,8 @@ public static string GenerateFile()
"IStreamIdFeature",
"IStreamAbortFeature",
"IStreamClosedFeature",
- "IConnectionMetricsTagsFeature"
+ "IConnectionMetricsTagsFeature",
+ "IConnectionEndPointFeature"
};
var implementedFeatures = new[]
@@ -33,7 +34,8 @@ public static string GenerateFile()
"IConnectionTransportFeature",
"IConnectionItemsFeature",
"IMemoryPoolFeature",
- "IConnectionLifetimeFeature"
+ "IConnectionLifetimeFeature",
+ "IConnectionEndPointFeature"
};
var usings = $@"
diff --git a/src/Servers/Kestrel/tools/CodeGenerator/TransportMultiplexedConnectionFeatureCollection.cs b/src/Servers/Kestrel/tools/CodeGenerator/TransportMultiplexedConnectionFeatureCollection.cs
index 29a4cd8a7c3a..dd4e5485caa7 100644
--- a/src/Servers/Kestrel/tools/CodeGenerator/TransportMultiplexedConnectionFeatureCollection.cs
+++ b/src/Servers/Kestrel/tools/CodeGenerator/TransportMultiplexedConnectionFeatureCollection.cs
@@ -16,6 +16,7 @@ public static string GenerateFile()
"IConnectionItemsFeature",
"IMemoryPoolFeature",
"IConnectionLifetimeFeature",
+ "IConnectionEndPointFeature",
"IProtocolErrorCodeFeature",
"ITlsConnectionFeature"
};
@@ -24,7 +25,8 @@ public static string GenerateFile()
"IConnectionIdFeature",
"IConnectionItemsFeature",
"IMemoryPoolFeature",
- "IConnectionLifetimeFeature"
+ "IConnectionLifetimeFeature",
+ "IConnectionEndPointFeature"
};
var usings = $@"
diff --git a/src/Shared/ConnectionEndpointTags.cs b/src/Shared/ConnectionEndpointTags.cs
new file mode 100644
index 000000000000..320ed963df9f
--- /dev/null
+++ b/src/Shared/ConnectionEndpointTags.cs
@@ -0,0 +1,126 @@
+// 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.Net;
+using System.Net.Sockets;
+using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Connections.Features;
+using Microsoft.AspNetCore.Http.Features;
+
+#nullable enable
+
+internal static class ConnectionEndpointTags
+{
+ ///
+ /// Adds connection endpoint tags to a TagList using IConnectionEndPointFeature.
+ ///
+ /// The TagList to add tags to.
+ /// The feature collection to get endpoint information from.
+ public static void AddConnectionEndpointTags(ref TagList tags, IFeatureCollection features)
+ {
+ var endpointFeature = features.Get();
+ if (endpointFeature is null)
+ {
+ return;
+ }
+
+ var localEndpoint = endpointFeature.LocalEndPoint;
+ if (localEndpoint is IPEndPoint localIPEndPoint)
+ {
+ tags.Add("server.address", localIPEndPoint.Address.ToString());
+ tags.Add("server.port", localIPEndPoint.Port);
+
+ switch (localIPEndPoint.Address.AddressFamily)
+ {
+ case AddressFamily.InterNetwork:
+ tags.Add("network.type", "ipv4");
+ break;
+ case AddressFamily.InterNetworkV6:
+ tags.Add("network.type", "ipv6");
+ break;
+ }
+
+ // There isn't an easy way to detect whether QUIC is the underlying transport.
+ // This code assumes that a multiplexed connection is QUIC.
+ // Improve in the future if there are additional multiplexed connection types.
+ // Note: We can't determine transport from features alone, so we default to "tcp"
+ tags.Add("network.transport", "tcp");
+ }
+ else if (localEndpoint is UnixDomainSocketEndPoint udsEndPoint)
+ {
+ tags.Add("server.address", udsEndPoint.ToString());
+ tags.Add("network.transport", "unix");
+ }
+ else if (localEndpoint is NamedPipeEndPoint namedPipeEndPoint)
+ {
+ tags.Add("server.address", namedPipeEndPoint.ToString());
+ tags.Add("network.transport", "pipe");
+ }
+ else if (localEndpoint != null)
+ {
+ tags.Add("server.address", localEndpoint.ToString());
+ tags.Add("network.transport", localEndpoint.AddressFamily.ToString());
+ }
+ }
+
+ ///
+ /// Adds connection endpoint tags to a TagList using IConnectionEndPointFeature.
+ ///
+ /// The TagList to add tags to.
+ /// The connection context to get endpoint information from.
+ public static void AddConnectionEndpointTags(ref TagList tags, BaseConnectionContext connectionContext)
+ {
+ EndPoint? localEndpoint = null;
+
+ // Try to get the local endpoint from the feature first, then fall back to the direct property
+ var endpointFeature = connectionContext.Features.Get();
+ if (endpointFeature is not null)
+ {
+ localEndpoint = endpointFeature.LocalEndPoint;
+ }
+
+ // If the feature didn't provide an endpoint, fall back to the direct property
+ if (localEndpoint is null)
+ {
+ localEndpoint = connectionContext.LocalEndPoint;
+ }
+
+ if (localEndpoint is IPEndPoint localIPEndPoint)
+ {
+ tags.Add("server.address", localIPEndPoint.Address.ToString());
+ tags.Add("server.port", localIPEndPoint.Port);
+
+ switch (localIPEndPoint.Address.AddressFamily)
+ {
+ case AddressFamily.InterNetwork:
+ tags.Add("network.type", "ipv4");
+ break;
+ case AddressFamily.InterNetworkV6:
+ tags.Add("network.type", "ipv6");
+ break;
+ }
+
+ // There isn't an easy way to detect whether QUIC is the underlying transport.
+ // This code assumes that a multiplexed connection is QUIC.
+ // Improve in the future if there are additional multiplexed connection types.
+ var transport = connectionContext is not MultiplexedConnectionContext ? "tcp" : "udp";
+ tags.Add("network.transport", transport);
+ }
+ else if (localEndpoint is UnixDomainSocketEndPoint udsEndPoint)
+ {
+ tags.Add("server.address", udsEndPoint.ToString());
+ tags.Add("network.transport", "unix");
+ }
+ else if (localEndpoint is NamedPipeEndPoint namedPipeEndPoint)
+ {
+ tags.Add("server.address", namedPipeEndPoint.ToString());
+ tags.Add("network.transport", "pipe");
+ }
+ else if (localEndpoint != null)
+ {
+ tags.Add("server.address", localEndpoint.ToString());
+ tags.Add("network.transport", localEndpoint.AddressFamily.ToString());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs
index 3a859123651d..3ca22f53e6c8 100644
--- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs
+++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs
@@ -31,6 +31,7 @@ internal sealed partial class HttpConnectionContext : ConnectionContext,
IConnectionInherentKeepAliveFeature,
IConnectionLifetimeFeature,
IConnectionLifetimeNotificationFeature,
+ IConnectionEndPointFeature,
#pragma warning disable CA2252 // This API requires opting into preview features
IStatefulReconnectFeature
#pragma warning restore CA2252 // This API requires opting into preview features
@@ -97,6 +98,7 @@ public HttpConnectionContext(string connectionId, string connectionToken, ILogge
Features.Set(this);
Features.Set(this);
Features.Set(this);
+ Features.Set(this);
if (useStatefulReconnect)
{
diff --git a/src/SignalR/common/Http.Connections/src/Microsoft.AspNetCore.Http.Connections.csproj b/src/SignalR/common/Http.Connections/src/Microsoft.AspNetCore.Http.Connections.csproj
index 6449f8900b1f..4d103f7800d5 100644
--- a/src/SignalR/common/Http.Connections/src/Microsoft.AspNetCore.Http.Connections.csproj
+++ b/src/SignalR/common/Http.Connections/src/Microsoft.AspNetCore.Http.Connections.csproj
@@ -29,6 +29,7 @@
+
diff --git a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs
index 5e914b31e29e..da58b44a2520 100644
--- a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs
+++ b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs
@@ -92,7 +92,7 @@ public override async Task OnConnectedAsync(HubConnectionContext connection)
// OnConnectedAsync won't work with client results (ISingleClientProxy.InvokeAsync)
InitializeHub(hub, connection, invokeAllowed: false);
- activity = StartActivity(SignalRServerActivitySource.OnConnected, ActivityKind.Internal, linkedActivity: null, scope.ServiceProvider, nameof(hub.OnConnectedAsync), headers: null, _logger);
+ activity = StartActivity(SignalRServerActivitySource.OnConnected, ActivityKind.Internal, linkedActivity: null, scope.ServiceProvider, nameof(hub.OnConnectedAsync), headers: null, _logger, connection);
if (_onConnectedMiddleware != null)
{
@@ -127,7 +127,7 @@ public override async Task OnDisconnectedAsync(HubConnectionContext connection,
{
InitializeHub(hub, connection);
- activity = StartActivity(SignalRServerActivitySource.OnDisconnected, ActivityKind.Internal, linkedActivity: null, scope.ServiceProvider, nameof(hub.OnDisconnectedAsync), headers: null, _logger);
+ activity = StartActivity(SignalRServerActivitySource.OnDisconnected, ActivityKind.Internal, linkedActivity: null, scope.ServiceProvider, nameof(hub.OnDisconnectedAsync), headers: null, _logger, connection);
if (_onDisconnectedMiddleware != null)
{
@@ -404,7 +404,7 @@ static async Task ExecuteInvocation(DefaultHubDispatcher dispatcher,
// 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(SignalRServerActivitySource.InvocationIn, ActivityKind.Server, connection.OriginalActivity, scope.ServiceProvider, hubMethodInvocationMessage.Target, hubMethodInvocationMessage.Headers, logger);
+ var activity = StartActivity(SignalRServerActivitySource.InvocationIn, ActivityKind.Server, connection.OriginalActivity, scope.ServiceProvider, hubMethodInvocationMessage.Target, hubMethodInvocationMessage.Headers, logger, connection);
object? result;
try
@@ -522,7 +522,7 @@ private async Task StreamAsync(string invocationId, HubConnectionContext connect
Activity.Current = null;
}
- var activity = StartActivity(SignalRServerActivitySource.InvocationIn, ActivityKind.Server, connection.OriginalActivity, scope.ServiceProvider, hubMethodInvocationMessage.Target, hubMethodInvocationMessage.Headers, _logger);
+ var activity = StartActivity(SignalRServerActivitySource.InvocationIn, ActivityKind.Server, connection.OriginalActivity, scope.ServiceProvider, hubMethodInvocationMessage.Target, hubMethodInvocationMessage.Headers, _logger, connection);
try
{
@@ -829,7 +829,7 @@ public override IReadOnlyList GetParameterTypes(string methodName)
// 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(string operationName, ActivityKind kind, Activity? linkedActivity, IServiceProvider serviceProvider, string methodName, IDictionary? headers, ILogger logger)
+ private static Activity? StartActivity(string operationName, ActivityKind kind, Activity? linkedActivity, IServiceProvider serviceProvider, string methodName, IDictionary? headers, ILogger logger, HubConnectionContext? connection = null)
{
var activitySource = serviceProvider.GetService()?.ActivitySource;
if (activitySource is null)
@@ -843,15 +843,20 @@ public override IReadOnlyList GetParameterTypes(string methodName)
return null;
}
- IEnumerable> 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", ...),
- ];
+ var tagList = new TagList
+ {
+ { "rpc.method", methodName },
+ { "rpc.system", "signalr" },
+ { "rpc.service", _fullHubName }
+ };
+
+ // Add connection endpoint tags if connection is available
+ if (connection is not null)
+ {
+ ConnectionEndpointTags.AddConnectionEndpointTags(ref tagList, connection.Features);
+ }
+
+ IEnumerable> tags = tagList;
IEnumerable? links = (linkedActivity is not null) ? [new ActivityLink(linkedActivity.Context)] : null;
Activity? activity;
diff --git a/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj
index f5016566f977..7e2402244776 100644
--- a/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj
+++ b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj
@@ -22,6 +22,7 @@
+
diff --git a/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.Activity.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.Activity.cs
index ba54aa4043b8..f73f51470f08 100644
--- a/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.Activity.cs
+++ b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.Activity.cs
@@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
+using System.Linq;
+using System.Net;
using System.Threading.Channels;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.AspNetCore.SignalR.Internal;
@@ -523,4 +525,93 @@ private static void AssertHubMethodActivity(Activity activity, Activity pa
Assert.Equal(linkedActivity.SpanId, Assert.Single(activity.Links).Context.SpanId);
}
}
+
+ [Fact]
+ public async Task HubMethodActivityIncludesConnectionEndpointTags()
+ {
+ using (StartVerifiableLog())
+ {
+ var serverChannel = Channel.CreateUnbounded();
+ 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 = a => serverChannel.Writer.TryWrite(a)
+ };
+ ActivitySource.AddActivityListener(listener);
+
+ var mockHttpRequestActivity = new Activity("HttpRequest");
+ mockHttpRequestActivity.Start();
+ Activity.Current = mockHttpRequestActivity;
+
+ var connectionHandler = serviceProvider.GetService>();
+
+ using (var client = new TestClient())
+ {
+ // Set up endpoint information on the connection to ensure the tags are added
+ client.Connection.LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 5000);
+ client.Connection.RemoteEndPoint = new IPEndPoint(IPAddress.Parse("192.168.1.100"), 12345);
+
+ var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout();
+
+ var connectActivity = await serverChannel.Reader.ReadAsync().DefaultTimeout();
+
+ // Verify that endpoint tags are included in the activity
+ var tags = connectActivity.Tags.ToArray();
+
+ // Should have the standard 3 rpc tags plus endpoint tags
+ Assert.True(tags.Length >= 3, $"Expected at least 3 tags, but found {tags.Length}");
+
+ // Verify the standard SignalR tags are present
+ var rpcMethodTag = tags.FirstOrDefault(t => t.Key == "rpc.method");
+ Assert.NotNull(rpcMethodTag.Key);
+ Assert.Equal(nameof(MethodHub.OnConnectedAsync), rpcMethodTag.Value);
+
+ var rpcSystemTag = tags.FirstOrDefault(t => t.Key == "rpc.system");
+ Assert.NotNull(rpcSystemTag.Key);
+ Assert.Equal("signalr", rpcSystemTag.Value);
+
+ var rpcServiceTag = tags.FirstOrDefault(t => t.Key == "rpc.service");
+ Assert.NotNull(rpcServiceTag.Key);
+ Assert.Equal(typeof(MethodHub).FullName, rpcServiceTag.Value);
+
+ // Verify endpoint tags are present
+ var serverAddressTag = tags.FirstOrDefault(t => t.Key == "server.address");
+ Assert.NotNull(serverAddressTag.Key);
+ Assert.Equal("127.0.0.1", serverAddressTag.Value);
+
+ var serverPortTag = tags.FirstOrDefault(t => t.Key == "server.port");
+ Assert.NotNull(serverPortTag.Key);
+ Assert.Equal(5000, serverPortTag.Value);
+
+ var networkTypeTag = tags.FirstOrDefault(t => t.Key == "network.type");
+ Assert.NotNull(networkTypeTag.Key);
+ Assert.Equal("ipv4", networkTypeTag.Value);
+
+ var networkTransportTag = tags.FirstOrDefault(t => t.Key == "network.transport");
+ Assert.NotNull(networkTransportTag.Key);
+ Assert.Equal("tcp", networkTransportTag.Value);
+
+ client.Dispose();
+ await connectionHandlerTask;
+ }
+
+ var disconnectActivity = await serverChannel.Reader.ReadAsync().DefaultTimeout();
+
+ // Verify disconnect activity also has endpoint tags
+ var disconnectTags = disconnectActivity.Tags.ToArray();
+ var disconnectServerAddressTag = disconnectTags.FirstOrDefault(t => t.Key == "server.address");
+ Assert.NotNull(disconnectServerAddressTag.Key);
+ Assert.Equal("127.0.0.1", disconnectServerAddressTag.Value);
+ }
+ }
}