【译】.NET 8 网络改进(二)
原文 | Máňa,Natalia Kondratyeva
翻译 | 郑子铭
修改 HttpClient 日志记录
自定义(甚至简单地关闭)HttpClientFactory 日志记录是长期请求的功能之一 (dotnet/runtime#77312)。
旧日志记录概述
HttpClientFactory 添加的默认(“旧”)日志记录非常详细,每个请求发出 8 条日志消息:
- 使用请求 URI 启动通知 — 在通过委托处理程序管道传播之前;
- 请求标头 - 在处理程序管道之前;
- 使用请求 URI 启动通知 — 在处理程序管道之后;
- 请求标头——处理程序管道之后;
- 在通过委托处理程序管道将响应传播回之前,停止通知已用时间;
- 响应头——在传播回响应之前;
- 停止通知并显示经过的时间——在传播回响应之后;
- 响应标头 - 将响应传播回来之后。
这可以用下图来说明。在此图和下图中,* 和 [...] 表示日志记录事件(在默认实现中,日志消息被写入 ILogger),--> 表示通过应用程序层和传输层的数据流。
Request -->
* [Start notification] // "Start processing HTTP request ..." (1)
* [Request headers] // "Request Headers: ..." (2)
--> Additional Handler #1 -->
--> .... -->
--> Additional Handler #N -->
* [Start notification] // "Sending HTTP request ..." (3)
* [Request headers] // "Request Headers: ..." (4)
--> Primary Handler -->
--------Transport--layer------->
// Server sends response
<-------Transport--layer--------
<-- Primary Handler <--
* [Stop notification] // "Received HTTP response ..." (5)
* [Response headers] // "Response Headers: ..." (6)
<-- Additional Handler #N <--
<-- .... <--
<-- Additional Handler #1 <--
* [Stop notification] // "End processing HTTP request ..." (7)
* [Response headers] // "Response Headers: ..." (8)
Response <--
默认 HttpClientFactory 日志记录的控制台输出如下所示:
var client = _httpClientFactory.CreateClient();
await client.GetAsync("https://httpbin.org/get");
info: System.Net.Http.HttpClient.test.LogicalHandler[100]
Start processing HTTP request GET https://httpbin.org/get
trce: System.Net.Http.HttpClient.test.LogicalHandler[102]
Request Headers:
....
info: System.Net.Http.HttpClient.test.ClientHandler[100]
Sending HTTP request GET https://httpbin.org/get
trce: System.Net.Http.HttpClient.test.ClientHandler[102]
Request Headers:
....
info: System.Net.Http.HttpClient.test.ClientHandler[101]
Received HTTP response headers after 581.2898ms - 200
trce: System.Net.Http.HttpClient.test.ClientHandler[103]
Response Headers:
....
info: System.Net.Http.HttpClient.test.LogicalHandler[101]
End processing HTTP request after 618.9736ms - 200
trce: System.Net.Http.HttpClient.test.LogicalHandler[103]
Response Headers:
....
请注意,为了查看跟踪级别消息,您需要在全局日志记录配置文件中或通过 SetMinimumLevel(LogLevel.Trace) 选择加入该消息。但即使只考虑信息性消息,“旧”日志记录每个请求仍然有 4 条消息。
要删除默认(或之前添加的)日志记录,您可以使用新的RemoveAllLoggers() 扩展方法。它与上面“为所有客户端设置默认值”部分中描述的ConfigureHttpClientDefaults API 结合起来特别强大。这样,您可以在一行中删除所有客户端的“旧”日志记录:
services.ConfigureHttpClientDefaults(b => b.RemoveAllLoggers()); // remove HttpClientFactory default logging for all clients
如果您需要恢复“旧”日志记录,例如对于特定客户端,您可以使用 AddDefaultLogger() 来执行此操作。
添加自定义日志记录
除了能够删除“旧”日志记录之外,新的 HttpClientFactory API 还允许您完全自定义日志记录。您可以指定当 HttpClient 启动请求、接收响应或引发异常时记录的内容和方式。
如果您选择这样做,您可以同时添加多个自定义记录器 - 例如,控制台和 ETW 记录器,或“包装”和“不包装”记录器。由于其附加性质,您可能需要事先显式删除默认的“旧”日志记录。
要添加自定义日志记录,您需要实现 IHttpClientLogger 接口,然后使用 AddLogger 将自定义记录器添加到客户端。请注意,日志记录实现不应引发任何异常,否则可能会中断请求执行。
登记:
services.AddSingleton<SimpleConsoleLogger>(); // register the logger in DI
services.AddHttpClient("foo") // add a client
.RemoveAllLoggers() // remove previous logging
.AddLogger<SimpleConsoleLogger>(); // add the custom logger
示例记录器实现:
// outputs one line per request to console
public class SimpleConsoleLogger : IHttpClientLogger
{
public object? LogRequestStart(HttpRequestMessage request) => null;
public void LogRequestStop(object? ctx, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed)
=> Console.WriteLine($"{request.Method} {request.RequestUri?.AbsoluteUri} - {(int)response.StatusCode} {response.StatusCode} in {elapsed.TotalMilliseconds}ms");
public void LogRequestFailed(object? ctx, HttpRequestMessage request, HttpResponseMessage? response, Exception e, TimeSpan elapsed)
=> Console.WriteLine($"{request.Method} {request.RequestUri?.AbsoluteUri} - Exception {e.GetType().FullName}: {e.Message}");
}
示例输出:
var client = _httpClientFactory.CreateClient("foo");
await client.GetAsync("https://httpbin.org/get");
await client.PostAsync("https://httpbin.org/post", new ByteArrayContent(new byte[] { 42 }));
await client.GetAsync("http://httpbin.org/status/500");
await client.GetAsync("http://localhost:1234");
GET https://httpbin.org/get - 200 OK in 393.2039ms
POST https://httpbin.org/post - 200 OK in 95.524ms
GET https://httpbin.org/status/500 - 500 InternalServerError in 99.5025ms
GET http://localhost:1234/ - Exception System.Net.Http.HttpRequestException: No connection could be made because the target machine actively refused it. (localhost:1234)
请求上下文对象
上下文对象可用于将 LogRequestStart 调用与相应的 LogRequestStop 调用相匹配,以将数据从一个调用传递到另一个调用。 Context 对象由 LogRequestStart 生成,然后传递回 LogRequestStop。这可以是属性包或保存必要数据的任何其他对象。
如果不需要上下文对象,实现可以从 LogRequestStart 返回 null。
以下示例显示了如何使用上下文对象来传递自定义请求标识符。
public class RequestIdLogger : IHttpClientLogger
{
private readonly ILogger _log;
public RequestIdLogger(ILogger<RequestIdLogger> log)
{
_log = log;
}
private static readonly Action<ILogger, Guid, string?, Exception?> _requestStart =
LoggerMessage.Define<Guid, string?>(
LogLevel.Information,
EventIds.RequestStart,
"Request Id={RequestId} ({Host}) started");
private static readonly Action<ILogger, Guid, double, Exception?> _requestStop =
LoggerMessage.Define<Guid, double>(
LogLevel.Information,
EventIds.RequestStop,
"Request Id={RequestId} succeeded in {elapsed}ms");
private static readonly Action<ILogger, Guid, Exception?> _requestFailed =
LoggerMessage.Define<Guid>(
LogLevel.Error,
EventIds.RequestFailed,
"Request Id={RequestId} FAILED");
public object? LogRequestStart(HttpRequestMessage request)
{
var ctx = new Context(Guid.NewGuid());
_requestStart(_log, ctx.RequestId, request.RequestUri?.Host, null);
return ctx;
}
public void LogRequestStop(object? ctx, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed)
=> _requestStop(_log, ((Context)ctx!).RequestId, elapsed.TotalMilliseconds, null);
public void LogRequestFailed(object? ctx, HttpRequestMessage request, HttpResponseMessage? response, Exception e, TimeSpan elapsed)
=> _requestFailed(_log, ((Context)ctx!).RequestId, null);
public static class EventIds
{
public static readonly EventId RequestStart = new(1, "RequestStart");
public static readonly EventId RequestStop = new(2, "RequestStop");
public static readonly EventId RequestFailed = new(3, "RequestFailed");
}
record Context(Guid RequestId);
}
info: RequestIdLogger[1]
Request Id=d0d63b84-cd67-4d21-ae9a-b63d26dfde50 (httpbin.org) started
info: RequestIdLogger[2]
Request Id=d0d63b84-cd67-4d21-ae9a-b63d26dfde50 succeeded in 530.1664ms
info: RequestIdLogger[1]
Request Id=09403213-dd3a-4101-88e8-db8ab19e1eeb (httpbin.org) started
info: RequestIdLogger[2]
Request Id=09403213-dd3a-4101-88e8-db8ab19e1eeb succeeded in 83.2484ms
info: RequestIdLogger[1]
Request Id=254e49bd-f640-4c56-b62f-5de678eca129 (httpbin.org) started
info: RequestIdLogger[2]
Request Id=254e49bd-f640-4c56-b62f-5de678eca129 succeeded in 162.7776ms
info: RequestIdLogger[1]
Request Id=e25ccb08-b97e-400d-b42b-b09d6c42adec (localhost) started
fail: RequestIdLogger[3]
Request Id=e25ccb08-b97e-400d-b42b-b09d6c42adec FAILED
避免从内容流中读取
例如,如果您打算阅读和记录请求和响应内容,请注意,它可能会对最终用户体验产生不利的副作用并导致错误。例如,请求内容可能在发送之前被消耗,或者巨大的响应内容可能最终被缓冲在内存中。此外,在 .NET 7 之前,访问标头不是线程安全的,可能会导致错误和意外行为。
谨慎使用异步日志记录
我们期望同步 IHttpClientLogger 接口适用于绝大多数自定义日志记录用例。出于性能原因,建议不要在日志记录中使用异步。但是,如果严格要求日志记录中的异步访问,您可以实现异步版本 IHttpClientAsyncLogger。它派生自 IHttpClientLogger,因此可以使用相同的 AddLogger API 进行注册。
请注意,在这种情况下,还应该实现日志记录方法的同步对应项,特别是如果该实现是面向 .NET Standard 或 .NET 5+ 的库的一部分。同步对应项是从同步 HttpClient.Send 方法调用的;即使 .NET Standard 表面不包含它们,.NET Standard 库也可以在 .NET 5+ 应用程序中使用,因此最终用户可以访问同步 HttpClient.Send 方法。
包装和不包装记录仪
当您添加记录器时,您可以显式设置wrapHandlersPipeline参数来指定记录器是否将被
- 包装处理程序管道(添加到管道的顶部,对应于上面旧日志记录概述部分中的 1、2、7 和 8 号消息)
Request -->
* [LogRequestStart()] // wrapHandlersPipeline=TRUE
--> Additional Handlers #1..N --> // handlers pipeline
--> Primary Handler -->
--------Transport--layer--------
<-- Primary Handler <--
<-- Additional Handlers #N..1 <-- // handlers pipeline
* [LogRequestStop()] // wrapHandlersPipeline=TRUE
Response <--
- 或者,不包装处理程序管道(添加到底部,对应于上面旧日志记录概述部分中的第 3、4、5 和 6 号消息)。
Request -->
--> Additional Handlers #1..N --> // handlers pipeline
* [LogRequestStart()] // wrapHandlersPipeline=FALSE
--> Primary Handler -->
--------Transport--layer--------
<-- Primary Handler <--
* [LogRequestStop()] // wrapHandlersPipeline=FALSE
<-- Additional Handlers #N..1 <-- // handlers pipeline
Response <--
默认情况下,记录器添加为不包装。
在将重试处理程序添加到管道的情况下(例如 Polly 或某些重试的自定义实现),包装和不包装管道之间的区别最为显着。在这种情况下,包装记录器(位于顶部)将记录有关单个成功请求的消息,记录的经过时间将是从用户发起请求到收到响应的总时间。非包装记录器(位于底部)将记录每次重试迭代,第一个可能记录异常或不成功的状态代码,最后一个记录成功。每种情况下消耗的时间都是纯粹在主处理程序中花费的时间(实际在网络上发送请求的处理程序,例如 HttpClientHandler)。
这可以用下图来说明:
- 包装案例 (wrapHandlersPipeline=TRUE)
Request -->
* [LogRequestStart()]
--> Additional Handlers #1..(N-1) -->
--> Retry Handler -->
--> //1
--> Primary Handler -->
<-- "503 Service Unavailable" <--
--> //2
--> Primary Handler ->
<-- "503 Service Unavailable" <--
--> //3
--> Primary Handler -->
<-- "200 OK" <--
<-- Retry Handler <--
<-- Additional Handlers #(N-1)..1 <--
* [LogRequestStop()]
Response <--
info: Example.CustomLogger.Wrapping[1]
GET https://consoto.com/
info: Example.CustomLogger.Wrapping[2]
200 OK - 809.2135ms
- 不包装案例 (wrapHandlersPipeline=FALSE)
Request -->
--> Additional Handlers #1..(N-1) -->
--> Retry Handler -->
--> //1
* [LogRequestStart()]
--> Primary Handler -->
<-- "503 Service Unavailable" <--
* [LogRequestStop()]
--> //2
* [LogRequestStart()]
--> Primary Handler -->
<-- "503 Service Unavailable" <--
* [LogRequestStop()]
--> //3
* [LogRequestStart()]
--> Primary Handler -->
<-- "200 OK" <--
* [LogRequestStop()]
<-- Retry Handler <--
<-- Additional Handlers #(N-1)..1 <--
Response <--
info: Example.CustomLogger.NotWrapping[1]
GET https://consoto.com/
info: Example.CustomLogger.NotWrapping[2]
503 Service Unavailable - 98.613ms
info: Example.CustomLogger.NotWrapping[1]
GET https://consoto.com/
info: Example.CustomLogger.NotWrapping[2]
503 Service Unavailable - 96.1932ms
info: Example.CustomLogger.NotWrapping[1]
GET https://consoto.com/
info: Example.CustomLogger.NotWrapping[2]
200 OK - 579.2133ms
原文链接
.NET 8 Networking Improvements
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。
如有任何疑问,请与我联系 (MingsonZheng@outlook.com)
【译】.NET 8 网络改进(二)的更多相关文章
- 【译】.NET 6 网络改进
原文 | Máňa Píchová 翻译 | 郑子铭 对于 .NET 的每个新版本,我们都希望发布一篇博客文章,重点介绍网络的一些变化和改进.在这篇文章中,我很高兴谈论 .NET 6 中的变化. 这篇 ...
- 通用js函数集锦<来源于网络> 【二】
通用js函数集锦<来源于网络> [二] 1.数组方法集2.cookie方法集3.url方法集4.正则表达式方法集5.字符串方法集6.加密方法集7.日期方法集8.浏览器检测方法集9.json ...
- 34 网络相关函数(二)——live555源码阅读(四)网络
34 网络相关函数(二)——live555源码阅读(四)网络 34 网络相关函数(二)——live555源码阅读(四)网络 2)socketErr 套接口错误 3)groupsockPriv函数 4) ...
- Android系列之网络(二)----HTTP请求头与响应头
[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/ ...
- Linux网络编程(二)
Linux网络编程(二) 使用多进程实现服务器并发访问. 采用多进程的方式实现服务器的并发访问的经典范例. 程序实现功能: 1.客户端从标准输入读入一行文字,发送到服务器. 2.服务器接收到客户端发来 ...
- Python for Infomatics 第12章 网络编程二(译)
注:文章原文为Dr. Charles Severance 的 <Python for Informatics>.文中代码用3.4版改写,并在本机测试通过. 12.3 用HTTP协议获取一张 ...
- 探究UE4网络系列(二)、UE4网络核心类分析
转载请标明出处:http://www.cnblogs.com/zblade/ 一.概要 前面分析了网络核心的基础类Socket/BSDSocket/SocketSubsystem/SocketSubs ...
- C#网络编程二:Socket编程
一:什么是SOCKET socket的英文原义是"孔"或"插座".作为进程通信机制,取后一种意思.通常也称作"套接字",用于描述IP地址和端 ...
- CDN网络(二)之配置和优化CDN核心缓存软件--squid
前言 squid是众多CDN厂商使用的核心缓存软件,都在已有的基础上进行二次开发.在部署squid的时候,建议遵循下面的规范. 1. 使用大内存服务器 对于热点文件,我们让squid用内存缓存,这样大 ...
- DNN网络(二)反向传播算法
本文摘自: https://www.cnblogs.com/pinard/p/6422831.html http://www.cnblogs.com/charlotte77/p/5629865.htm ...
随机推荐
- java进阶(28)--Map集合
一.Map简介: 1.Map与collection没有继承关系 2.Map集合以key与value的方式存储数据 二.常用方法: 1.void clear():清空Map集合
- Flutter 3 发布了(文末推荐一个免费的在线Flutter学习教程)
翻译自 Tim Sneath 2022年5月12日的文章 <Introducing Flutter 3> 作者 : Tim Sneath 翻译 : 沙漠尽头的狼(谷歌翻译加持) 链接 : ...
- 用C# WPF简单实现仪表控件
时间如流水,只能流去不流回! 点赞再看,养成习惯,这是您给我创作的动力! 本文 Dotnet9 https://dotnet9.com 已收录,站长乐于分享dotnet相关技术,比如Winform.W ...
- 03-ASIC和FPGA中的时钟结构
1 ASIC 中时钟的结构 ASIC电路中的时钟的结构.这是一个非常典型的MCU的时钟结构图.它的时钟结构和功能的划分.首先,我们通过外部振荡器发送了一个8MHz的时钟给PLL,经过分分频和倍频产生更 ...
- android studio 如何把依赖导出成 jar
反编译工具 dex-tools-2.1-SNAPSHOT 第一步 用一个普通的app工程,引用所有的库,然后生成apk文件 第二步 把apk文件,改扩展名为zip,解压后,里面有几个*.dex文件,拷 ...
- Go-用本地时间解析时间字符串
Go-用本地时间解析时间字符串 1. 指定本地时区 const ( gLocalTimeZone = "Asia/Shanghai" ) 2. 加载本地时区 var ( gLoca ...
- [转帖]SQL Server 中如何移动tempdb到新的位置
https://www.cnblogs.com/OpenCoder/p/10322904.html 操作步骤:1.检查tempdb的逻辑名字和它的存在位置.可以使用下面语句: SELECT name, ...
- [转帖]TiDB系统调参实战经验
https://tidb.net/blog/c9466c40#TiDB%E7%B3%BB%E7%BB%9F%E8%B0%83%E5%8F%82%E5%AE%9E%E6%88%98%E7%BB%8F%E ...
- [转帖]CPU Turbo&Cstate&Pstate简介
https://www.jianshu.com/p/eaefd1eb0ac6 测试环境 CPU 1 Intel(R) Xeon(R) Gold 5218 CPU @ 2.30GHz 16 3130 3 ...
- qperf 简要总结 - 延迟与带宽信息
总结 同一个虚拟机: 延迟: 12us 带宽: 6GB/S 同一个物理机上面的虚拟机: 延迟: 50us-100us 带宽: 1.2GB/S 同一个交换机上面的虚拟机: 延迟: 60us 带宽: 12 ...