diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj
index 4fafeccae048ac..6789d578ffd9f4 100644
--- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj
+++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj
@@ -64,6 +64,7 @@
+
@@ -209,7 +210,7 @@
-
+
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs
index 72f1f88e1e1c3d..aa704ae4f74e12 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs
@@ -21,14 +21,16 @@ internal sealed class DiagnosticsHandler : HttpMessageHandlerStage
private readonly HttpMessageHandler _innerHandler;
private readonly DistributedContextPropagator _propagator;
private readonly HeaderDescriptor[]? _propagatorFields;
+ private readonly IWebProxy? _proxy;
- public DiagnosticsHandler(HttpMessageHandler innerHandler, DistributedContextPropagator propagator, bool autoRedirect = false)
+ public DiagnosticsHandler(HttpMessageHandler innerHandler, DistributedContextPropagator propagator, IWebProxy? proxy, bool autoRedirect = false)
{
Debug.Assert(GlobalHttpSettings.DiagnosticsHandler.EnableActivityPropagation);
Debug.Assert(innerHandler is not null && propagator is not null);
_innerHandler = innerHandler;
_propagator = propagator;
+ _proxy = proxy;
// Prepare HeaderDescriptors for fields we need to clear when following redirects
if (autoRedirect && _propagator.Fields is IReadOnlyCollection fields && fields.Count > 0)
@@ -125,7 +127,7 @@ private async ValueTask SendAsyncCore(HttpRequestMessage re
if (request.RequestUri is Uri requestUri && requestUri.IsAbsoluteUri)
{
- activity.SetTag("server.address", requestUri.Host);
+ activity.SetTag("server.address", DiagnosticsHelper.GetServerAddress(request, _proxy));
activity.SetTag("server.port", requestUri.Port);
activity.SetTag("url.full", UriRedactionHelper.GetRedactedUriString(requestUri));
}
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHelper.cs b/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHelper.cs
index 5b22e671420cd4..95d1e923bcec90 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHelper.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHelper.cs
@@ -35,6 +35,21 @@ internal static class DiagnosticsHelper
_ => httpVersion.ToString()
};
+ // Picks the value of the 'server.address' tag following rules specified in
+ // https://github.com/open-telemetry/semantic-conventions/blob/728e5d1/docs/http/http-spans.md#http-client-span
+ // When there is no proxy, we need to prioritize the contents of the Host header.
+ // Note that this is a best-effort guess, e.g. we are not checking if proxy.GetProxy(uri) returns null.
+ public static string GetServerAddress(HttpRequestMessage request, IWebProxy? proxy)
+ {
+ Debug.Assert(request.RequestUri is not null);
+ if ((proxy is null || proxy.IsBypassed(request.RequestUri)) && request.HasHeaders && request.Headers.Host is string hostHeader)
+ {
+ return HttpUtilities.ParseHostNameFromHeader(hostHeader);
+ }
+
+ return request.RequestUri.IdnHost;
+ }
+
public static bool TryGetErrorType(HttpResponseMessage? response, Exception? exception, out string? errorType)
{
if (response is not null)
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.AnyMobile.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.AnyMobile.cs
index da2eb54de9b8d9..960db9edc0d461 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.AnyMobile.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.AnyMobile.cs
@@ -43,13 +43,14 @@ private HttpMessageHandler Handler
// MetricsHandler should be descendant of DiagnosticsHandler in the handler chain to make sure the 'http.request.duration'
// metric is recorded before stopping the request Activity. This is needed to make sure that our telemetry supports Exemplars.
+ // Since HttpClientHandler.Proxy is unsupported on most platforms, don't bother passing it to telemetry handlers.
if (GlobalHttpSettings.MetricsHandler.IsGloballyEnabled)
{
- handler = new MetricsHandler(handler, _nativeMeterFactory, out _);
+ handler = new MetricsHandler(handler, _nativeMeterFactory, proxy: null, out _);
}
if (GlobalHttpSettings.DiagnosticsHandler.EnableActivityPropagation)
{
- handler = new DiagnosticsHandler(handler, DistributedContextPropagator.Current);
+ handler = new DiagnosticsHandler(handler, DistributedContextPropagator.Current, proxy: null);
}
// Ensure a single handler is used for all requests.
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs
index 9af5dba293170b..481eb0212524f6 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs
@@ -45,13 +45,14 @@ private HttpMessageHandler Handler
// MetricsHandler should be descendant of DiagnosticsHandler in the handler chain to make sure the 'http.request.duration'
// metric is recorded before stopping the request Activity. This is needed to make sure that our telemetry supports Exemplars.
+ // Since HttpClientHandler.Proxy is unsupported on most platforms, don't bother passing it to telemetry handlers.
if (GlobalHttpSettings.MetricsHandler.IsGloballyEnabled)
{
- handler = new MetricsHandler(handler, _meterFactory, out _);
+ handler = new MetricsHandler(handler, _meterFactory, proxy: null, out _);
}
if (GlobalHttpSettings.DiagnosticsHandler.EnableActivityPropagation)
{
- handler = new DiagnosticsHandler(handler, DistributedContextPropagator.Current);
+ handler = new DiagnosticsHandler(handler, DistributedContextPropagator.Current, proxy: null);
}
// Ensure a single handler is used for all requests.
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs
new file mode 100644
index 00000000000000..19dc5cc54dd0b0
--- /dev/null
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Net.Http
+{
+ internal static partial class HttpUtilities
+ {
+ public static string ParseHostNameFromHeader(string hostHeader)
+ {
+ // See if we need to trim off a port.
+ int colonPos = hostHeader.IndexOf(':');
+ if (colonPos >= 0)
+ {
+ // There is colon, which could either be a port separator or a separator in
+ // an IPv6 address. See if this is an IPv6 address; if it's not, use everything
+ // before the colon as the host name, and if it is, use everything before the last
+ // colon iff the last colon is after the end of the IPv6 address (otherwise it's a
+ // part of the address).
+ int ipV6AddressEnd = hostHeader.IndexOf(']');
+ if (ipV6AddressEnd == -1)
+ {
+ return hostHeader.Substring(0, colonPos);
+ }
+ else
+ {
+ colonPos = hostHeader.LastIndexOf(':');
+ if (colonPos > ipV6AddressEnd)
+ {
+ return hostHeader.Substring(0, colonPos);
+ }
+ }
+ }
+
+ return hostHeader;
+ }
+ }
+}
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/MetricsHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/MetricsHandler.cs
index 9b18f93b7d8e1d..4e6f6a05e69ce2 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/MetricsHandler.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/MetricsHandler.cs
@@ -15,12 +15,14 @@ internal sealed class MetricsHandler : HttpMessageHandlerStage
private readonly HttpMessageHandler _innerHandler;
private readonly UpDownCounter _activeRequests;
private readonly Histogram _requestsDuration;
+ private readonly IWebProxy? _proxy;
- public MetricsHandler(HttpMessageHandler innerHandler, IMeterFactory? meterFactory, out Meter meter)
+ public MetricsHandler(HttpMessageHandler innerHandler, IMeterFactory? meterFactory, IWebProxy? proxy, out Meter meter)
{
Debug.Assert(GlobalHttpSettings.MetricsHandler.IsGloballyEnabled);
_innerHandler = innerHandler;
+ _proxy = proxy;
meter = meterFactory?.Create("System.Net.Http") ?? SharedMeter.Instance;
@@ -137,14 +139,14 @@ private void RequestStop(HttpRequestMessage request, HttpResponseMessage? respon
}
}
- private static TagList InitializeCommonTags(HttpRequestMessage request)
+ private TagList InitializeCommonTags(HttpRequestMessage request)
{
TagList tags = default;
if (request.RequestUri is Uri requestUri && requestUri.IsAbsoluteUri)
{
tags.Add("url.scheme", requestUri.Scheme);
- tags.Add("server.address", requestUri.Host);
+ tags.Add("server.address", DiagnosticsHelper.GetServerAddress(request, _proxy));
tags.Add("server.port", DiagnosticsHelper.GetBoxedInt32(requestUri.Port));
}
tags.Add(DiagnosticsHelper.GetMethodTag(request.Method, out _));
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/ConnectionSetupDistributedTracing.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/ConnectionSetupDistributedTracing.cs
index 522a57f76e61aa..3a65389d20e045 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/ConnectionSetupDistributedTracing.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/ConnectionSetupDistributedTracing.cs
@@ -12,7 +12,7 @@ internal static class ConnectionSetupDistributedTracing
{
private static readonly ActivitySource s_connectionsActivitySource = new ActivitySource(DiagnosticsHandlerLoggingStrings.ConnectionsNamespace);
- public static Activity? StartConnectionSetupActivity(bool isSecure, HttpAuthority authority)
+ public static Activity? StartConnectionSetupActivity(bool isSecure, string? serverAddress, int port)
{
Activity? activity = null;
if (s_connectionsActivitySource.HasListeners())
@@ -25,11 +25,12 @@ internal static class ConnectionSetupDistributedTracing
if (activity is not null)
{
- activity.DisplayName = $"HTTP connection_setup {authority.HostValue}:{authority.Port}";
+ Debug.Assert(serverAddress is not null, "serverAddress should not be null when System.Net.Http.EnableActivityPropagation is true.");
+ activity.DisplayName = $"HTTP connection_setup {serverAddress}:{port}";
if (activity.IsAllDataRequested)
{
- activity.SetTag("server.address", authority.HostValue);
- activity.SetTag("server.port", authority.Port);
+ activity.SetTag("server.address", serverAddress);
+ activity.SetTag("server.port", port);
activity.SetTag("url.scheme", isSecure ? "https" : "http");
}
}
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.Http3.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.Http3.cs
index c2545cd4d7dc36..22bc7aa5f488d0 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.Http3.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.Http3.cs
@@ -263,7 +263,7 @@ private async Task InjectNewHttp3ConnectionAsync(RequestQueue.
{
if (TryGetHttp3Authority(queueItem.Request, out authority, out Exception? reasonException))
{
- connectionSetupActivity = ConnectionSetupDistributedTracing.StartConnectionSetupActivity(isSecure: true, authority);
+ connectionSetupActivity = ConnectionSetupDistributedTracing.StartConnectionSetupActivity(isSecure: true, _telemetryServerAddress, authority.Port);
// If the authority was sent as an option through alt-svc then include alt-used header.
connection = new Http3Connection(this, authority, includeAltUsedHeader: _http3Authority == authority);
QuicConnection quicConnection = await ConnectHelper.ConnectQuicAsync(queueItem.Request, new DnsEndPoint(authority.IdnHost, authority.Port), _poolManager.Settings._pooledConnectionIdleTimeout, _sslOptionsHttp3!, connection.StreamCapacityCallback, cts.Token).ConfigureAwait(false);
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs
index 2b6ae06651122b..e356c0306aba42 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs
@@ -35,6 +35,7 @@ internal sealed partial class HttpConnectionPool : IDisposable
private readonly HttpConnectionPoolManager _poolManager;
private readonly HttpConnectionKind _kind;
private readonly Uri? _proxyUri;
+ private readonly string? _telemetryServerAddress;
/// The origin authority used to construct the .
private readonly HttpAuthority _originAuthority;
@@ -72,12 +73,14 @@ internal sealed partial class HttpConnectionPool : IDisposable
/// The port with which this pool is associated.
/// The SSL host with which this pool is associated.
/// The proxy this pool targets (optional).
- public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionKind kind, string? host, int port, string? sslHostName, Uri? proxyUri)
+ /// The value of the 'server.address' tag to be emitted by Metrics and Distributed Tracing.
+ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionKind kind, string? host, int port, string? sslHostName, Uri? proxyUri, string? telemetryServerAddress)
{
_poolManager = poolManager;
_kind = kind;
_proxyUri = proxyUri;
_maxHttp11Connections = Settings._maxConnectionsPerServer;
+ _telemetryServerAddress = telemetryServerAddress;
// The only case where 'host' will not be set is if this is a Proxy connection pool.
Debug.Assert(host is not null || (kind == HttpConnectionKind.Proxy && proxyUri is not null));
@@ -279,6 +282,7 @@ private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnection
return sslOptions;
}
+ public string? TelemetryServerAddress => _telemetryServerAddress;
public HttpAuthority OriginAuthority => _originAuthority;
public HttpConnectionSettings Settings => _poolManager.Settings;
public HttpConnectionKind Kind => _kind;
@@ -557,7 +561,7 @@ public async ValueTask SendWithVersionDetectionAndRetryAsyn
Exception? exception = null;
TransportContext? transportContext = null;
- Activity? activity = ConnectionSetupDistributedTracing.StartConnectionSetupActivity(IsSecure, OriginAuthority);
+ Activity? activity = ConnectionSetupDistributedTracing.StartConnectionSetupActivity(IsSecure, _telemetryServerAddress, OriginAuthority.Port);
try
{
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs
index 786aff42b94a76..d92131e892a0a4 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs
@@ -69,11 +69,12 @@ protected void MarkConnectionAsEstablished(Activity? connectionSetupActivity, IP
this is Http2Connection ? "2" :
"3";
+ Debug.Assert(_pool.TelemetryServerAddress is not null, "TelemetryServerAddress should not be null when System.Diagnostics.Metrics.Meter.IsSupported is true.");
_connectionMetrics = new ConnectionMetrics(
metrics,
protocol,
_pool.IsSecure ? "https" : "http",
- _pool.OriginAuthority.HostValue,
+ _pool.TelemetryServerAddress,
_pool.OriginAuthority.Port,
remoteEndPoint?.Address?.ToString());
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs
index 8424d1b26cebe2..a696e153ca5c89 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs
@@ -227,35 +227,6 @@ public void Dispose()
public HttpConnectionSettings Settings => _settings;
public ICredentials? ProxyCredentials => _proxyCredentials;
- private static string ParseHostNameFromHeader(string hostHeader)
- {
- // See if we need to trim off a port.
- int colonPos = hostHeader.IndexOf(':');
- if (colonPos >= 0)
- {
- // There is colon, which could either be a port separator or a separator in
- // an IPv6 address. See if this is an IPv6 address; if it's not, use everything
- // before the colon as the host name, and if it is, use everything before the last
- // colon iff the last colon is after the end of the IPv6 address (otherwise it's a
- // part of the address).
- int ipV6AddressEnd = hostHeader.IndexOf(']');
- if (ipV6AddressEnd == -1)
- {
- return hostHeader.Substring(0, colonPos);
- }
- else
- {
- colonPos = hostHeader.LastIndexOf(':');
- if (colonPos > ipV6AddressEnd)
- {
- return hostHeader.Substring(0, colonPos);
- }
- }
- }
-
- return hostHeader;
- }
-
private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? proxyUri, bool isProxyConnect)
{
Uri? uri = request.RequestUri;
@@ -273,7 +244,7 @@ private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? prox
string? hostHeader = request.Headers.Host;
if (hostHeader != null)
{
- sslHostName = ParseHostNameFromHeader(hostHeader);
+ sslHostName = HttpUtilities.ParseHostNameFromHeader(hostHeader);
}
else
{
@@ -330,6 +301,34 @@ private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? prox
}
}
+ // Picks the value of the 'server.address' tag following rules specified in
+ // https://github.com/open-telemetry/semantic-conventions/blob/728e5d1/docs/http/http-spans.md#http-client-span
+ // When there is no proxy, we need to prioritize the contents of the Host header.
+ private static string? GetTelemetryServerAddress(HttpRequestMessage request, HttpConnectionKey key)
+ {
+ if (GlobalHttpSettings.MetricsHandler.IsGloballyEnabled || GlobalHttpSettings.DiagnosticsHandler.EnableActivityPropagation)
+ {
+ Uri? uri = request.RequestUri;
+ Debug.Assert(uri is not null);
+
+ if (key.SslHostName is not null)
+ {
+ return key.SslHostName;
+ }
+
+ if (key.ProxyUri is not null && key.Kind == HttpConnectionKind.Proxy)
+ {
+ // In case there is no tunnel, return the proxy address since the connection is shared.
+ return key.ProxyUri.IdnHost;
+ }
+
+ string? hostHeader = request.Headers.Host;
+ return hostHeader is null ? uri.IdnHost : HttpUtilities.ParseHostNameFromHeader(hostHeader);
+ }
+
+ return null;
+ }
+
public ValueTask SendAsyncCore(HttpRequestMessage request, Uri? proxyUri, bool async, bool doRequestAuth, bool isProxyConnect, CancellationToken cancellationToken)
{
HttpConnectionKey key = GetConnectionKey(request, proxyUri, isProxyConnect);
@@ -337,7 +336,7 @@ public ValueTask SendAsyncCore(HttpRequestMessage request,
HttpConnectionPool? pool;
while (!_pools.TryGetValue(key, out pool))
{
- pool = new HttpConnectionPool(this, key.Kind, key.Host, key.Port, key.SslHostName, key.ProxyUri);
+ pool = new HttpConnectionPool(this, key.Kind, key.Host, key.Port, key.SslHostName, key.ProxyUri, GetTelemetryServerAddress(request, key));
if (_cleaningTimer == null)
{
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.SocketsHttpHandler.cs
similarity index 97%
rename from src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.cs
rename to src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.SocketsHttpHandler.cs
index be47d560ea77e1..ed5f1c14459716 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.SocketsHttpHandler.cs
@@ -3,7 +3,7 @@
namespace System.Net.Http
{
- internal static class HttpUtilities
+ internal static partial class HttpUtilities
{
internal static bool IsSupportedScheme(string scheme) =>
IsSupportedNonSecureScheme(scheme) ||
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/SocketsHttpHandlerMetrics.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/SocketsHttpHandlerMetrics.cs
index 8933aa5c3fd23b..eabd5db88de69f 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/SocketsHttpHandlerMetrics.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/SocketsHttpHandlerMetrics.cs
@@ -46,7 +46,9 @@ public void RequestLeftQueue(HttpRequestMessage request, HttpConnectionPool pool
});
tags.Add("url.scheme", pool.IsSecure ? "https" : "http");
- tags.Add("server.address", pool.OriginAuthority.HostValue);
+
+ Debug.Assert(pool.TelemetryServerAddress is not null, "TelemetryServerAddress should not be null when System.Diagnostics.Metrics.Meter.IsSupported is true.");
+ tags.Add("server.address", pool.TelemetryServerAddress);
if (!pool.IsDefaultPort)
{
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs
index 19f4197ccd26d6..f4b5dba31dd724 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs
@@ -527,14 +527,14 @@ private HttpMessageHandlerStage SetupHandlerChain()
// metric is recorded before stopping the request Activity. This is needed to make sure that our telemetry supports Exemplars.
if (GlobalHttpSettings.MetricsHandler.IsGloballyEnabled)
{
- handler = new MetricsHandler(handler, settings._meterFactory, out Meter meter);
+ handler = new MetricsHandler(handler, settings._meterFactory, settings._proxy, out Meter meter);
settings._metrics = new SocketsHttpHandlerMetrics(meter);
}
// DiagnosticsHandler is inserted before RedirectHandler so that trace propagation is done on redirects as well
if (GlobalHttpSettings.DiagnosticsHandler.EnableActivityPropagation && settings._activityHeadersPropagator is DistributedContextPropagator propagator)
{
- handler = new DiagnosticsHandler(handler, propagator, settings._allowAutoRedirect);
+ handler = new DiagnosticsHandler(handler, propagator, settings._proxy, settings._allowAutoRedirect);
}
if (settings._allowAutoRedirect)
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs
index ffc8cc7fdd93ce..44d8488ad5adef 100644
--- a/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs
+++ b/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs
@@ -1665,6 +1665,112 @@ static async Task RunTest()
}
}
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task UseIPAddressInTargetUri_NoProxy_RecordsHostHeaderAsServerAddress(bool useTls)
+ {
+ if (UseVersion == HttpVersion30 && !useTls) return;
+
+ await RemoteExecutor.Invoke(RunTest, UseVersion.ToString(), TestAsync.ToString(), useTls.ToString()).DisposeAsync();
+ static async Task RunTest(string useVersion, string testAsync, string useTlsString)
+ {
+ bool useTls = bool.Parse(useTlsString);
+
+ Activity parentActivity = new Activity("parent").Start();
+
+ using ActivityRecorder requestRecorder = new("System.Net.Http", "System.Net.Http.HttpRequestOut")
+ {
+ ExpectedParent = parentActivity
+ };
+
+ using ActivityRecorder connectionSetupRecorder = new("Experimental.System.Net.Http.Connections", "Experimental.System.Net.Http.Connections.ConnectionSetup");
+
+ await GetFactoryForVersion(useVersion).CreateClientAndServerAsync(
+ async uri =>
+ {
+ string hostName = uri.Host;
+ uri = new Uri($"{uri.Scheme}://{IPAddress.Loopback}:{uri.Port}");
+
+ Version version = Version.Parse(useVersion);
+
+ using HttpClient client = new HttpClient(CreateHttpClientHandler(allowAllCertificates: true));
+
+ using HttpRequestMessage request = CreateRequest(HttpMethod.Get, uri, version, exactVersion: true);
+ request.Headers.Host = hostName;
+
+ await client.SendAsync(bool.Parse(testAsync), request);
+
+ Activity req = requestRecorder.VerifyActivityRecordedOnce();
+ ActivityAssert.HasTag(req, "server.address", hostName);
+
+ Activity conn = connectionSetupRecorder.VerifyActivityRecordedOnce();
+ ActivityAssert.HasTag(conn, "server.address", hostName);
+ },
+ async server =>
+ {
+ await server.AcceptConnectionSendResponseAndCloseAsync();
+ }, options: new GenericLoopbackOptions()
+ {
+ UseSsl = useTls,
+ });
+ }
+ }
+
+ [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ public async Task UseIPAddressInTargetUri_ProxyTunnel()
+ {
+ if (UseVersion != HttpVersion.Version11)
+ {
+ throw new SkipTestException("Test only for HTTP/1.1");
+ }
+
+ await RemoteExecutor.Invoke(RunTest, UseVersion.ToString(), TestAsync.ToString()).DisposeAsync();
+ static async Task RunTest(string useVersion, string testAsync)
+ {
+ Activity parentActivity = new Activity("parent").Start();
+
+ using ActivityRecorder requestRecorder = new("System.Net.Http", "System.Net.Http.HttpRequestOut")
+ {
+ ExpectedParent = parentActivity
+ };
+
+ using ActivityRecorder connectionSetupRecorder = new("Experimental.System.Net.Http.Connections", "Experimental.System.Net.Http.Connections.ConnectionSetup");
+
+ using LoopbackProxyServer proxyServer = LoopbackProxyServer.Create();
+ await GetFactoryForVersion(useVersion).CreateClientAndServerAsync(
+ async uri =>
+ {
+ uri = new Uri($"{uri.Scheme}://{IPAddress.Loopback}:{uri.Port}");
+
+ Version version = Version.Parse(useVersion);
+
+ HttpClientHandler handler = CreateHttpClientHandler(allowAllCertificates: true);
+ handler.Proxy = new WebProxy(proxyServer.Uri);
+ using HttpClient client = new HttpClient(handler);
+
+ using HttpRequestMessage request = CreateRequest(HttpMethod.Get, uri, version, exactVersion: true);
+ request.Headers.Host = "localhost";
+
+ await client.SendAsync(bool.Parse(testAsync), request);
+
+ // There should be only one Activity for the request. Server address should match the Uri host.
+ // https://github.com/open-telemetry/semantic-conventions/blob/728e5d1/docs/http/http-spans.md#http-client-span
+ ActivityAssert.HasTag(requestRecorder.FinishedActivities.Single(), "server.address", IPAddress.Loopback.ToString());
+
+ // Check the SslProxyTunnel connection only, it should use the host header.
+ Assert.Contains(connectionSetupRecorder.FinishedActivities, a => a.TagObjects.Any(t => t.Key == "server.port" && t.Value.Equals(uri.Port)));
+ },
+ async server =>
+ {
+ await server.AcceptConnectionSendResponseAndCloseAsync();
+ }, options: new GenericLoopbackOptions()
+ {
+ UseSsl = true,
+ });
+ }
+ }
+
private static T GetProperty(object obj, string propertyName)
{
Type t = obj.GetType();
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs
index c1cbd3540a823d..46a294180e9813 100644
--- a/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs
+++ b/src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs
@@ -920,6 +920,57 @@ await server.AcceptConnectionAsync(async conn =>
});
}
+ [ConditionalTheory(typeof(SocketsHttpHandler), nameof(SocketsHttpHandler.IsSupported))]
+ [InlineData(false)]
+ [InlineData(true)]
+ public Task UseIPAddressInTargetUri_NoProxy_RecordsHostHeaderAsServerAddress(bool useTls)
+ {
+ if (UseVersion == HttpVersion30 && !useTls)
+ {
+ throw new SkipTestException("No insecure connections with HTTP/3.");
+ }
+
+ return LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
+ {
+ using HttpMessageInvoker client = CreateHttpMessageInvoker();
+ uri = new Uri($"{uri.Scheme}://{IPAddress.Loopback}:{uri.Port}");
+
+ using InstrumentRecorder activeRequestsRecorder = SetupInstrumentRecorder(InstrumentNames.ActiveRequests);
+ using InstrumentRecorder requestDurationRecorder = SetupInstrumentRecorder(InstrumentNames.RequestDuration);
+ using InstrumentRecorder openConnectionsRecorder = SetupInstrumentRecorder(InstrumentNames.OpenConnections);
+ using InstrumentRecorder connectionDurationRecorder = SetupInstrumentRecorder(InstrumentNames.ConnectionDuration);
+ using InstrumentRecorder timeInQueueRecorder = SetupInstrumentRecorder(InstrumentNames.TimeInQueue);
+
+ using HttpRequestMessage request = new(HttpMethod.Get, uri) { Version = UseVersion, VersionPolicy = HttpVersionPolicy.RequestVersionExact };
+ request.Headers.Host = "localhost";
+ HttpResponseMessage response = await SendAsync(client, request);
+ response.Dispose(); // Make sure disposal doesn't interfere with recording by enforcing early disposal.
+
+ // Request metrics:
+ VerifyHostName(activeRequestsRecorder);
+ VerifyHostName(requestDurationRecorder);
+
+ // Connection metrics:
+ VerifyHostName(openConnectionsRecorder);
+ VerifyHostName(connectionDurationRecorder);
+ VerifyHostName(timeInQueueRecorder);
+ }, async server =>
+ {
+ await server.AcceptConnectionSendResponseAndCloseAsync();
+ }, options: new GenericLoopbackOptions()
+ {
+ UseSsl = useTls,
+ });
+
+ static void VerifyHostName(InstrumentRecorder recorder) where T : struct
+ {
+ foreach (Measurement m in recorder.GetMeasurements())
+ {
+ VerifyTag(m.Tags.ToArray(), "server.address", "localhost");
+ }
+ }
+ }
+
protected override void Dispose(bool disposing)
{
if (disposing)
@@ -1158,6 +1209,47 @@ await server.AcceptConnectionAsync(async connection =>
}, new LoopbackServer.Options() { UseSsl = true });
}
+
+ [ConditionalFact(typeof(SocketsHttpHandler), nameof(SocketsHttpHandler.IsSupported))]
+ public async Task UseIPAddressInTargetUri_ProxyTunnel_RequestMetricsRecordUriHostAsServerAddress()
+ {
+ using LoopbackProxyServer proxyServer = LoopbackProxyServer.Create();
+ await LoopbackServerFactory.CreateClientAndServerAsync(
+ async uri =>
+ {
+ uri = new Uri($"{uri.Scheme}://{IPAddress.Loopback}:{uri.Port}");
+
+ //HttpClientHandler handler = CreateHttpClientHandler(allowAllCertificates: true);
+ Handler.Proxy = new WebProxy(proxyServer.Uri);
+ using HttpMessageInvoker client = CreateHttpMessageInvoker();
+ using HttpRequestMessage request = CreateRequest(HttpMethod.Get, uri, UseVersion, exactVersion: true);
+ request.Headers.Host = "localhost";
+
+ using InstrumentRecorder activeRequestsRecorder = SetupInstrumentRecorder(InstrumentNames.ActiveRequests);
+ using InstrumentRecorder requestDurationRecorder = SetupInstrumentRecorder(InstrumentNames.RequestDuration);
+
+ (await SendAsync(client, request)).Dispose();
+
+ // Request metrics:
+ VerifyHostName(activeRequestsRecorder, uri.Host);
+ VerifyHostName(requestDurationRecorder, uri.Host);
+ },
+ async server =>
+ {
+ await server.AcceptConnectionSendResponseAndCloseAsync();
+ }, options: new GenericLoopbackOptions()
+ {
+ UseSsl = true,
+ });
+
+ void VerifyHostName(InstrumentRecorder recorder, string hostName) where T : struct
+ {
+ foreach (Measurement m in recorder.GetMeasurements())
+ {
+ VerifyTag(m.Tags.ToArray(), "server.address", hostName);
+ }
+ }
+ }
}
[ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile))]
diff --git a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj
index 40eacbfd8e6e08..be119816ef4532 100755
--- a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj
+++ b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj
@@ -1,4 +1,4 @@
-
+
../../src/Resources/Strings.resx
true
@@ -77,6 +77,8 @@
Link="ProductionCode\System\Net\Http\GlobalHttpSettings.cs"/>
+
-
+