diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs index 889b311f30c0..bbcd6ce0c532 100644 --- a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; +using HttpProtocol = Microsoft.AspNetCore.Http.HttpProtocol; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; @@ -59,6 +60,7 @@ public async Task ProcessRequestsAsync(IHttpApplication http // _http1Connection must be initialized before adding the connection to the connection manager requestProcessor = _http1Connection = new Http1Connection((HttpConnectionContext)_context); _protocolSelectionState = ProtocolSelectionState.Selected; + AddMetricsHttpProtocolTag(HttpProtocol.Http11); break; case HttpProtocols.Http2: // _http2Connection must be initialized before yielding control to the transport thread, @@ -66,10 +68,12 @@ public async Task ProcessRequestsAsync(IHttpApplication http // _http2Connection is about to be initialized. requestProcessor = new Http2Connection((HttpConnectionContext)_context); _protocolSelectionState = ProtocolSelectionState.Selected; + AddMetricsHttpProtocolTag(HttpProtocol.Http2); break; case HttpProtocols.Http3: requestProcessor = new Http3Connection((HttpMultiplexedConnectionContext)_context); _protocolSelectionState = ProtocolSelectionState.Selected; + AddMetricsHttpProtocolTag(HttpProtocol.Http3); break; case HttpProtocols.None: // An error was already logged in SelectProtocol(), but we should close the connection. @@ -112,6 +116,14 @@ public async Task ProcessRequestsAsync(IHttpApplication http } } + private void AddMetricsHttpProtocolTag(string httpProtocol) + { + if (_context.ConnectionContext.Features.Get() is { } metricsTags) + { + metricsTags.Tags.Add(new KeyValuePair("http-protocol", httpProtocol)); + } + } + // For testing only internal void Initialize(IRequestProcessor requestProcessor) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnectionOfT.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnectionOfT.cs index 700c879bae33..900219d4c7de 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnectionOfT.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnectionOfT.cs @@ -100,7 +100,7 @@ internal async Task ExecuteAsync() private sealed class ConnectionMetricsTagsFeature : IConnectionMetricsTagsFeature { - public ICollection> Tags => TagsList; + ICollection> IConnectionMetricsTagsFeature.Tags => TagsList; public List> TagsList { get; } = new List>(); } diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpMultiplexedConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpMultiplexedConnectionMiddleware.cs index 8fcbd2fe2f55..f310dc3a2edf 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpMultiplexedConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpMultiplexedConnectionMiddleware.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net; +using System.Security.Authentication; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting.Server; @@ -41,6 +42,12 @@ public Task OnConnectionAsync(MultiplexedConnectionContext connectionContext) localEndPoint, connectionContext.RemoteEndPoint as IPEndPoint); + if (connectionContext.Features.Get() is { } metricsTags) + { + // HTTP/3 is always TLS 1.3. If multiple versions are support in the future then this value will need to be detected. + metricsTags.Tags.Add(new KeyValuePair("tls-protocol", SslProtocols.Tls13.ToString())); + } + var connection = new HttpConnection(httpConnectionContext); return connection.ProcessRequestsAsync(_application); diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs index 25a23ee3f491..51242c844763 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs @@ -202,6 +202,11 @@ public async Task OnConnectionAsync(ConnectionContext context) _logger.HttpsConnectionEstablished(context.ConnectionId, sslStream.SslProtocol); + if (context.Features.Get() is { } metricsTags) + { + metricsTags.Tags.Add(new KeyValuePair("tls-protocol", sslStream.SslProtocol.ToString())); + } + var originalTransport = context.Transport; try diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs index 86457feab09b..394341aa9c4e 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs @@ -81,7 +81,7 @@ await connection.ReceiveEnd( Assert.Collection(connectionDuration.GetMeasurements(), m => { - AssertDuration(m, "127.0.0.1:0"); + AssertDuration(m, "127.0.0.1:0", "HTTP/1.1"); Assert.Equal("value!", (string)m.Tags.ToArray().Single(t => t.Key == "custom").Value); }); Assert.Collection(currentConnections.GetMeasurements(), m => AssertCount(m, 1, "127.0.0.1:0"), m => AssertCount(m, -1, "127.0.0.1:0")); @@ -213,7 +213,7 @@ await connection.ReceiveEnd( Assert.Collection(connectionDuration.GetMeasurements(), m => { - AssertDuration(m, "127.0.0.1:0"); + AssertDuration(m, "127.0.0.1:0", "HTTP/1.1"); Assert.Equal("value!", (string)m.Tags.ToArray().Single(t => t.Key == "custom").Value); Assert.Empty(m.Tags.ToArray().Where(t => t.Key == "test")); }); @@ -274,7 +274,7 @@ public async Task Http1Connection_Error() Assert.Collection(connectionDuration.GetMeasurements(), m => { - AssertDuration(m, "127.0.0.1:0"); + AssertDuration(m, "127.0.0.1:0", httpProtocol: null); Assert.Equal("System.InvalidOperationException", (string)m.Tags.ToArray().Single(t => t.Key == "exception-name").Value); }); Assert.Collection(currentConnections.GetMeasurements(), m => AssertCount(m, 1, "127.0.0.1:0"), m => AssertCount(m, -1, "127.0.0.1:0")); @@ -305,7 +305,7 @@ await connection.ReceiveEnd("HTTP/1.1 101 Switching Protocols", ""); } - Assert.Collection(connectionDuration.GetMeasurements(), m => AssertDuration(m, "127.0.0.1:0")); + Assert.Collection(connectionDuration.GetMeasurements(), m => AssertDuration(m, "127.0.0.1:0", "HTTP/1.1")); Assert.Collection(currentConnections.GetMeasurements(), m => AssertCount(m, 1, "127.0.0.1:0"), m => AssertCount(m, -1, "127.0.0.1:0")); Assert.Collection(currentUpgradedRequests.GetMeasurements(), m => Assert.Equal(1, m.Value), m => Assert.Equal(-1, m.Value)); @@ -393,7 +393,7 @@ public async Task Http2Connection() Assert.NotNull(connectionId); Assert.Equal(2, requestsReceived); - Assert.Collection(connectionDuration.GetMeasurements(), m => AssertDuration(m, "127.0.0.1:0")); + Assert.Collection(connectionDuration.GetMeasurements(), m => AssertDuration(m, "127.0.0.1:0", "HTTP/2")); Assert.Collection(currentConnections.GetMeasurements(), m => AssertCount(m, 1, "127.0.0.1:0"), m => AssertCount(m, -1, "127.0.0.1:0")); Assert.Collection(queuedConnections.GetMeasurements(), m => AssertCount(m, 1, "127.0.0.1:0"), m => AssertCount(m, -1, "127.0.0.1:0")); @@ -430,10 +430,18 @@ private static async Task EchoApp(HttpContext httpContext) } } - private static void AssertDuration(Measurement measurement, string localEndpoint) + private static void AssertDuration(Measurement measurement, string localEndpoint, string httpProtocol) { Assert.True(measurement.Value > 0); Assert.Equal(localEndpoint, (string)measurement.Tags.ToArray().Single(t => t.Key == "endpoint").Value); + if (httpProtocol is not null) + { + Assert.Equal(httpProtocol, (string)measurement.Tags.ToArray().Single(t => t.Key == "http-protocol").Value); + } + else + { + Assert.DoesNotContain(measurement.Tags.ToArray(), t => t.Key == "http-protocol"); + } } private static void AssertCount(Measurement measurement, long expectedValue, string localEndpoint) diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http2/Http2RequestTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http2/Http2RequestTests.cs index 0edf0ee3ea5d..a07108d512a7 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http2/Http2RequestTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http2/Http2RequestTests.cs @@ -1,16 +1,19 @@ // 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.Metrics; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Security.Authentication; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -19,6 +22,61 @@ namespace Interop.FunctionalTests.Http2; [Collection(nameof(NoParallelCollection))] public class Http2RequestTests : LoggedTest { + [Fact] + public async Task GET_Metrics_HttpProtocolAndTlsSet() + { + // Arrange + var protocolTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var builder = CreateHostBuilder(c => + { + protocolTcs.SetResult(c.Features.Get().SslStream.SslProtocol); + return Task.CompletedTask; + }, protocol: HttpProtocols.Http2, plaintext: false); + + using (var host = builder.Build()) + { + var meterFactory = host.Services.GetRequiredService(); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var connectionDuration = new InstrumentRecorder(meterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); + using var measurementReporter = new MeasurementReporter(meterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); + measurementReporter.Register(m => + { + tcs.SetResult(); + }); + + await host.StartAsync(); + var client = HttpHelpers.CreateClient(); + + // Act + var request1 = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/"); + request1.Version = HttpVersion.Version20; + request1.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + + var response1 = await client.SendAsync(request1, CancellationToken.None); + response1.EnsureSuccessStatusCode(); + + var protocol = await protocolTcs.Task.DefaultTimeout(); + + // Dispose the client to end the connection. + client.Dispose(); + // Wait for measurement to be available. + await tcs.Task.DefaultTimeout(); + + // Assert + Assert.Collection(connectionDuration.GetMeasurements(), + m => + { + Assert.True(m.Value > 0); + Assert.Equal(protocol.ToString(), m.Tags.ToArray().Single(t => t.Key == "tls-protocol").Value); + Assert.Equal("HTTP/2", m.Tags.ToArray().Single(t => t.Key == "http-protocol").Value); + Assert.Equal($"127.0.0.1:{host.GetPort()}", m.Tags.ToArray().Single(t => t.Key == "endpoint").Value); + }); + + await host.StopAsync(); + } + } + [Fact] public async Task GET_NoTLS_Http11RequestToHttp2Endpoint_400Result() { diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs index 40dcaf8e43f8..e298fd69135c 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Globalization; using System.Net; using System.Net.Http; @@ -18,6 +19,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; @@ -68,6 +70,55 @@ public void CompleteStream() private static readonly byte[] TestData = Encoding.UTF8.GetBytes("Hello world"); + [ConditionalFact] + [MsQuicSupported] + public async Task GET_Metrics_HttpProtocolAndTlsSet() + { + // Arrange + var builder = CreateHostBuilder(context => Task.CompletedTask); + + using (var host = builder.Build()) + { + var meterFactory = host.Services.GetRequiredService(); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var connectionDuration = new InstrumentRecorder(meterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); + using var measurementReporter = new MeasurementReporter(meterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); + measurementReporter.Register(m => + { + tcs.SetResult(); + }); + + await host.StartAsync(); + var client = HttpHelpers.CreateClient(); + + // Act + var request1 = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/"); + request1.Version = HttpVersion.Version30; + request1.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + + var response1 = await client.SendAsync(request1, CancellationToken.None); + response1.EnsureSuccessStatusCode(); + + // Dispose the client to end the connection. + client.Dispose(); + // Wait for measurement to be available. + await tcs.Task.DefaultTimeout(); + + // Assert + Assert.Collection(connectionDuration.GetMeasurements(), + m => + { + Assert.True(m.Value > 0); + Assert.Equal("Tls13", m.Tags.ToArray().Single(t => t.Key == "tls-protocol").Value); + Assert.Equal("HTTP/3", m.Tags.ToArray().Single(t => t.Key == "http-protocol").Value); + Assert.Equal($"127.0.0.1:{host.GetPort()}", m.Tags.ToArray().Single(t => t.Key == "endpoint").Value); + }); + + await host.StopAsync(); + } + } + // Verify HTTP/2 and HTTP/3 match behavior [ConditionalTheory] [MsQuicSupported] diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Interop.FunctionalTests.csproj b/src/Servers/Kestrel/test/Interop.FunctionalTests/Interop.FunctionalTests.csproj index c1792942611a..5b092bee3d01 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Interop.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Interop.FunctionalTests.csproj @@ -22,6 +22,7 @@ +