Skip to content

Commit 28a4c95

Browse files
authored
HttpClientHandler request metrics (#87319)
duration, current requests, failed requests
1 parent 218d2ef commit 28a4c95

16 files changed

+1418
-48
lines changed

src/libraries/System.Net.Http/ref/System.Net.Http.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ public HttpClientHandler() { }
135135
public long MaxRequestContentBufferSize { get { throw null; } set { } }
136136
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
137137
public int MaxResponseHeadersLength { get { throw null; } set { } }
138+
[System.CLSCompliantAttribute(false)]
139+
public System.Diagnostics.Metrics.IMeterFactory? MeterFactory { get { throw null; } set { } }
138140
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
139141
public bool PreAuthenticate { get { throw null; } set { } }
140142
public System.Collections.Generic.IDictionary<string, object?> Properties { get { throw null; } }
@@ -397,6 +399,8 @@ public SocketsHttpHandler() { }
397399
public int MaxConnectionsPerServer { get { throw null; } set { } }
398400
public int MaxResponseDrainSize { get { throw null; } set { } }
399401
public int MaxResponseHeadersLength { get { throw null; } set { } }
402+
[System.CLSCompliantAttribute(false)]
403+
public System.Diagnostics.Metrics.IMeterFactory? MeterFactory { get { throw null; } set { } }
400404
public System.TimeSpan PooledConnectionIdleTimeout { get { throw null; } set { } }
401405
public System.TimeSpan PooledConnectionLifetime { get { throw null; } set { } }
402406
public bool PreAuthenticate { get { throw null; } set { } }
@@ -901,3 +905,15 @@ public WarningHeaderValue(int code, string agent, string text, System.DateTimeOf
901905
public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? input, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.Http.Headers.WarningHeaderValue? parsedValue) { throw null; }
902906
}
903907
}
908+
namespace System.Net.Http.Metrics
909+
{
910+
public sealed class HttpMetricsEnrichmentContext
911+
{
912+
internal HttpMetricsEnrichmentContext() { }
913+
public System.Net.Http.HttpRequestMessage Request { get { throw null; } }
914+
public System.Net.Http.HttpResponseMessage? Response { get { throw null; } }
915+
public System.Exception? Exception { get { throw null; } }
916+
public void AddCustomTag(string name, object? value) { throw null; }
917+
public static void AddCallback(System.Net.Http.HttpRequestMessage request, System.Action<System.Net.Http.Metrics.HttpMetricsEnrichmentContext> callback) { throw null; }
918+
}
919+
}

src/libraries/System.Net.Http/src/System.Net.Http.csproj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@
122122
<Compile Include="System\Net\Http\Headers\UriHeaderParser.cs" />
123123
<Compile Include="System\Net\Http\Headers\ViaHeaderValue.cs" />
124124
<Compile Include="System\Net\Http\Headers\WarningHeaderValue.cs" />
125+
<Compile Include="System\Net\Http\Metrics\HttpMetricsEnrichmentContext.cs" />
126+
<Compile Include="System\Net\Http\Metrics\MetricsHandler.cs" />
125127
<Compile Include="System\Net\Http\SocketsHttpHandler\SocketsHttpPlaintextStreamFilterContext.cs" />
126128
<Compile Include="$(CommonPath)DisableRuntimeMarshalling.cs"
127129
Link="Common\DisableRuntimeMarshalling.cs" />
@@ -227,7 +229,7 @@
227229
Link="Common\System\Net\DebugSafeHandleZeroOrMinusOneIsInvalid.cs" />
228230
<Compile Include="$(CommonPath)System\Threading\Tasks\TaskCompletionSourceWithCancellation.cs"
229231
Link="Common\System\Threading\Tasks\TaskCompletionSourceWithCancellation.cs" />
230-
<!-- Header support -->
232+
<!-- Header support -->
231233
<Compile Include="$(CommonPath)System\Net\Http\aspnetcore\IHttpStreamHeadersHandler.cs">
232234
<Link>Common\System\Net\Http\aspnetcore\IHttpStreamHeadersHandler.cs</Link>
233235
</Compile>
@@ -471,4 +473,4 @@
471473
<ItemGroup>
472474
<None Include="Resources\SR.resx" />
473475
</ItemGroup>
474-
</Project>
476+
</Project>

