如何获得实际请求执行时间

时间:2018-01-10 17:15:17

标签: c# asp.net-core stopwatch asp.net-core-middleware

鉴于以下中间件:

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)值捕获实际的请求执行时间?我想将此值存储在数据库中或在其他地方使用它。

2 个答案:

答案 0 :(得分:14)

我认为这个问题非常有趣,所以我对此进行了一些研究,以了解WebHost实际上是如何测量和显示请求时间的。底线是:既没有好的,也没有简单的方法来获取这些信息,而且一切都像是黑客。但如果你还有兴趣,请继续关注。

当应用程序启动时,WebHostBuilder会构建WebHost,而HostingApplication会创建HostingApplication。这基本上是负责响应传入请求的根组件。它是在请求进入时调用中间件管道的组件。

也是将创建HostingApplicationDiagnostics的组件,它允许收集有关请求处理的诊断信息。在请求开始时,HostingApplicationDiagnostics.BeginRequest会致电HostingApplicationDiagnostics.RequestEnd,并在请求结束时致电HostingApplicationDiagnostics

毫不奇怪,WebHost将衡量请求持续时间,并记录您已经看到的DiagnosticListener的消息。所以这是我们必须更密切地检查以确定如何获取信息的类。

有两件事,诊断对象用于报告诊断信息:记录器和DiagnosticListener

诊断监听器

DiagnosticListener是一个有趣的事情:它基本上是一个普通的event sink,你可以举起事件。然后其他对象可以订阅它以收听这些事件。所以这对我们来说几乎听起来很完美!

HostingApplicationDiagnostics使用的WebHost对象由Startup传递,实际上gets resolved from dependency injection。由于它是registered by the WebHostBuilder as a singleton),我们实际上可以从依赖注入中解析侦听器并订阅它的事件。所以,让我们在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。当事件发生时,我们将获得一个键值对,其中键是事件的标识符,值是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); } } } 传递的自定义对象。

但是在我们实现观察者之前,我们应该看看HostingApplicationDiagnostics实际引发的事件类型。

不幸的是,当请求结束时,诊断列表中引发的事件刚刚传递the end timestamp,因此我们还需要监听引发的事件{{ 3}}读取开始时间戳的请求。但这会将状态引入我们的观察者,这是我们想要避免的。另外,实际的事件名称常量是at the beginning,这可能是我们应该避免使用它们的指标。

首选方法是使用与诊断观察者密切相关的prefixed with Deprecated。活动显然是跟踪应用程序中出现的活动的状态。它们在某些时候开始和停止,并且已经记录了它们自己运行的时间。因此,我们可以让观察者听取停止事件,以便活动在完成时得到通知:

Activity.Current

不幸的是没有没有缺点的解决方案......我发现这个解决方案对于并行请求来说非常不准确(例如,当打开一个同时具有并行请求的图像或脚本的页面时)。这可能是因为我们使用静态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); } } 来获取活动。然而,似乎没有办法只获得单个请求的活动,例如来自传递的键值对。

所以我回过头来再次尝试了我原来的想法,使用那些已弃用的事件。我理解它的方式是顺便说一句。他们刚被弃用是因为建议使用活动,而不是因为它们很快就会被删除(当然我们正在处理实现细节和内部类,所以这些东西可能随时改变)。为了避免并发问题,我们需要确保将状态存储在HTTP上下文中(而不是类字段):

HttpContext.Items

运行此操作时,我们确实获得了准确的结果,并且我们还可以访问HttpContext,我们可以使用它来识别请求。当然,这里涉及的开销非常明显:反映访问属性值,必须将信息存储在HostingApplicationDiagnostics中,整个观察者的事情一般......这可能不是一种非常有效的方法。

关于诊断来源和活动的更多内容:activitiesDiagnosticSource Users Guid

登录

上面某处我提到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() { } } } 也会将信息报告给日志工具。当然:这毕竟是我们在控制台中看到的。如果我们Activity User Guide,我们可以看到这已经计算出适当的持续时间。由于这是结构化日志记录,我们可以使用它来获取该信息。

因此,让我们尝试编写一个自定义记录器来检查look at the implementation,看看我们能做些什么:

HostingRequestFinishedLog

不幸的是(你现在可能喜欢这个词,对吧?),州级WebHost.CreateDefaultBuilder(args) .ConfigureLogging(logging => { logging.AddProvider(new RequestDurationLogger()); }) .UseStartup<Startup>() .Build(); 是内部的,所以我们不能直接使用它。所以我们必须使用反射来识别它。但是我们只需要它的名字,然后我们就可以从只读列表中提取值。

现在我们需要做的就是将logger(provider)注册到web主机:

HostingApplicationDiagnostics

实际上,我们需要能够访问标准日志记录所具有的完全相同的信息。

但是,有两个问题:我们这里没有HttpContext,因此我们无法获得有关此持续时间实际属于哪个请求的信息。正如您在_httpContext中看到的,此记录调用实际上仅在that exact state object时进行。

我们可以通过使用反射读取私有字段HostingApplicationDiagnostics来获取HttpContext,但是我们无法对日志级别做任何事情。当然,我们正在创建一个记录器以从一个特定的日志记录调用中获取信息这一事实是一个超级黑客,无论如何可能都不是一个好主意。

结论

所以,这太可怕了。根本没有从{{1}}检索此信息的干净方法。我们还必须记住,诊断内容实际上只在启用时运行。而性能关键型应用程序可能会在某个时刻禁用它。无论如何,将这些信息用于诊断之外的任何事情都是一个坏主意,因为它一般来说太脆弱了。

那么什么是更好的解决方案?一个超出诊断环境的解决方案? 一个早期运行的简单中间件;就像你已经使用过一样。是的,这可能不如从外部请求处理管道遗漏一些路径那样准确,但它仍然是实际应用程序代码的准确度量。毕竟,如果我们想要衡量框架性能,我们必须从外部测量它:作为客户端,发出请求(就像基准测试一样)。

顺便说一下。这也是Stack Overflow自己的the log level is at least Information的工作原理。你只是MiniProfiler就是这样。

答案 1 :(得分:2)

您可以使用中间件进行一些更改。我这样使用来将响应时间添加到响应头:

 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);
    }
}