Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SocketsHttpHandler requests-queue-duration metrics #88981

Merged
merged 4 commits into from
Jul 17, 2023
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Threading;

Expand All @@ -21,24 +22,22 @@ internal sealed partial class HttpTelemetry
private EventCounter? _http30RequestsQueueDurationCounter;

[NonEvent]
public void Http11RequestLeftQueue(double timeOnQueueMilliseconds)
public void RequestLeftQueue(int versionMajor, TimeSpan duration)
{
_http11RequestsQueueDurationCounter?.WriteMetric(timeOnQueueMilliseconds);
RequestLeftQueue(timeOnQueueMilliseconds, versionMajor: 1, versionMinor: 1);
}
Debug.Assert(versionMajor is 1 or 2 or 3);

[NonEvent]
public void Http20RequestLeftQueue(double timeOnQueueMilliseconds)
{
_http20RequestsQueueDurationCounter?.WriteMetric(timeOnQueueMilliseconds);
RequestLeftQueue(timeOnQueueMilliseconds, versionMajor: 2, versionMinor: 0);
}
EventCounter? counter = versionMajor switch
{
1 => _http11RequestsQueueDurationCounter,
2 => _http20RequestsQueueDurationCounter,
_ => _http30RequestsQueueDurationCounter
};

[NonEvent]
public void Http30RequestLeftQueue(double timeOnQueueMilliseconds)
{
_http30RequestsQueueDurationCounter?.WriteMetric(timeOnQueueMilliseconds);
RequestLeftQueue(timeOnQueueMilliseconds, versionMajor: 3, versionMinor: 0);
double timeOnQueueMs = duration.TotalMilliseconds;

counter?.WriteMetric(timeOnQueueMs);

RequestLeftQueue(timeOnQueueMs, (byte)versionMajor, versionMinor: versionMajor == 1 ? (byte)1 : (byte)0);
}

