當前位置: 妍妍網 > 碼農

【譯】.NET 8 網路改進(二)

2024-02-13碼農

點選上方「DotNet NB」 關註公眾號

1

/

D o t N e t N B 224

原文 | Máňa,Natalia Kondratyeva

轉譯 | 鄭子銘

修改 HttpClient 日誌記錄

自訂(甚至簡單地關閉)HttpClientFactory 日誌記錄是長期請求的功能之一 (dotnet/runtime#77312)。

舊日誌記錄概述

HttpClientFactory 添加的預設(「舊」)日誌記錄非常詳細,每個請求發出 8 條日誌訊息:

  1. 使用請求 URI 啟動通知 — 在透過委托處理常式管道傳播之前;

  2. 請求檔頭 - 在處理常式管道之前;

  3. 使用請求 URI 啟動通知 — 在處理常式管道之後;

  4. 請求檔頭——處理常式管道之後;

  5. 在透過委托處理常式管道將響應傳播回之前,停止通知已用時間;

  6. 響應頭——在傳播回響應之前;

  7. 停止通知並顯示經過的時間——在傳播回響應之後;

  8. 響應檔頭 - 將響應傳播回來之後。

這可以用下圖來說明。在此圖和下圖中,* 和 [...] 表示日誌記錄事件(在預設實作中,日誌訊息被寫入 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--------
<-- PrimaryHandler <--
* [Stop notification] // "Received HTTP response ..." (5)
* [Response headers] // "Response Headers: ..." (6)
<-- AdditionalHandler #N <--
<-- .... <--
<-- AdditionalHandler #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]
StartprocessingHTTPrequestGEThttps://httpbin.org/get
trce:System.Net.Http.HttpClient.test.LogicalHandler[102]
Request Headers:
....
info:System.Net.Http.HttpClient.test.ClientHandler[100]
SendingHTTPrequestGEThttps://httpbin.org/get
trce:System.Net.Http.HttpClient.test.ClientHandler[102]
Request Headers:
....
info:System.Net.Http.HttpClient.test.ClientHandler[101]
ReceivedHTTPresponseheadersafter581.2898ms-200
trce:System.Net.Http.HttpClient.test.ClientHandler[103]
Response Headers:
....
info:System.Net.Http.HttpClient.test.LogicalHandler[101]
EndprocessingHTTPrequestafter618.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 classSimpleConsoleLogger : IHttpClientLogger
{
publicobject? LogRequestStart(HttpRequestMessage request) => null;
publicvoidLogRequestStop(object? ctx, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed)
=> Console.WriteLine($"{request.Method} {request.RequestUri?.AbsoluteUri} - {(int)response.StatusCode} {response.StatusCode} in {elapsed.TotalMilliseconds}ms");
publicvoidLogRequestFailed(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(newbyte[] { 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 classRequestIdLogger : IHttpClientLogger
{
private readonly ILogger _log;
publicRequestIdLogger(ILogger<RequestIdLogger> log)
{
_log = log;
}
privatestatic readonly Action<ILogger, Guid, string?, Exception?> _requestStart =
LoggerMessage.Define<Guid, string?>(
LogLevel.Information,
EventIds.RequestStart,
"Request Id={RequestId} ({Host}) started");
privatestatic readonly Action<ILogger, Guid, double, Exception?> _requestStop =
LoggerMessage.Define<Guid, double>(
LogLevel.Information,
EventIds.RequestStop,
"Request Id={RequestId} succeeded in {elapsed}ms");
privatestatic readonly Action<ILogger, Guid, Exception?> _requestFailed =
LoggerMessage.Define<Guid>(
LogLevel.Error,
EventIds.RequestFailed,
"Request Id={RequestId} FAILED");
public object? LogRequestStart(HttpRequestMessage request)
{
varctx=newContext(Guid.NewGuid());
_requestStart(_log, ctx.RequestId, request.RequestUri?.Host, null);
return ctx;
}
publicvoidLogRequestStop(object? ctx, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed)
=> _requestStop(_log, ((Context)ctx!).RequestId, elapsed.TotalMilliseconds, null);
publicvoidLogRequestFailed(object? ctx, HttpRequestMessage request, HttpResponseMessage? response, Exception e, TimeSpan elapsed)
=> _requestFailed(_log, ((Context)ctx!).RequestId, null);
publicstatic classEventIds
{
publicstatic readonly EventIdRequestStart=new(1, "RequestStart");
publicstatic readonly EventIdRequestStop=new(2, "RequestStop");
publicstatic readonly EventIdRequestFailed=new(3, "RequestFailed");
}
recordContext(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 in530.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 in83.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 in162.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" <--
    <-- RetryHandler <--
    <-- AdditionalHandlers #(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()]
    <-- RetryHandler <--
    <-- AdditionalHandlers #(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