Skip to content

Commit 24a2837

Browse files
authored
Add TLS and HTTP protocol to kestrel-connection-duration counter (#48723)
1 parent a61229a commit 24a2837

File tree

8 files changed

+150
-8
lines changed

8 files changed

+150
-8
lines changed

src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
1313
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
1414
using Microsoft.Extensions.Logging;
15+
using HttpProtocol = Microsoft.AspNetCore.Http.HttpProtocol;
1516

1617
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
1718

@@ -59,17 +60,20 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> http
5960
// _http1Connection must be initialized before adding the connection to the connection manager
6061
requestProcessor = _http1Connection = new Http1Connection<TContext>((HttpConnectionContext)_context);
6162
_protocolSelectionState = ProtocolSelectionState.Selected;
63+
AddMetricsHttpProtocolTag(HttpProtocol.Http11);
6264
break;
6365
case HttpProtocols.Http2:
6466
// _http2Connection must be initialized before yielding control to the transport thread,
6567
// to prevent a race condition where _http2Connection.Abort() is called just as
6668
// _http2Connection is about to be initialized.
6769
requestProcessor = new Http2Connection((HttpConnectionContext)_context);
6870
_protocolSelectionState = ProtocolSelectionState.Selected;
71+
AddMetricsHttpProtocolTag(HttpProtocol.Http2);
6972
break;
7073
case HttpProtocols.Http3:
7174
requestProcessor = new Http3Connection((HttpMultiplexedConnectionContext)_context);
7275
_protocolSelectionState = ProtocolSelectionState.Selected;
76+
AddMetricsHttpProtocolTag(HttpProtocol.Http3);
7377
break;
7478
case HttpProtocols.None:
7579
// An error was already logged in SelectProtocol(), but we should close the connection.
@@ -112,6 +116,14 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> http
112116
}
113117
}
114118

119+
private void AddMetricsHttpProtocolTag(string httpProtocol)
120+
{
121+
if (_context.ConnectionContext.Features.Get<IConnectionMetricsTagsFeature>() is { } metricsTags)
122+
{
123+
metricsTags.Tags.Add(new KeyValuePair<string, object?>("http-protocol", httpProtocol));
124+
}
125+
}
126+
115127
// For testing only
116128
internal void Initialize(IRequestProcessor requestProcessor)
117129
{

src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnectionOfT.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ internal async Task ExecuteAsync()
100100

101101
private sealed class ConnectionMetricsTagsFeature : IConnectionMetricsTagsFeature
102102
{
103-
public ICollection<KeyValuePair<string, object?>> Tags => TagsList;
103+
ICollection<KeyValuePair<string, object?>> IConnectionMetricsTagsFeature.Tags => TagsList;
104104

105105
public List<KeyValuePair<string, object?>> TagsList { get; } = new List<KeyValuePair<string, object?>>();
106106
}

src/Servers/Kestrel/Core/src/Middleware/HttpMultiplexedConnectionMiddleware.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Net;
5+
using System.Security.Authentication;
56
using Microsoft.AspNetCore.Connections;
67
using Microsoft.AspNetCore.Connections.Features;
78
using Microsoft.AspNetCore.Hosting.Server;
@@ -41,6 +42,12 @@ public Task OnConnectionAsync(MultiplexedConnectionContext connectionContext)
4142
localEndPoint,
4243
connectionContext.RemoteEndPoint as IPEndPoint);
4344

45+
if (connectionContext.Features.Get<IConnectionMetricsTagsFeature>() is { } metricsTags)
46+
{
47+
// HTTP/3 is always TLS 1.3. If multiple versions are support in the future then this value will need to be detected.
48+
metricsTags.Tags.Add(new KeyValuePair<string, object?>("tls-protocol", SslProtocols.Tls13.ToString()));
49+
}
50+
4451
var connection = new HttpConnection(httpConnectionContext);
4552