src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,6 @@ private static HttpResponseMessage ConvertResponse(HttpRequestMessage request, W
289289

290290
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
291291
{
292-
ArgumentNullException.ThrowIfNull(request);
293292
bool? allowAutoRedirect = _isAllowAutoRedirectTouched ? AllowAutoRedirect : null;
294293
#if FEATURE_WASM_THREADS
295294
return JSHost.CurrentOrMainJSSynchronizationContext.Send(() =>

src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/SocketsHttpHandler.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Threading.Tasks;
1010
using System.Diagnostics.CodeAnalysis;
1111
using System.Diagnostics;
12+
using System.Diagnostics.Metrics;
1213

1314
namespace System.Net.Http
1415
{
@@ -91,6 +92,13 @@ public int MaxResponseDrainSize
9192
set => throw new PlatformNotSupportedException();
9293
}
9394

95+
[CLSCompliant(false)]
96+
public IMeterFactory? MeterFactory
97+
{
98+
get => throw new PlatformNotSupportedException();
99+
set => throw new PlatformNotSupportedException();
100+
}
101+
94102
public TimeSpan ResponseDrainTimeout
95103
{
96104
get => throw new PlatformNotSupportedException();

src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ internal override ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage re
7878
{
7979
if (IsEnabled())
8080
{
81-
ArgumentNullException.ThrowIfNull(request);
8281
return SendAsyncCore(request, async, cancellationToken);
8382
}
8483
else

src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.AnyMobile.cs

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
54
using System.Collections.Concurrent;
65
using System.Collections.Generic;
76
using System.Diagnostics;
8-
using System.Diagnostics.CodeAnalysis;
7+
using System.Diagnostics.Metrics;
98
using System.Globalization;
9+
using System.Net.Http.Metrics;
1010
using System.Net.Security;
1111
using System.Reflection;
1212
using System.Runtime.ExceptionServices;
@@ -21,37 +21,28 @@ namespace System.Net.Http
2121
public partial class HttpClientHandler : HttpMessageHandler
2222
{
2323
private readonly SocketsHttpHandler? _socketHandler;
24-
private readonly DiagnosticsHandler? _diagnosticsHandler;
25-
2624
private readonly HttpMessageHandler? _nativeHandler;
25+
private MetricsHandler? _metricsHandler;
2726

2827
private static readonly ConcurrentDictionary<string, MethodInfo?> s_cachedMethods =
2928
new ConcurrentDictionary<string, MethodInfo?>();
3029

30+
private IMeterFactory? _meterFactory;
3131
private ClientCertificateOption _clientCertificateOptions;
3232

3333
private volatile bool _disposed;
3434

3535
public HttpClientHandler()
3636
{
37-
HttpMessageHandler handler;
38-
3937
if (IsNativeHandlerEnabled)
4038
{
4139
_nativeHandler = CreateNativeHandler();
42-
handler = _nativeHandler;
4340
}
4441
else
4542
{
4643
_socketHandler = new SocketsHttpHandler();
47-
handler = _socketHandler;
4844
ClientCertificateOptions = ClientCertificateOption.Manual;
4945
}
50-
51-
if (DiagnosticsHandler.IsGloballyEnabled())
52-
{
53-
_diagnosticsHandler = new DiagnosticsHandler(handler, DistributedContextPropagator.Current);
54-
}
5546
}
5647

5748
protected override void Dispose(bool disposing)
@@ -73,6 +64,21 @@ protected override void Dispose(bool disposing)
7364
base.Dispose(disposing);
7465
}
7566

67+
[CLSCompliant(false)]
68+
public IMeterFactory? MeterFactory
69+
{
70+
get => _meterFactory;
71+
set
72+
{
73+
ObjectDisposedException.ThrowIf(_disposed, this);
74+
if (_metricsHandler != null)
75+
{
76+
throw new InvalidOperationException(SR.net_http_operation_started);
77+
}
78+
_meterFactory = value;
79+
}
80+
}
81+
7682
[UnsupportedOSPlatform("browser")]
7783
public bool UseCookies
7884
{
@@ -713,19 +719,9 @@ protected internal override HttpResponseMessage Send(HttpRequestMessage request,
713719
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
714720
CancellationToken cancellationToken)
715721
{
716-
if (DiagnosticsHandler.IsGloballyEnabled() && _diagnosticsHandler != null)
717-
{
718-
return _diagnosticsHandler!.SendAsync(request, cancellationToken);
719-
}
720-
721-
if (IsNativeHandlerEnabled)
722-
{
723-
return _nativeHandler!.SendAsync(request, cancellationToken);
724-
}
725-
else
726-
{
727-
return _socketHandler!.SendAsync(request, cancellationToken);
728-
}
722+
ArgumentNullException.ThrowIfNull(request);
723+
MetricsHandler handler = _metricsHandler ?? SetupHandlerChain();
724+
return handler.SendAsync(request, cancellationToken);
729725
}
730726

731727
// lazy-load the validator func so it can be trimmed by the ILLinker if it isn't used.
@@ -741,6 +737,23 @@ protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessa
741737
}
742738
}
743739

740+
private MetricsHandler SetupHandlerChain()
741+
{
742+
HttpMessageHandler handler = IsNativeHandlerEnabled ? _nativeHandler! : _socketHandler!;
743+
if (DiagnosticsHandler.IsGloballyEnabled())
744+
{
745+
handler = new DiagnosticsHandler(handler, DistributedContextPropagator.Current);
746+
}
747+
MetricsHandler metricsHandler = new MetricsHandler(handler, _meterFactory);
748+
749+
// Ensure a single handler is used for all requests.
750+
if (Interlocked.CompareExchange(ref _metricsHandler, metricsHandler, null) != null)
751+
{
752+
handler.Dispose();
753+
}
754+
return _metricsHandler;
755+
}
756+
744757
private void ThrowForModifiedManagedSslOptionsIfStarted()
745758
{
746759
// Hack to trigger an InvalidOperationException if a property that's stored on

src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
using System.Security.Cryptography.X509Certificates;
1010
using System.Threading;
1111
using System.Threading.Tasks;
12-
using System.Diagnostics;
12+
using System.Diagnostics.Metrics;
1313
#if TARGET_BROWSER
14+
using System.Diagnostics;
15+
using System.Net.Http.Metrics;
1416
using HttpHandlerType = System.Net.Http.BrowserHttpHandler;
1517
#else
1618
using HttpHandlerType = System.Net.Http.SocketsHttpHandler;
@@ -23,7 +25,33 @@ public partial class HttpClientHandler : HttpMessageHandler
2325
private readonly HttpHandlerType _underlyingHandler;
2426

2527
#if TARGET_BROWSER
26-
private HttpMessageHandler Handler { get; }
28+
private IMeterFactory? _meterFactory;
29+
private MetricsHandler? _metricsHandler;
30+
31+
private MetricsHandler Handler
32+
{
33+
get
34+
{
35+
if (_metricsHandler != null)
36+
{
37+
return _metricsHandler;
38+
}
39+
40+
HttpMessageHandler handler = _underlyingHandler;
41+
if (DiagnosticsHandler.IsGloballyEnabled())
42+
{
43+
handler = new DiagnosticsHandler(handler, DistributedContextPropagator.Current);
44+
}
45+
MetricsHandler metricsHandler = new MetricsHandler(handler, _meterFactory);
46+
47+
// Ensure a single handler is used for all requests.
48+
if (Interlocked.CompareExchange(ref _metricsHandler, metricsHandler, null) != null)
49+
{
50+
metricsHandler.Dispose();
51+
}
52+
return _metricsHandler;
53+
}
54+
}
2755
#else
2856
private HttpHandlerType Handler => _underlyingHandler;
2957
#endif
@@ -34,14 +62,6 @@ public HttpClientHandler()
3462
{
3563
_underlyingHandler = new HttpHandlerType();
3664

37-
#if TARGET_BROWSER
38-
Handler = _underlyingHandler;
39-
if (DiagnosticsHandler.IsGloballyEnabled())
40-
{
41-
Handler = new DiagnosticsHandler(Handler, DistributedContextPropagator.Current);
42-
}
43-
#endif
44-
4565
ClientCertificateOptions = ClientCertificateOption.Manual;
4666
}
4767

@@ -60,6 +80,33 @@ protected override void Dispose(bool disposing)
6080
public virtual bool SupportsProxy => HttpHandlerType.SupportsProxy;
6181
public virtual bool SupportsRedirectConfiguration => HttpHandlerType.SupportsRedirectConfiguration;
6282

83+
/// <summary>
84+
/// Gets or sets the <see cref="IMeterFactory"/> to create a custom <see cref="Meter"/> for the <see cref="HttpClientHandler"/> instance.
85+
/// </summary>
86+
/// <remarks>
87+
/// When <see cref="MeterFactory"/> is set to a non-<see langword="null"/> value, all metrics emitted by the <see cref="HttpClientHandler"/> instance
88+
/// will be recorded using the <see cref="Meter"/> provided by the <see cref="IMeterFactory"/>.
89+
/// </remarks>
90+
[CLSCompliant(false)]
91+
public IMeterFactory? MeterFactory
92+
{
93+
#if TARGET_BROWSER
94+
get => _meterFactory;
95+
set
96+
{
97+
ObjectDisposedException.ThrowIf(_disposed, this);
98+
if (_metricsHandler != null)
99+
{
100+
throw new InvalidOperationException(SR.net_http_operation_started);
101+
}
102+
_meterFactory = value;
103+
}
104+
#else
105+
get => _underlyingHandler.MeterFactory;
106+
set => _underlyingHandler.MeterFactory = value;
107+
#endif
108+
}
109+
63110
[UnsupportedOSPlatform("browser")]
64111
public bool UseCookies
65112
{
@@ -296,11 +343,21 @@ public SslProtocols SslProtocols
296343
[UnsupportedOSPlatform("browser")]
297344
//[UnsupportedOSPlatform("ios")]
298345
//[UnsupportedOSPlatform("tvos")]
299-
protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) =>
300-
Handler.Send(request, cancellationToken);
346+
protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
347+
{
348+
#if TARGET_BROWSER
349+
throw new PlatformNotSupportedException();
350+
#else
351+
ArgumentNullException.ThrowIfNull(request);
352+
return Handler.Send(request, cancellationToken);
353+
#endif
354+
}
301355

302-
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
303-
Handler.SendAsync(request, cancellationToken);
356+
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
357+
{
358+
ArgumentNullException.ThrowIfNull(request);
359+
return Handler.SendAsync(request, cancellationToken);
360+
}
304361

305362
// lazy-load the validator func so it can be trimmed by the ILLinker if it isn't used.
306363
private static Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool>? s_dangerousAcceptAnyServerCertificateValidator;

src/libraries/System.Net.Http/src/System/Net/Http/HttpMessageInvoker.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.IO;
5-
using System.Net.Http.Headers;
64
using System.Runtime.Versioning;
75
using System.Threading;
86
using System.Threading.Tasks;
@@ -124,7 +122,6 @@ protected virtual void Dispose(bool disposing)
124122
if (disposing && !_disposed)
125123
{
126124
_disposed = true;
127-
128125
if (_disposeHandler)
129126
{
130127
_handler.Dispose();

src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestMessage.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class HttpRequestMessage : IDisposable
3030
private Version _version;
3131
private HttpVersionPolicy _versionPolicy;
3232
private HttpContent? _content;
33-
private HttpRequestOptions? _options;
33+
internal HttpRequestOptions? _options;
3434

3535
public Version Version
3636
{

0 commit comments

Comments
 (0)