在.NET 5中搭配HttpClientFactory使用HttpClient时如何记录连接池信息?
实现HttpClient请求全链路时序耗时采集方案
要获取类似Chrome的请求各阶段耗时(DNS解析、TCP连接、TLS握手等),可以通过以下两种方案实现,两种方案均兼容你现有.NET 5 + HttpClientFactory + NLog的技术栈,无需修改现有日志配置:
方案1:使用DiagnosticSource全局监听(无侵入,适用于所有HttpClient实例)
.NET 内置的HttpClient会通过DiagnosticSource发布全生命周期的事件,无需修改现有HttpClient注册逻辑,只需订阅对应事件即可采集各阶段耗时:
- 实现诊断观察者类
using System.Diagnostics; using System.Net.Http; public class HttpClientDiagnosticObserver : IObserver<DiagnosticListener> { private readonly ILogger<HttpClientDiagnosticObserver> _logger; private readonly AsyncLocal<HttpRequestTiming> _timingContext = new(); public HttpClientDiagnosticObserver(ILogger<HttpClientDiagnosticObserver> logger) { _logger = logger; } public void OnCompleted() { } public void OnError(Exception error) { } public void OnNext(DiagnosticListener listener) { if (listener.Name == "HttpHandlerDiagnosticListener") { listener.Subscribe(new HttpHandlerEventObserver(_logger, _timingContext)); } } } public class HttpHandlerEventObserver : IObserver<KeyValuePair<string, object>> { private readonly ILogger _logger; private readonly AsyncLocal<HttpRequestTiming> _timingContext; public HttpHandlerEventObserver(ILogger logger, AsyncLocal<HttpRequestTiming> timingContext) { _logger = logger; _timingContext = timingContext; } public void OnCompleted() { } public void OnError(Exception error) { } public void OnNext(KeyValuePair<string, object> value) { switch (value.Key) { case "System.Net.Http.HttpRequestOut.Start": _timingContext.Value = new HttpRequestTiming { Request = (HttpRequestMessage)value.Value.GetType().GetProperty("Request")?.GetValue(value.Value), StartTime = Stopwatch.GetTimestamp() }; break; case "System.Net.Http.DnsResolutionStart": if (_timingContext.Value != null) _timingContext.Value.DnsStart = Stopwatch.GetTimestamp(); break; case "System.Net.Http.DnsResolutionStop": if (_timingContext.Value != null) _timingContext.Value.DnsDuration = Stopwatch.GetElapsedTime(_timingContext.Value.DnsStart).TotalMilliseconds; break; case "System.Net.Http.TcpConnectStart": if (_timingContext.Value != null) _timingContext.Value.TcpConnectStart = Stopwatch.GetTimestamp(); break; case "System.Net.Http.TcpConnectStop": if (_timingContext.Value != null) _timingContext.Value.TcpConnectDuration = Stopwatch.GetElapsedTime(_timingContext.Value.TcpConnectStart).TotalMilliseconds; break; case "System.Net.Http.TlsHandshakeStart": if (_timingContext.Value != null) _timingContext.Value.TlsHandshakeStart = Stopwatch.GetTimestamp(); break; case "System.Net.Http.TlsHandshakeStop": if (_timingContext.Value != null) _timingContext.Value.TlsHandshakeDuration = Stopwatch.GetElapsedTime(_timingContext.Value.TlsHandshakeStart).TotalMilliseconds; break; case "System.Net.Http.HttpRequestOut.Stop": if (_timingContext.Value != null) { var totalDuration = Stopwatch.GetElapsedTime(_timingContext.Value.StartTime).TotalMilliseconds; _logger.LogInformation("请求时序详情|请求地址:{RequestUri}|DNS解析耗时:{DnsDuration}ms|TCP连接耗时:{TcpConnectDuration}ms|TLS握手耗时:{TlsDuration}ms|总耗时:{TotalDuration}ms", _timingContext.Value.Request.RequestUri, _timingContext.Value.DnsDuration, _timingContext.Value.TcpConnectDuration, _timingContext.Value.TlsHandshakeDuration, totalDuration); _timingContext.Value = null; } break; } } } public class HttpRequestTiming { public HttpRequestMessage Request { get; set; } public long StartTime { get; set; } public long DnsStart { get; set; } public double DnsDuration { get; set; } public long TcpConnectStart { get; set; } public double TcpConnectDuration { get; set; } public long TlsHandshakeStart { get; set; } public double TlsHandshakeDuration { get; set; } }
- 服务注册与启动订阅
// Startup.cs ConfigureServices 中注册观察者 services.AddSingleton<HttpClientDiagnosticObserver>(); // Startup.cs Configure 中添加全局订阅 var observer = app.ApplicationServices.GetRequiredService<HttpClientDiagnosticObserver>(); DiagnosticListener.AllListeners.Subscribe(observer);
- 替换原有HttpClientHandler为SocketsHttpHandler(兼容所有原有配置,保证事件正常触发)
services.AddHttpClient("BasicUsage", c => { // c.Timeout = TimeSpan.FromMilliseconds(30); }).ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, UseCookies = true, Credentials = CredentialCache.DefaultCredentials, SslOptions = new SslClientAuthenticationOptions { EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11, RemoteCertificateValidationCallback = (message, cert, chain, errors) => true }, MaxAutomaticRedirections = 5, CookieContainer = cookies, });
方案2:自定义DelegatingHandler(可控性强,仅针对指定HttpClient生效)
如果只需要给特定名称的HttpClient增加时序统计,可以自定义委托Handler实现:
- 实现自定义时序Handler
public class TimingDelegatingHandler : DelegatingHandler { private readonly ILogger<TimingDelegatingHandler> _logger; public TimingDelegatingHandler(ILogger<TimingDelegatingHandler> logger) { _logger = logger; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var timingInfo = new HttpRequestTiming(); request.Properties["TimingInfo"] = timingInfo; var totalStopwatch = Stopwatch.StartNew(); var response = await base.SendAsync(request, cancellationToken); totalStopwatch.Stop(); _logger.LogInformation("请求时序详情|请求地址:{RequestUri}|DNS解析耗时:{DnsDuration}ms|TCP连接耗时:{TcpConnectDuration}ms|TLS握手耗时:{TlsDuration}ms|总耗时:{TotalDuration}ms", request.RequestUri, timingInfo.DnsDuration, timingInfo.TcpConnectDuration, timingInfo.TlsHandshakeDuration, totalStopwatch.Elapsed.TotalMilliseconds); return response; } }
- 注册HttpClient时注入Handler并配置阶段回调,直接替换你原有HttpClient注册逻辑即可:
services.AddTransient<TimingDelegatingHandler>(); services.AddHttpClient("BasicUsage", c => { // c.Timeout = TimeSpan.FromMilliseconds(30); }) .AddHttpMessageHandler<TimingDelegatingHandler>() .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, UseCookies = true, Credentials = CredentialCache.DefaultCredentials, MaxAutomaticRedirections = 5, CookieContainer = cookies, // 自定义连接回调统计DNS、TCP耗时 ConnectCallback = async (context, token) => { var timingInfo = (HttpRequestTiming)context.InitialRequestMessage.Properties["TimingInfo"]; // DNS计时 var dnsStart = Stopwatch.GetTimestamp(); var addresses = await Dns.GetHostAddressesAsync(context.DnsEndPoint.Host, token); timingInfo.DnsDuration = Stopwatch.GetElapsedTime(dnsStart).TotalMilliseconds; // TCP连接计时 var tcpStart = Stopwatch.GetTimestamp(); var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); try { await socket.ConnectAsync(addresses, context.DnsEndPoint.Port, token); timingInfo.TcpConnectDuration = Stopwatch.GetElapsedTime(tcpStart).TotalMilliseconds; return new NetworkStream(socket, ownsSocket: true); } catch { socket.Dispose(); throw; } }, SslOptions = new SslClientAuthenticationOptions { EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11, RemoteCertificateValidationCallback = (message, cert, chain, errors) => true, // TLS握手计时 OnAuthenticateAsClientAsync = async (stream, targetHost, clientCertificates, serverCertificate, enabledSslProtocols, cancellationToken) => { var timingInfo = (HttpRequestTiming)((SocketsConnectionContext)stream.GetType().GetProperty("Context", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(stream)).InitialRequestMessage.Properties["TimingInfo"]; var tlsStart = Stopwatch.GetTimestamp(); await stream.AuthenticateAsClientAsync(targetHost, clientCertificates, enabledSslProtocols, false, cancellationToken); timingInfo.TlsHandshakeDuration = Stopwatch.GetElapsedTime(tlsStart).TotalMilliseconds; } } });
注意事项
- 连接复用场景下,DNS、TCP、TLS阶段仅在新建连接时产生耗时,复用已有连接的请求这三项耗时会显示为0,符合实际网络逻辑
- 两种方案采集的时序数据会直接写入你已配置的NLog链路,无需额外适配日志组件
内容的提问来源于stack exchange,提问作者vaishali
相关产品推荐
相关产品推荐

