-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Add SocketsHttpHandler requests-queue-duration metrics #88981
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -363,6 +363,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 : DefaultHttpsPort); | ||
|
|
||
| /// <summary> | ||
| /// An ASCII origin string per RFC 6454 Section 6.2, in format <scheme>://<host>[:<port>] | ||
|
|
@@ -998,19 +999,20 @@ private async ValueTask<Http3Connection> GetHttp3ConnectionAsync(HttpRequestMess | |
| ThrowGetVersionException(request, 3, reasonException); | ||
| } | ||
|
|
||
| long queueStartingTimestamp = HttpTelemetry.Log.IsEnabled() ? Stopwatch.GetTimestamp() : 0; | ||
| bool requestsQueueDurationEnabled = HttpTelemetry.Log.IsEnabled() || Settings._metrics!.RequestsQueueDuration.Enabled; | ||
| long queueStartingTimestamp = requestsQueueDurationEnabled ? Stopwatch.GetTimestamp() : 0; | ||
|
|
||
| ValueTask<Http3Connection> connectionTask = GetHttp3ConnectionAsync(request, authority, cancellationToken); | ||
|
|
||
| if (HttpTelemetry.Log.IsEnabled() && connectionTask.IsCompleted) | ||
| if (requestsQueueDurationEnabled && 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); | ||
| HttpResponseMessage response = await connection.SendAsync(request, requestsQueueDurationEnabled, queueStartingTimestamp, cancellationToken).ConfigureAwait(false); | ||
|
|
||
| // If an Alt-Svc authority returns 421, it means it can't actually handle the request. | ||
| // An authority is supposed to be able to handle ALL requests to the origin, so this is a server bug. | ||
|
|
@@ -1089,7 +1091,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); | ||
|
|
@@ -1121,7 +1123,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. | ||
|
|
@@ -1199,6 +1201,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 || | ||
|
|
@@ -2430,6 +2433,7 @@ private void Trace(string? message, [CallerMemberName] string? memberName = null | |
| message); // message | ||
|
|
||
| private struct RequestQueue<T> | ||
| where T : HttpConnectionBase? | ||
| { | ||
| public struct QueueItem | ||
| { | ||
|
|
@@ -2599,6 +2603,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. | ||
|
|
@@ -2607,22 +2612,25 @@ 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 | ||
| { | ||
| 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); | ||
| } | ||
| pool.Settings._metrics!.RequestLeftQueue(pool, Stopwatch.GetElapsedTime(startingTimestamp), versionMajor: typeof(T) == typeof(HttpConnection) ? 1 : 2); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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 | ||
|
|
@@ -19,5 +20,42 @@ 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; | ||
|
|
||
| tags.Add("protocol", versionMajor switch | ||
| { | ||
| 1 => "HTTP/1.1", | ||
| 2 => "HTTP/2", | ||
| _ => "HTTP/3" | ||
| }); | ||
|
Comment on lines
+38
to
+43
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
|
||
| 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); | ||
| } | ||
|
|
||
| if (HttpTelemetry.Log.IsEnabled()) | ||
| { | ||
| HttpTelemetry.Log.RequestLeftQueue(versionMajor, duration); | ||
| } | ||
MihaZupan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.