簡體   English   中英

如何獲得實際的請求執行時間

[英]How to get actual request execution time

鑒於以下中間件:

public class RequestDurationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestDurationMiddleware> _logger;

    public RequestDurationMiddleware(RequestDelegate next, ILogger<RequestDurationMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        var watch = Stopwatch.StartNew();
        await _next.Invoke(context);
        watch.Stop();

        _logger.LogTrace("{duration}ms", watch.ElapsedMilliseconds);
    }
}

由於管道的原因,它發生在管道結束之前並記錄不同的時間:

WebApi.Middlewares.RequestDurationMiddleware 2018-01-10 15:00:16.372 -02:00 [Verbose]  382ms
Microsoft.AspNetCore.Server.Kestrel 2018-01-10 15:00:16.374 -02:00 [Debug]  Connection id ""0HLAO9CRJUV0C"" completed keep alive response.
Microsoft.AspNetCore.Hosting.Internal.WebHost 2018-01-10 15:00:16.391 -02:00 [Information]  "Request finished in 405.1196ms 400 application/json; charset=utf-8"

在這種情況下,如何從 WebHost(示例中為 405.1196ms)值捕獲實際請求執行時間? 我想將此值存儲在數據庫中或在其他地方使用它。

我覺得這個問題真的很有趣,所以我研究了一下,以弄清楚 WebHost 是如何實際測量和顯示請求時間的。 底線是:獲取這些信息既沒有好的方法,也沒有簡單的方法,也沒有很好的方法,一切都感覺像是黑客攻擊。 但是,如果您仍然感興趣,請繼續關注。

當應用程序啟動時, WebHostBuilder構造WebHost ,后者又會創建HostingApplication 這基本上是負責響應傳入請求的根組件。 當請求進來時,它是將調用中間件管道的組件。

也是創建HostingApplicationDiagnostics的組件,它允許收集有關請求處理的診斷信息。 在請求開始時, HostingApplication將調用HostingApplicationDiagnostics.BeginRequest ,在請求結束時,它將調用HostingApplicationDiagnostics.RequestEnd

毫不奇怪, HostingApplicationDiagnostics將測量請求持續時間並記錄您所看到的WebHost消息。 所以這是我們必須更仔細地檢查以找出如何獲取信息的類。

診斷對象用於報告診斷信息有兩件事:記錄器和DiagnosticListener

診斷監聽器

DiagnosticListener是一件有趣的事情:它基本上是一個通用的事件接收器,您可以在其上引發事件。 然后其他對象可以訂閱它來監聽這些事件。 所以這聽起來非常適合我們的目的!

HostingApplicationDiagnostics使用的DiagnosticListener對象由WebHost傳遞,它實際上是從依賴注入中解析的 由於它被WebHostBuilder注冊為單例,我們實際上可以從依賴注入解析監聽器並訂閱它的事件。 所以讓我們在我們的Startup這樣做:

public void ConfigureServices(IServiceCollection services)
{
    // …

    // register our observer
    services.AddSingleton<DiagnosticObserver>();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env,
    // we inject both the DiagnosticListener and our DiagnosticObserver here
    DiagnosticListener diagnosticListenerSource, DiagnosticObserver diagnosticObserver)
{
    // subscribe to the listener
    diagnosticListenerSource.Subscribe(diagnosticObserver);

    // …
}

這已經足以讓我們的DiagnosticObserver運行。 我們的觀察者需要實現IObserver<KeyValuePair<string, object>> 當事件發生時,我們將獲得一個鍵值對,其中鍵是事件的標識符,值是HostingApplicationDiagnostics傳遞的自定義對象。

但是在我們實現我們的觀察者之前,我們實際上應該看看HostingApplicationDiagnostics實際引發了什么樣的事件。

不幸的是,當請求結束時,事件即在診斷利斯特提出的只是被傳遞的結束時間戳,所以我們還需要聽時引發該事件在開始閱讀開始時間戳的要求。 但這會將狀態引入我們的觀察者,這是我們想要避免的。 此外,實際的事件名稱常量Deprecated前綴,這可能表明我們應該避免使用這些常量。

