Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/libraries/System.Net.Http/src/System.Net.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
<Compile Include="System\Net\Http\HttpIOException.cs" />
<Compile Include="System\Net\Http\HttpRuleParser.cs" />
<Compile Include="System\Net\Http\HttpTelemetry.cs" />
<Compile Include="System\Net\Http\HttpUtilities.cs" />
<Compile Include="System\Net\Http\HttpVersionPolicy.cs" />
<Compile Include="System\Net\Http\MessageProcessingHandler.cs" />
<Compile Include="System\Net\Http\MultipartContent.cs" />
Expand Down Expand Up @@ -209,7 +210,7 @@
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpContentStream.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpContentWriteStream.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpKeepAlivePingPolicy.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpUtilities.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpUtilities.SocketsHttpHandler.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\IHttpTrace.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\IMultiWebProxy.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\MultiProxy.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> fields && fields.Count > 0)
Expand Down Expand Up @@ -125,7 +127,7 @@ private async ValueTask<HttpResponseMessage> 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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
37 changes: 37 additions & 0 deletions src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ internal sealed class MetricsHandler : HttpMessageHandlerStage
private readonly HttpMessageHandler _innerHandler;
private readonly UpDownCounter<long> _activeRequests;
private readonly Histogram<double> _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;

Expand Down Expand Up @@ -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 _));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ private async Task InjectNewHttp3ConnectionAsync(RequestQueue<Http3Connection?>.
{
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>The origin authority used to construct the <see cref="HttpConnectionPool"/>.</summary>
private readonly HttpAuthority _originAuthority;
Expand Down Expand Up @@ -72,12 +73,14 @@ internal sealed partial class HttpConnectionPool : IDisposable
/// <param name="port">The port with which this pool is associated.</param>
/// <param name="sslHostName">The SSL host with which this pool is associated.</param>
/// <param name="proxyUri">The proxy this pool targets (optional).</param>
public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionKind kind, string? host, int port, string? sslHostName, Uri? proxyUri)
/// <param name="telemetryServerAddress">The value of the 'server.address' tag to be emitted by Metrics and Distributed Tracing.</param>
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));
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -557,7 +561,7 @@ public async ValueTask<HttpResponseMessage> SendWithVersionDetectionAndRetryAsyn
Exception? exception = null;
TransportContext? transportContext = null;

Activity? activity = ConnectionSetupDistributedTracing.StartConnectionSetupActivity(IsSecure, OriginAuthority);
Activity? activity = ConnectionSetupDistributedTracing.StartConnectionSetupActivity(IsSecure, _telemetryServerAddress, OriginAuthority.Port);

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -330,14 +301,42 @@ 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<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request, Uri? proxyUri, bool async, bool doRequestAuth, bool isProxyConnect, CancellationToken cancellationToken)
{
HttpConnectionKey key = GetConnectionKey(request, proxyUri, isProxyConnect);

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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) ||
Expand Down
Loading
Loading