Skip to content

Commit 5ab200c

Browse files
authored
HTTP Metrics: Uncoditionally report server.port (#104741)
Since `server.port` is required, we should not omit it for default ports in `http.client.request.duration`. For consistency, this PR also makes `http.client.connection.duration` and `http.client.open_connections` REQUIRED, meaning port will be uncoditionally present in those metrics too. Resolves #94829.
1 parent 74af205 commit 5ab200c

File tree

6 files changed

+64
-30
lines changed

6 files changed

+64
-30
lines changed

src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ await _innerHandler.SendAsync(request, cancellationToken).ConfigureAwait(false)
200200
// Add standard tags known at request completion.
201201
if (response is not null)
202202
{
203-
activity.SetTag("http.response.status_code", DiagnosticsHelper.GetBoxedStatusCode((int)response.StatusCode));
203+
activity.SetTag("http.response.status_code", DiagnosticsHelper.GetBoxedInt32((int)response.StatusCode));
204204
activity.SetTag("network.protocol.version", DiagnosticsHelper.GetProtocolVersionString(response.Version));
205205
}
206206

@@ -219,7 +219,7 @@ await _innerHandler.SendAsync(request, cancellationToken).ConfigureAwait(false)
219219
// Add standard tags known at request completion.
220220
if (response is not null)
221221
{
222-
activity.SetTag("http.response.status_code", DiagnosticsHelper.GetBoxedStatusCode((int)response.StatusCode));
222+
activity.SetTag("http.response.status_code", DiagnosticsHelper.GetBoxedInt32((int)response.StatusCode));
223223
activity.SetTag("network.protocol.version", DiagnosticsHelper.GetProtocolVersionString(response.Version));
224224
}
225225

src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHelper.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,15 @@ public static bool TryGetErrorType(HttpResponseMessage? response, Exception? exc
106106
private static string[]? s_statusCodeStrings;
107107

108108
#pragma warning disable CA1859 // we explictly box here
109-
public static object GetBoxedStatusCode(int statusCode)
109+
// Returns a pooled object if 'value' is between 0-512,
110+
// saving allocations for standard HTTP status codes and small port tag values.
111+
public static object GetBoxedInt32(int value)
110112
{
111113
object[] boxes = LazyInitializer.EnsureInitialized(ref s_boxedStatusCodes, static () => new object[512]);
112114

113-
return (uint)statusCode < (uint)boxes.Length
114-
? boxes[statusCode] ??= statusCode
115-
: statusCode;
115+
return (uint)value < (uint)boxes.Length
116+
? boxes[value] ??= value
117+
: value;
116118
}
117119
#pragma warning restore
118120

src/libraries/System.Net.Http/src/System/Net/Http/Metrics/MetricsHandler.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ private void RequestStop(HttpRequestMessage request, HttpResponseMessage? respon
110110

111111
if (response is not null)
112112
{
113-
tags.Add("http.response.status_code", DiagnosticsHelper.GetBoxedStatusCode((int)response.StatusCode));
113+
tags.Add("http.response.status_code", DiagnosticsHelper.GetBoxedInt32((int)response.StatusCode));
114114
tags.Add("network.protocol.version", DiagnosticsHelper.GetProtocolVersionString(response.Version));
115115
}
116116

@@ -140,11 +140,7 @@ private static TagList InitializeCommonTags(HttpRequestMessage request)
140140
{
141141
tags.Add("url.scheme", requestUri.Scheme);
142142
tags.Add("server.address", requestUri.Host);
143-
// Add port tag when not the default value for the current scheme
144-
if (!requestUri.IsDefaultPort)
145-
{
146-
tags.Add("server.port", requestUri.Port);
147-
}
143+
tags.Add("server.port", DiagnosticsHelper.GetBoxedInt32(requestUri.Port));
148144
}
149145
tags.Add(DiagnosticsHelper.GetMethodTag(request.Method, out _));
150146

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ protected void MarkConnectionAsEstablished(Activity? connectionSetupActivity, IP
7272
protocol,
7373
_pool.IsSecure ? "https" : "http",
7474
_pool.OriginAuthority.HostValue,
75-
_pool.IsDefaultPort ? null : _pool.OriginAuthority.Port,
75+
_pool.OriginAuthority.Port,
7676
remoteEndPoint?.Address?.ToString());
7777

7878
_connectionMetrics.ConnectionEstablished();

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/ConnectionMetrics.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ internal sealed class ConnectionMetrics
1313
private readonly object _protocolVersionTag;
1414
private readonly object _schemeTag;
1515
private readonly object _hostTag;
16-
private readonly object? _portTag;
16+
private readonly object _portTag;
1717
private readonly object? _peerAddressTag;
1818
private bool _currentlyIdle;
1919

20-
public ConnectionMetrics(SocketsHttpHandlerMetrics metrics, string protocolVersion, string scheme, string host, int? port, string? peerAddress)
20+
public ConnectionMetrics(SocketsHttpHandlerMetrics metrics, string protocolVersion, string scheme, string host, int port, string? peerAddress)
2121
{
2222
_metrics = metrics;
2323
_openConnectionsEnabled = _metrics.OpenConnections.Enabled;
2424
_protocolVersionTag = protocolVersion;
2525
_schemeTag = scheme;
2626
_hostTag = host;
27-
_portTag = port;
27+
_portTag = DiagnosticsHelper.GetBoxedInt32(port);
2828
_peerAddressTag = peerAddress;
2929
}
3030

@@ -36,11 +36,7 @@ private TagList GetTags()
3636
tags.Add("network.protocol.version", _protocolVersionTag);
3737
tags.Add("url.scheme", _schemeTag);
3838
tags.Add("server.address", _hostTag);
39-
40-
if (_portTag is not null)
41-
{
42-
tags.Add("server.port", _portTag);
43-
}
39+
tags.Add("server.port", _portTag);
4440

4541
if (_peerAddressTag is not null)
4642
{

src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using System.Threading;
1515
using System.Threading.Tasks;
1616
using Microsoft.DotNet.RemoteExecutor;
17+
using Microsoft.DotNet.XUnitExtensions;
1718
using Xunit;
1819
using Xunit.Abstractions;
1920

@@ -71,13 +72,12 @@ protected HttpMetricsTestBase(ITestOutputHelper output) : base(output)
7172
}
7273

7374

74-
private static void VerifyPeerAddress(KeyValuePair<string, object?>[] tags)
75+
private static void VerifyPeerAddress(KeyValuePair<string, object?>[] tags, IPAddress[] validPeerAddresses = null)
7576
{
7677
string ipString = (string)tags.Single(t => t.Key == "network.peer.address").Value;
78+
validPeerAddresses ??= [IPAddress.Loopback.MapToIPv6(), IPAddress.Loopback, IPAddress.IPv6Loopback];
7779
IPAddress ip = IPAddress.Parse(ipString);
78-
Assert.True(ip.Equals(IPAddress.Loopback.MapToIPv6()) ||
79-
ip.Equals(IPAddress.Loopback) ||
80-
ip.Equals(IPAddress.IPv6Loopback));
80+
Assert.Contains(ip, validPeerAddresses);
8181
}
8282

8383

@@ -126,24 +126,29 @@ protected static void VerifyActiveRequests(string instrumentName, long measureme
126126
Assert.Equal(method, tags.Single(t => t.Key == "http.request.method").Value);
127127
}
128128

129-
protected static void VerifyOpenConnections(string actualName, object measurement, KeyValuePair<string, object?>[] tags, long expectedValue, Uri uri, Version? protocolVersion, string state)
129+
protected static void VerifyOpenConnections(string actualName, object measurement, KeyValuePair<string, object?>[] tags, long expectedValue, Uri uri, Version? protocolVersion, string state, IPAddress[] validPeerAddresses = null)
130130
{
131131
Assert.Equal(InstrumentNames.OpenConnections, actualName);
132132
Assert.Equal(expectedValue, Assert.IsType<long>(measurement));
133133
VerifySchemeHostPortTags(tags, uri);
134134
VerifyTag(tags, "network.protocol.version", GetVersionString(protocolVersion));
135135
VerifyTag(tags, "http.connection.state", state);
136-
VerifyPeerAddress(tags);
136+
VerifyPeerAddress(tags, validPeerAddresses);
137137
}
138138

139-
protected static void VerifyConnectionDuration(string instrumentName, object measurement, KeyValuePair<string, object?>[] tags, Uri uri, Version? protocolVersion)
139+
protected static void VerifyConnectionDuration(string instrumentName, object measurement, KeyValuePair<string, object?>[] tags, Uri uri, Version? protocolVersion, IPAddress[] validPeerAddresses = null)
140140
{
141141
Assert.Equal(InstrumentNames.ConnectionDuration, instrumentName);
142142
double value = Assert.IsType<double>(measurement);
143-
Assert.InRange(value, double.Epsilon, 60);
143+
144+
// This flakes for remote requests on CI.
145+
if (validPeerAddresses is null)
146+
{
147+
Assert.InRange(value, double.Epsilon, 60);
148+
}
144149
VerifySchemeHostPortTags(tags, uri);
145150
VerifyTag(tags, "network.protocol.version", GetVersionString(protocolVersion));
146-
VerifyPeerAddress(tags);
151+
VerifyPeerAddress(tags, validPeerAddresses);
147152
}
148153

149154
protected static void VerifyTimeInQueue(string instrumentName, object measurement, KeyValuePair<string, object?>[] tags, Uri uri, Version? protocolVersion, string method = "GET")
@@ -362,6 +367,41 @@ public Task RequestDuration_Success_Recorded(string method, HttpStatusCode statu
362367
});
363368
}
364369

370+
[OuterLoop("Uses external server.")]
371+
[ConditionalFact]
372+
public async Task ExternalServer_DurationMetrics_Recorded()
373+
{
374+
if (UseVersion == HttpVersion.Version30)
375+
{
376+
throw new SkipTestException("No remote HTTP/3 server available for testing.");
377+
}
378+
379+
using InstrumentRecorder<double> requestDurationRecorder = SetupInstrumentRecorder<double>(InstrumentNames.RequestDuration);
380+
using InstrumentRecorder<double> connectionDurationRecorder = SetupInstrumentRecorder<double>(InstrumentNames.ConnectionDuration);
381+
using InstrumentRecorder<long> openConnectionsRecorder = SetupInstrumentRecorder<long>(InstrumentNames.OpenConnections);
382+
383+
Uri uri = UseVersion == HttpVersion.Version11
384+
? Test.Common.Configuration.Http.RemoteHttp11Server.EchoUri
385+
: Test.Common.Configuration.Http.RemoteHttp2Server.EchoUri;
386+
IPAddress[] addresses = await Dns.GetHostAddressesAsync(uri.Host);
387+
addresses = addresses.Union(addresses.Select(a => a.MapToIPv6())).ToArray();
388+
389+
using (HttpMessageInvoker client = CreateHttpMessageInvoker())
390+
{
391+
using HttpRequestMessage request = new(HttpMethod.Get, uri) { Version = UseVersion };
392+
request.Headers.ConnectionClose = true;
393+
using HttpResponseMessage response = await SendAsync(client, request);
394+
await response.Content.LoadIntoBufferAsync();
395+
await WaitForEnvironmentTicksToAdvance();
396+
}
397+
398+
VerifyRequestDuration(Assert.Single(requestDurationRecorder.GetMeasurements()), uri, UseVersion, 200, "GET");
399+
Measurement<double> cd = Assert.Single(connectionDurationRecorder.GetMeasurements());
400+
VerifyConnectionDuration(InstrumentNames.ConnectionDuration, cd.Value, cd.Tags.ToArray(), uri, UseVersion, addresses);
401+
Measurement<long> oc = openConnectionsRecorder.GetMeasurements().First();
402+
VerifyOpenConnections(InstrumentNames.OpenConnections, oc.Value, oc.Tags.ToArray(), 1, uri, UseVersion, "idle", addresses);
403+
}
404+
365405
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
366406
public async Task RequestDuration_HttpTracingEnabled_RecordedWhileRequestActivityRunning()
367407
{

0 commit comments

Comments
 (0)