首選方法是使用與診斷觀察者也密切相關的活動 活動顯然是跟蹤出現在應用程序中的活動的狀態。 它們在某個時刻開始和停止,並且已經記錄了它們自己運行的時間。 所以我們可以讓我們的觀察者監聽活動的停止事件,以便在活動完成時得到通知:

public class DiagnosticObserver : IObserver<KeyValuePair<string, object>>
{
    private readonly ILogger<DiagnosticObserver> _logger;
    public DiagnosticObserver(ILogger<DiagnosticObserver> logger)
    {
        _logger = logger;
    }

    public void OnCompleted() { }
    public void OnError(Exception error) { }

    public void OnNext(KeyValuePair<string, object> value)
    {
        if (value.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop")
        {
            var httpContext = value.Value.GetType().GetProperty("HttpContext")?.GetValue(value.Value) as HttpContext;
            var activity = Activity.Current;

            _logger.LogWarning("Request ended for {RequestPath} in {Duration} ms",
                httpContext.Request.Path, activity.Duration.TotalMilliseconds);
        }
    }
}

不幸的是,沒有沒有缺點的解決方案......我發現這個解決方案對於並行請求非常不准確(例如,當打開一個頁面也有並行請求的圖像或腳本時)。 這可能是因為我們使用靜態Activity.Current來獲取活動。 然而,似乎並沒有真正獲得單個請求的活動的方法,例如從傳遞的鍵值對。

所以我回去再次嘗試我最初的想法,使用那些已棄用的事件。 我理解它的方式是順便說一句。 它們被棄用只是因為推薦使用活動,而不是因為它們很快就會被刪除(當然,我們正在處理實現細節和內部類,所以這些東西隨時可能改變)。 為了避免並發問題,我們需要確保將狀態存儲在 HTTP 上下文中(而不是類字段):

private const string StartTimestampKey = "DiagnosticObserver_StartTimestamp";

public void OnNext(KeyValuePair<string, object> value)
{
    if (value.Key == "Microsoft.AspNetCore.Hosting.BeginRequest")
    {
        var httpContext = (HttpContext)value.Value.GetType().GetProperty("httpContext").GetValue(value.Value);
        httpContext.Items[StartTimestampKey] = (long)value.Value.GetType().GetProperty("timestamp").GetValue(value.Value);
    }
    else if (value.Key == "Microsoft.AspNetCore.Hosting.EndRequest")
    {
        var httpContext = (HttpContext)value.Value.GetType().GetProperty("httpContext").GetValue(value.Value);
        var endTimestamp = (long)value.Value.GetType().GetProperty("timestamp").GetValue(value.Value);
        var startTimestamp = (long)httpContext.Items[StartTimestampKey];

        var duration = new TimeSpan((long)((endTimestamp - startTimestamp) * TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency));
        _logger.LogWarning("Request ended for {RequestPath} in {Duration} ms",
            httpContext.Request.Path, duration.TotalMilliseconds);
    }
}

運行它時,我們確實得到了准確的結果,我們還可以訪問 HttpContext,我們可以使用它來識別請求。 當然,這里涉及的開銷非常明顯:訪問屬性值的反射,必須在HttpContext.Items存儲信息,整個觀察者的事情……這可能不是一個非常高效的方式來做到這一點。

進一步閱讀診斷源和活動: DiagnosticSource 用戶指南活動用戶指南

日志記錄

我在上面的某個地方提到HostingApplicationDiagnostics也將信息報告給日志記錄工具。 當然:這畢竟是我們在控制台中看到的。 如果我們查看實現,我們可以看到這里已經計算了適當的持續時間。 由於這是結構化日志,我們可以使用它來獲取該信息。

所以讓我們嘗試編寫一個自定義記錄器來檢查那個確切的狀態對象,看看我們能做什么:

public class RequestDurationLogger : ILogger, ILoggerProvider
{
    public ILogger CreateLogger(string categoryName) => this;
    public void Dispose() { }
    public IDisposable BeginScope<TState>(TState state) => NullDisposable.Instance;
    public bool IsEnabled(LogLevel logLevel) => true;

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        if (state.GetType().FullName == "Microsoft.AspNetCore.Hosting.Internal.HostingRequestFinishedLog" &&
            state is IReadOnlyList<KeyValuePair<string, object>> values &&
            values.FirstOrDefault(kv => kv.Key == "ElapsedMilliseconds").Value is double milliseconds)
        {
            Console.WriteLine($"Request took {milliseconds} ms");
        }
    }