protected override void OnEventCommand(EventCommandEventArgs command)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,6 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, lon
QuicConnection? conn = _connection;
if (conn != null)
{
if (HttpTelemetry.Log.IsEnabled() && queueStartingTimestamp == 0)
{
queueStartingTimestamp = Stopwatch.GetTimestamp();
}

quicStream = await conn.OpenOutboundStreamAsync(QuicStreamType.Bidirectional, cancellationToken).ConfigureAwait(false);

requestStream = new Http3RequestStream(request, this, quicStream);
Expand All @@ -198,9 +193,16 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, lon
catch (QuicException e) when (e.QuicError != QuicError.OperationAborted) { }
finally
{
if (HttpTelemetry.Log.IsEnabled() && queueStartingTimestamp != 0)
if (queueStartingTimestamp != 0)
{
HttpTelemetry.Log.Http30RequestLeftQueue(Stopwatch.GetElapsedTime(queueStartingTimestamp).TotalMilliseconds);
TimeSpan duration = Stopwatch.GetElapsedTime(queueStartingTimestamp);

_pool.Settings._metrics!.RequestLeftQueue(Pool, duration, versionMajor: 3);

if (HttpTelemetry.Log.IsEnabled())
{
HttpTelemetry.Log.RequestLeftQueue(versionMajor: 3, duration);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,18 @@ public HttpConnectionBase(HttpConnectionPool pool)
metrics.IdleConnections.Enabled ||
metrics.ConnectionDuration.Enabled)
{
// While requests may report HTTP/1.0 as the protocol, we treat all HTTP/1.X connections as HTTP/1.1.
string protocol =
this is HttpConnection ? "HTTP/1.1" :
this is Http2Connection ? "HTTP/2" :
"HTTP/3";

int port = pool.OriginAuthority.Port;
int defaultPort = pool.IsSecure ? HttpConnectionPool.DefaultHttpsPort : HttpConnectionPool.DefaultHttpPort;

_connectionMetrics = new ConnectionMetrics(
metrics,
protocol,
pool.IsSecure ? "https" : "http",
pool.OriginAuthority.HostValue,
port == defaultPort ? null : port);
pool.IsDefaultPort ? null : pool.OriginAuthority.Port);

_connectionMetrics.ConnectionEstablished();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,9 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK
{
// Precalculate ASCII bytes for Host header
// Note that if _host is null, this is a (non-tunneled) proxy connection, and we can't cache the hostname.
hostHeader =
(_originAuthority.Port != (sslHostName == null ? DefaultHttpPort : DefaultHttpsPort)) ?
$"{_originAuthority.HostValue}:{_originAuthority.Port}" :
_originAuthority.HostValue;
hostHeader = IsDefaultPort
? _originAuthority.HostValue
: $"{_originAuthority.HostValue}:{_originAuthority.Port}";

// Note the IDN hostname should always be ASCII, since it's already been IDNA encoded.
byte[] hostHeaderLine = new byte[6 + hostHeader.Length + 2]; // Host: foo\r\n
Expand Down Expand Up @@ -363,6 +362,7 @@ private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnection
public ICredentials? ProxyCredentials => _poolManager.ProxyCredentials;
public byte[]? HostHeaderLineBytes => _hostHeaderLineBytes;
public CredentialCache? PreAuthCredentials { get; }
public bool IsDefaultPort => OriginAuthority.Port == (IsSecure ? DefaultHttpsPort : DefaultHttpPort);

/// <summary>
/// An ASCII origin string per RFC 6454 Section 6.2, in format &lt;scheme&gt;://&lt;host&gt;[:&lt;port&gt;]
Expand All @@ -381,7 +381,7 @@ public byte[] Http2AltSvcOriginUri
sb.Append(IsSecure ? "https://" : "http://")
.Append(_originAuthority.IdnHost);

if (_originAuthority.Port != (IsSecure ? DefaultHttpsPort : DefaultHttpPort))
if (!IsDefaultPort)
{
sb.Append(CultureInfo.InvariantCulture, $":{_originAuthority.Port}");
}
Expand Down Expand Up @@ -998,16 +998,10 @@ private async ValueTask<Http3Connection> GetHttp3ConnectionAsync(HttpRequestMess
ThrowGetVersionException(request, 3, reasonException);
}

long queueStartingTimestamp = HttpTelemetry.Log.IsEnabled() ? Stopwatch.GetTimestamp() : 0;
long queueStartingTimestamp = HttpTelemetry.Log.IsEnabled() || Settings._metrics!.RequestsQueueDuration.Enabled ? Stopwatch.GetTimestamp() : 0;

ValueTask<Http3Connection> connectionTask = GetHttp3ConnectionAsync(request, authority, cancellationToken);

if (HttpTelemetry.Log.IsEnabled() && connectionTask.IsCompleted)
{
// We avoid logging RequestLeftQueue if a stream was available immediately (synchronously)
queueStartingTimestamp = 0;
}

Http3Connection connection = await connectionTask.ConfigureAwait(false);

HttpResponseMessage response = await connection.SendAsync(request, queueStartingTimestamp, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -1089,7 +1083,7 @@ public async ValueTask<HttpResponseMessage> SendWithVersionDetectionAndRetryAsyn
if (!TryGetPooledHttp2Connection(request, out Http2Connection? connection, out http2ConnectionWaiter) &&
http2ConnectionWaiter != null)
{
connection = await http2ConnectionWaiter.WaitForConnectionAsync(async, cancellationToken).ConfigureAwait(false);
connection = await http2ConnectionWaiter.WaitForConnectionAsync(this, async, cancellationToken).ConfigureAwait(false);
}

Debug.Assert(connection is not null || !_http2Enabled);
Expand Down Expand Up @@ -1121,7 +1115,7 @@ public async ValueTask<HttpResponseMessage> SendWithVersionDetectionAndRetryAsyn
// Use HTTP/1.x.
if (!TryGetPooledHttp11Connection(request, async, out HttpConnection? connection, out http11ConnectionWaiter))
{
connection = await http11ConnectionWaiter.WaitForConnectionAsync(async, cancellationToken).ConfigureAwait(false);
connection = await http11ConnectionWaiter.WaitForConnectionAsync(this, async, cancellationToken).ConfigureAwait(false);
}

connection.Acquire(); // In case we are doing Windows (i.e. connection-based) auth, we need to ensure that we hold on to this specific connection while auth is underway.
Expand Down Expand Up @@ -1199,6 +1193,7 @@ public async ValueTask<HttpResponseMessage> SendWithVersionDetectionAndRetryAsyn
}

private void CancelIfNecessary<T>(HttpConnectionWaiter<T>? waiter, bool requestCancelled)
where T : HttpConnectionBase?
{
int timeout = GlobalHttpSettings.SocketsHttpHandler.PendingConnectionTimeoutOnRequestCompletion;
if (waiter?.ConnectionCancellationTokenSource is null ||
Expand Down Expand Up @@ -2430,6 +2425,7 @@ private void Trace(string? message, [CallerMemberName] string? memberName = null
message); // message

private struct RequestQueue<T>
where T : HttpConnectionBase?
{
public struct QueueItem
{
Expand Down Expand Up @@ -2599,6 +2595,7 @@ public QueueItem PeekNextRequestForConnectionAttempt()
}

private sealed class HttpConnectionWaiter<T> : TaskCompletionSourceWithCancellation<T>
where T : HttpConnectionBase?
{
// When a connection attempt is pending, reference the connection's CTS, so we can tear it down if the initiating request is cancelled
// or completes on a different connection.
Expand All @@ -2607,21 +2604,32 @@ private sealed class HttpConnectionWaiter<T> : TaskCompletionSourceWithCancellat
// Distinguish connection cancellation that happens because the initiating request is cancelled or completed on a different connection.
public bool CancelledByOriginatingRequestCompletion { get; set; }

public async ValueTask<T> WaitForConnectionAsync(bool async, CancellationToken requestCancellationToken)
public ValueTask<T> WaitForConnectionAsync(HttpConnectionPool pool, bool async, CancellationToken requestCancellationToken)
{
return HttpTelemetry.Log.IsEnabled() || pool.Settings._metrics!.RequestsQueueDuration.Enabled
? WaitForConnectionWithTelemetryAsync(pool, async, requestCancellationToken)
: WaitWithCancellationAsync(async, requestCancellationToken);
}

private async ValueTask<T> WaitForConnectionWithTelemetryAsync(HttpConnectionPool pool, bool async, CancellationToken requestCancellationToken)
{
Debug.Assert(typeof(T) == typeof(HttpConnection) || typeof(T) == typeof(Http2Connection));

long startingTimestamp = Stopwatch.GetTimestamp();
try
{
return await WaitWithCancellationAsync(async, requestCancellationToken).ConfigureAwait(false);
}
finally
{
TimeSpan duration = Stopwatch.GetElapsedTime(startingTimestamp);
int versionMajor = typeof(T) == typeof(HttpConnection) ? 1 : 2;

pool.Settings._metrics!.RequestLeftQueue(pool, duration, versionMajor);

if (HttpTelemetry.Log.IsEnabled())
{
if (typeof(T) == typeof(HttpConnection))
HttpTelemetry.Log.Http11RequestLeftQueue(Stopwatch.GetElapsedTime(startingTimestamp).TotalMilliseconds);
else if (typeof(T) == typeof(Http2Connection))
HttpTelemetry.Log.Http20RequestLeftQueue(Stopwatch.GetElapsedTime(startingTimestamp).TotalMilliseconds);
HttpTelemetry.Log.RequestLeftQueue(versionMajor, duration);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.Metrics;

namespace System.Net.Http.Metrics
Expand All @@ -19,5 +20,38 @@ internal sealed class SocketsHttpHandlerMetrics(Meter meter)
name: "http-client-connection-duration",
unit: "s",
description: "The duration of outbound HTTP connections.");

public readonly Histogram<double> RequestsQueueDuration = meter.CreateHistogram<double>(
name: "http-client-requests-queue-duration",
unit: "s",
description: "The amount of time requests spent on a queue waiting for an available connection.");

public void RequestLeftQueue(HttpConnectionPool pool, TimeSpan duration, int versionMajor)
{
Debug.Assert(versionMajor is 1 or 2 or 3);

if (RequestsQueueDuration.Enabled)
{
TagList tags = default;

// While requests may report HTTP/1.0 as the protocol, we treat all HTTP/1.X connections as HTTP/1.1.
tags.Add("protocol", versionMajor switch
{
1 => "HTTP/1.1",
2 => "HTTP/2",
_ => "HTTP/3"
});
Comment on lines +38 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume it makes more sense to conform to other connection metrics and existing event counters, but note that request metrics log HTTP/1.0 if HttpRequestMessage.Version is 1.0. Is it worth a comment?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense for requests to log 1.0, but for connections, we only really have HTTP/1.1 that we share between 1.0 and 1.1 requests.
Added a small comment.


tags.Add("scheme", pool.IsSecure ? "https" : "http");
tags.Add("host", pool.OriginAuthority.HostValue);

if (!pool.IsDefaultPort)
{
tags.Add("port", pool.OriginAuthority.Port);
}

RequestsQueueDuration.Record(duration.TotalSeconds, tags);
}
}
}
}
Loading