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); + } + } }