    private class NullDisposable : IDisposable
    {
        public static readonly NullDisposable Instance = new NullDisposable();
        public void Dispose() { }
    }
}

不幸的是(你現在可能很喜歡這個詞,對吧?),狀態類HostingRequestFinishedLog是內部的,所以我們不能直接使用它。 所以我們必須使用反射來識別它。 但是我們只需要它的名字,然后我們就可以從只讀列表中提取值。

現在我們需要做的就是向 Web 主機注冊該記錄器(提供程序):

WebHost.CreateDefaultBuilder(args)
    .ConfigureLogging(logging =>
    {
        logging.AddProvider(new RequestDurationLogger());
    })
    .UseStartup<Startup>()
    .Build();

這實際上就是我們能夠訪問標准日志記錄所具有的完全相同的信息所需要的全部內容。

但是,有兩個問題: 我們這里沒有 HttpContext ,因此我們無法獲取有關此持續時間實際上屬於哪個請求的信息。 正如您在HostingApplicationDiagnostics看到的HostingApplicationDiagnostics ,此日志記錄調用實際上僅在日志級別至少為Information

我們可以通過使用反射讀取私有字段_httpContext來獲取 HttpContext,但我們對日志級別無能為力。 當然,我們正在創建一個記錄器來從一個特定的日志記錄調用中獲取信息這一事實是一個超級黑客,無論如何可能不是一個好主意。

結論

所以,這一切都太可怕了。 根本沒有一種干凈的方法可以從HostingApplicationDiagnostics檢索此信息。 而且我們還必須記住,診斷內容實際上只有在啟用時才會運行。 性能關鍵應用程序可能會在某一時刻禁用它。 無論如何,將這些信息用於診斷之外的任何事情都是一個壞主意,因為它通常太脆弱了。

那么更好的解決方案是什么? 在診斷上下文之外工作的解決方案? 一個早期運行的簡單中間件 就像你已經使用過一樣。 是的,這可能不像它會從外部請求處理管道中遺漏一些路徑那樣准確,但它仍然是實際應用程序代碼准確度量 畢竟,如果我們想衡量框架性能,無論如何我們都必須從外部衡量它:作為客戶端,發出請求(就像基准測試一樣)。

順便說一句。 這也是 Stack Overflow 自己的MiniProfiler 的工作方式。 您只需盡早注冊中間件即可

您可以使用中間件進行一些更改。 我使用這樣的方法來為響應頭添加響應時間:

 public class ResponseTimeMiddleware
{
    // Name of the Response Header, Custom Headers starts with "X-"  
    private const string RESPONSE_HEADER_RESPONSE_TIME = "X-Response-Time-ms";
    // Handle to the next Middleware in the pipeline  
    private readonly RequestDelegate _next;
    public ResponseTimeMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public Task InvokeAsync(HttpContext context)
    {
        // Start the Timer using Stopwatch  
        var watch = new Stopwatch();
        watch.Start();
        context.Response.OnStarting(() => {
            // Stop the timer information and calculate the time   
            watch.Stop();
            var responseTimeForCompleteRequest = watch.ElapsedMilliseconds;
            // Add the Response time information in the Response headers.   
            context.Response.Headers[RESPONSE_HEADER_RESPONSE_TIME] = responseTimeForCompleteRequest.ToString();
            return Task.CompletedTask;
        });
        // Call the next delegate/middleware in the pipeline   
        return this._next(context);
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM