Skip to content

Add TLS and HTTP protocol to kestrel-connection-duration counter #48723

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -59,17 +60,20 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> http
// _http1Connection must be initialized before adding the connection to the connection manager
requestProcessor = _http1Connection = new Http1Connection<TContext>((HttpConnectionContext)_context);
_protocolSelectionState = ProtocolSelectionState.Selected;
AddMetricsHttpProtocolTag(HttpProtocol.Http11);
break;
case HttpProtocols.Http2:
// _http2Connection must be initialized before yielding control to the transport thread,
// to prevent a race condition where _http2Connection.Abort() is called just as
// _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.
Expand Down Expand Up @@ -112,6 +116,14 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> http
}
}

private void AddMetricsHttpProtocolTag(string httpProtocol)
{
if (_context.ConnectionContext.Features.Get<IConnectionMetricsTagsFeature>() is { } metricsTags)
{
metricsTags.Tags.Add(new KeyValuePair<string, object?>("http-protocol", httpProtocol));
}
}

// For testing only
internal void Initialize(IRequestProcessor requestProcessor)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ internal async Task ExecuteAsync()

private sealed class ConnectionMetricsTagsFeature : IConnectionMetricsTagsFeature
{
public ICollection<KeyValuePair<string, object?>> Tags => TagsList;
ICollection<KeyValuePair<string, object?>> IConnectionMetricsTagsFeature.Tags => TagsList;

public List<KeyValuePair<string, object?>> TagsList { get; } = new List<KeyValuePair<string, object?>>();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,6 +42,12 @@ public Task OnConnectionAsync(MultiplexedConnectionContext connectionContext)
localEndPoint,
connectionContext.RemoteEndPoint as IPEndPoint);

if (connectionContext.Features.Get<IConnectionMetricsTagsFeature>() 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<string, object?>("tls-protocol", SslProtocols.Tls13.ToString()));
}

var connection = new HttpConnection(httpConnectionContext);

return connection.ProcessRequestsAsync(_application);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@ public async Task OnConnectionAsync(ConnectionContext context)

_logger.HttpsConnectionEstablished(context.ConnectionId, sslStream.SslProtocol);

if (context.Features.Get<IConnectionMetricsTagsFeature>() is { } metricsTags)
{
metricsTags.Tags.Add(new KeyValuePair<string, object?>("tls-protocol", sslStream.SslProtocol.ToString()));
}

var originalTransport = context.Transport;

try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down Expand Up @@ -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"));
});
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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));

Expand Down Expand Up @@ -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"));

Expand Down Expand Up @@ -430,10 +430,18 @@ private static async Task EchoApp(HttpContext httpContext)
}
}

private static void AssertDuration(Measurement<double> measurement, string localEndpoint)
private static void AssertDuration(Measurement<double> 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<long> measurement, long expectedValue, string localEndpoint)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<SslProtocols>(TaskCreationOptions.RunContinuationsAsynchronously);
var builder = CreateHostBuilder(c =>
{
protocolTcs.SetResult(c.Features.Get<ISslStreamFeature>().SslStream.SslProtocol);
return Task.CompletedTask;
}, protocol: HttpProtocols.Http2, plaintext: false);

using (var host = builder.Build())
{
var meterFactory = host.Services.GetRequiredService<IMeterFactory>();

var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
using var connectionDuration = new InstrumentRecorder<double>(meterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration");
using var measurementReporter = new MeasurementReporter<double>(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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<IMeterFactory>();

var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
using var connectionDuration = new InstrumentRecorder<double>(meterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration");
using var measurementReporter = new MeasurementReporter<double>(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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<Compile Include="$(SharedSourceRoot)HttpClient\HttpEventSourceListener.cs" LinkBase="shared" />
<Content Include="$(SharedSourceRoot)TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
<Compile Include="$(SharedSourceRoot)TransportTestHelpers\MsQuicSupportedAttribute.cs" LinkBase="shared\TransportTestHelpers\MsQuicSupportedAttribute.cs" />
<Compile Include="$(SharedSourceRoot)Metrics\TestMeterFactory.cs" LinkBase="shared" />
<Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\TlsAlpnSupportedAttribute.cs" Link="shared\TransportTestHelpers\TlsAlpnSupportedAttribute.cs" />
<Compile Include="$(KestrelSharedSourceRoot)test\ServerRetryHelper.cs" LinkBase="shared" />
</ItemGroup>
Expand Down