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"/> + - +