4653
return connection.ProcessRequestsAsync(_application);

src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ public async Task OnConnectionAsync(ConnectionContext context)
202202

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

205+
if (context.Features.Get<IConnectionMetricsTagsFeature>() is { } metricsTags)
206+
{
207+
metricsTags.Tags.Add(new KeyValuePair<string, object?>("tls-protocol", sslStream.SslProtocol.ToString()));
208+
}
209+
205210
var originalTransport = context.Transport;
206211

207212
try

src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ await connection.ReceiveEnd(
8181

8282
Assert.Collection(connectionDuration.GetMeasurements(), m =>
8383
{
84-
AssertDuration(m, "127.0.0.1:0");
84+
AssertDuration(m, "127.0.0.1:0", "HTTP/1.1");
8585
Assert.Equal("value!", (string)m.Tags.ToArray().Single(t => t.Key == "custom").Value);
8686
});
8787
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(
213213

214214
Assert.Collection(connectionDuration.GetMeasurements(), m =>
215215
{
216-
AssertDuration(m, "127.0.0.1:0");
216+
AssertDuration(m, "127.0.0.1:0", "HTTP/1.1");
217217
Assert.Equal("value!", (string)m.Tags.ToArray().Single(t => t.Key == "custom").Value);
218218
Assert.Empty(m.Tags.ToArray().Where(t => t.Key == "test"));
219219
});
@@ -274,7 +274,7 @@ public async Task Http1Connection_Error()
274274

275275
Assert.Collection(connectionDuration.GetMeasurements(), m =>
276276
{
277-
AssertDuration(m, "127.0.0.1:0");
277+
AssertDuration(m, "127.0.0.1:0", httpProtocol: null);
278278
Assert.Equal("System.InvalidOperationException", (string)m.Tags.ToArray().Single(t => t.Key == "exception-name").Value);
279279
});
280280
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",
305305
"");
306306
}
307307

308-
Assert.Collection(connectionDuration.GetMeasurements(), m => AssertDuration(m, "127.0.0.1:0"));
308+
Assert.Collection(connectionDuration.GetMeasurements(), m => AssertDuration(m, "127.0.0.1:0", "HTTP/1.1"));
309309
Assert.Collection(currentConnections.GetMeasurements(), m => AssertCount(m, 1, "127.0.0.1:0"), m => AssertCount(m, -1, "127.0.0.1:0"));
310310
Assert.Collection(currentUpgradedRequests.GetMeasurements(), m => Assert.Equal(1, m.Value), m => Assert.Equal(-1, m.Value));
311311

@@ -393,7 +393,7 @@ public async Task Http2Connection()
393393
Assert.NotNull(connectionId);
394394
Assert.Equal(2, requestsReceived);
395395

396-
Assert.Collection(connectionDuration.GetMeasurements(), m => AssertDuration(m, "127.0.0.1:0"));
396+
Assert.Collection(connectionDuration.GetMeasurements(), m => AssertDuration(m, "127.0.0.1:0", "HTTP/2"));
397397
Assert.Collection(currentConnections.GetMeasurements(), m => AssertCount(m, 1, "127.0.0.1:0"), m => AssertCount(m, -1, "127.0.0.1:0"));
398398
Assert.Collection(queuedConnections.GetMeasurements(), m => AssertCount(m, 1, "127.0.0.1:0"), m => AssertCount(m, -1, "127.0.0.1:0"));
399399

@@ -430,10 +430,18 @@ private static async Task EchoApp(HttpContext httpContext)
430430
}
431431
}
432432

