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

原文 | 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--------
                <-- 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/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。

如有任何疑問,請與我聯繫 ([email protected])

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章