433-
private static void AssertDuration(Measurement<double> measurement, string localEndpoint)
433+
private static void AssertDuration(Measurement<double> measurement, string localEndpoint, string httpProtocol)
434434
{
435435
Assert.True(measurement.Value > 0);
436436
Assert.Equal(localEndpoint, (string)measurement.Tags.ToArray().Single(t => t.Key == "endpoint").Value);
437+
if (httpProtocol is not null)
438+
{
439+
Assert.Equal(httpProtocol, (string)measurement.Tags.ToArray().Single(t => t.Key == "http-protocol").Value);
440+
}
441+
else
442+
{
443+
Assert.DoesNotContain(measurement.Tags.ToArray(), t => t.Key == "http-protocol");
444+
}
437445
}
438446

439447
private static void AssertCount(Measurement<long> measurement, long expectedValue, string localEndpoint)

src/Servers/Kestrel/test/Interop.FunctionalTests/Http2/Http2RequestTests.cs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.Metrics;
45
using System.Net;
56
using System.Net.Http;
67
using System.Net.Http.Headers;
8+
using System.Security.Authentication;
79
using Microsoft.AspNetCore.Hosting;
810
using Microsoft.AspNetCore.Http;
9-
using Microsoft.AspNetCore.Http.Headers;
1011
using Microsoft.AspNetCore.Internal;
1112
using Microsoft.AspNetCore.Server.Kestrel.Core;
13+
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
1214
using Microsoft.AspNetCore.Testing;
1315
using Microsoft.Extensions.DependencyInjection;
16+
using Microsoft.Extensions.Diagnostics.Metrics;
1417
using Microsoft.Extensions.Hosting;
1518
using Microsoft.Extensions.Logging;
1619

@@ -19,6 +22,61 @@ namespace Interop.FunctionalTests.Http2;
1922
[Collection(nameof(NoParallelCollection))]
2023
public class Http2RequestTests : LoggedTest
2124
{
25+
[Fact]
26+
public async Task GET_Metrics_HttpProtocolAndTlsSet()
27+
{
28+
// Arrange
29+
var protocolTcs = new TaskCompletionSource<SslProtocols>(TaskCreationOptions.RunContinuationsAsynchronously);
30+
var builder = CreateHostBuilder(c =>
31+
{
32+
protocolTcs.SetResult(c.Features.Get<ISslStreamFeature>().SslStream.SslProtocol);
33+
return Task.CompletedTask;
34+
}, protocol: HttpProtocols.Http2, plaintext: false);
35+
36+
using (var host = builder.Build())
37+
{
38+
var meterFactory = host.Services.GetRequiredService<IMeterFactory>();
39+
40+
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
41+
using var connectionDuration = new InstrumentRecorder<double>(meterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration");
42+
using var measurementReporter = new MeasurementReporter<double>(meterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration");
43+
measurementReporter.Register(m =>
44+
{
45+
tcs.SetResult();
46+
});
47+
48+
await host.StartAsync();
49+
var client = HttpHelpers.CreateClient();
50+
51+
// Act
52+
var request1 = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/");
53+
request1.Version = HttpVersion.Version20;
54+
request1.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
55+
56+
var response1 = await client.SendAsync(request1, CancellationToken.None);
57+
response1.EnsureSuccessStatusCode();
58+
59+
var protocol = await protocolTcs.Task.DefaultTimeout();
60+
61+
// Dispose the client to end the connection.
62+
client.Dispose();
63+
// Wait for measurement to be available.
64+
await tcs.Task.DefaultTimeout();
65+
66+
// Assert
67+
Assert.Collection(connectionDuration.GetMeasurements(),
68+
m =>
69+
{
70+
Assert.True(m.Value > 0);
71+
Assert.Equal(protocol.ToString(), m.Tags.ToArray().Single(t => t.Key == "tls-protocol").Value);
72+
Assert.Equal("HTTP/2", m.Tags.ToArray().Single(t => t.Key == "http-protocol").Value);
73+
Assert.Equal($"127.0.0.1:{host.GetPort()}", m.Tags.ToArray().Single(t => t.Key == "endpoint").Value);
74+
});
75+
76+
await host.StopAsync();
77+
}
78+
}
79+
2280
[Fact]
2381
public async Task GET_NoTLS_Http11RequestToHttp2Endpoint_400Result()
2482
{

src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5+
using System.Diagnostics.Metrics;
56
using System.Globalization;
67
using System.Net;
78
using System.Net.Http;
@@ -18,6 +19,7 @@
1819
using Microsoft.AspNetCore.Server.Kestrel.Https;
1920
using Microsoft.AspNetCore.Testing;
2021
using Microsoft.Extensions.DependencyInjection;
22+
using Microsoft.Extensions.Diagnostics.Metrics;
2123
using Microsoft.Extensions.Hosting;
2224
using Microsoft.Extensions.Logging;
2325
using Microsoft.Extensions.Logging.Testing;
@@ -68,6 +70,55 @@ public void CompleteStream()
6870

6971
private static readonly byte[] TestData = Encoding.UTF8.GetBytes("Hello world");
7072

73+
[ConditionalFact]
74+
[MsQuicSupported]
75+
public async Task GET_Metrics_HttpProtocolAndTlsSet()
76+
{
77+
// Arrange
78+
var builder = CreateHostBuilder(context => Task.CompletedTask);
79+
80+
using (var host = builder.Build())
81+
{
82+
var meterFactory = host.Services.GetRequiredService<IMeterFactory>();
83+
84+
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
85+
using var connectionDuration = new InstrumentRecorder<double>(meterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration");
86+
using var measurementReporter = new MeasurementReporter<double>(meterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration");
87+
measurementReporter.Register(m =>
88+
{
89+
tcs.SetResult();
90+
});
91+
92+
await host.StartAsync();
93+
var client = HttpHelpers.CreateClient();
94+
95+
// Act
96+
var request1 = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/");
97+
request1.Version = HttpVersion.Version30;
98+
request1.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
99+
100+
var response1 = await client.SendAsync(request1, CancellationToken.None);
101+
response1.EnsureSuccessStatusCode();
102+
103+
// Dispose the client to end the connection.
104+
client.Dispose();
105+
// Wait for measurement to be available.
106+
await tcs.Task.DefaultTimeout();
107+
108+
// Assert
109+
Assert.Collection(connectionDuration.GetMeasurements(),
110+
m =>
111+
{
112+
Assert.True(m.Value > 0);
113+
Assert.Equal("Tls13", m.Tags.ToArray().Single(t => t.Key == "tls-protocol").Value);
114+
Assert.Equal("HTTP/3", m.Tags.ToArray().Single(t => t.Key == "http-protocol").Value);
115+
Assert.Equal($"127.0.0.1:{host.GetPort()}", m.Tags.ToArray().Single(t => t.Key == "endpoint").Value);
116+
});
117+
118+
await host.StopAsync();
119+
}
120+
}
121+
71122
// Verify HTTP/2 and HTTP/3 match behavior
72123
[ConditionalTheory]
73124
[MsQuicSupported]

src/Servers/Kestrel/test/Interop.FunctionalTests/Interop.FunctionalTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<Compile Include="$(SharedSourceRoot)HttpClient\HttpEventSourceListener.cs" LinkBase="shared" />
2323
<Content Include="$(SharedSourceRoot)TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
2424
<Compile Include="$(SharedSourceRoot)TransportTestHelpers\MsQuicSupportedAttribute.cs" LinkBase="shared\TransportTestHelpers\MsQuicSupportedAttribute.cs" />
25+
<Compile Include="$(SharedSourceRoot)Metrics\TestMeterFactory.cs" LinkBase="shared" />
2526
<Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\TlsAlpnSupportedAttribute.cs" Link="shared\TransportTestHelpers\TlsAlpnSupportedAttribute.cs" />
2627
<Compile Include="$(KestrelSharedSourceRoot)test\ServerRetryHelper.cs" LinkBase="shared" />
2728
</ItemGroup>

0 commit comments

Comments
 (0)