如何获取实际请求执行时间
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
的消息。所以这是我们必须更仔细地检查以弄清楚如何获取信息的 class。
诊断对象使用两个东西来报告诊断信息:记录器和 DiagnosticListener
。
诊断侦听器
DiagnosticListener
是一件有趣的事情:它基本上是一个通用的 event sink,您可以在其上引发事件。然后其他对象可以订阅它来监听这些事件。所以这对于我们的目的来说几乎是完美的!
HostingApplicationDiagnostics
使用的 DiagnosticListener
对象由 WebHost
传递,它实际上 gets resolved from dependency injection. Since it is registered by the WebHostBuilder
as a singleton,我们实际上可以从依赖注入中解析监听器并且订阅它的事件。所以让我们在 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
运行ning 了。我们的观察者需要实现 IObserver<KeyValuePair<string, object>>
。当事件发生时,我们将得到一个键值对,其中键是事件的标识符,值是通过 HostingApplicationDiagnostics
.
传递的自定义对象。
但在我们实现我们的观察者之前,我们实际上应该看看什么样的事件 HostingApplicationDiagnostics
实际引发。
不幸的是,当请求结束时,在诊断列表器上引发的事件刚刚通过the end timestamp, so we would also need to listen to the event that is raised at the beginning of the request to read the start timestamp. But that would introduce state into our observer which is something we want to avoid here. In addition, the actual event name constants are prefixed with Deprecated
,这可能表明我们应该避免使用这些.
首选方法是使用activities,这也与诊断观察器密切相关。活动显然是跟踪应用程序中出现的活动的状态。它们在某个时间点开始和停止,并且已经记录了它们自己 运行 的时间。所以我们可以让我们的观察者监听 activity 的停止事件,以便在它完成时得到通知:
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
来获取 activity。然而,似乎并没有办法只为单个请求获取 activity,例如来自传递的键值对。
所以我回去再次尝试我最初的想法,使用那些已弃用的事件。我理解它的方式是顺便说一句。它们之所以被弃用,是因为建议使用活动,而不是因为它们很快就会被删除(当然,我们正在处理实现细节和内部 class,因此这些事情可能随时更改)。为了避免并发问题,我们需要确保将状态存储在 HTTP 上下文中(而不是 class 字段):
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 Users Guid and Activity User Guide。
日志记录
我在上面的某处提到 HostingApplicationDiagnostics
也将信息报告给日志记录工具。当然:这毕竟是我们在控制台中看到的。如果我们 look at the implementation,我们可以看到这已经在这里计算出适当的持续时间。由于这是结构化日志记录,我们可以使用它来获取该信息。
所以让我们尝试编写一个自定义记录器来检查 that exact state object 并看看我们能做什么:
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() { }
}
}
不幸的是(你现在可能喜欢这个词了,对吧?),状态 class HostingRequestFinishedLog
是内部的,所以我们不能直接使用它。所以我们要用反射来识别它。但是我们只需要它的名字,然后我们就可以从只读列表中提取值。
现在我们需要做的就是向网络主机注册该记录器(提供程序):
WebHost.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.AddProvider(new RequestDurationLogger());
})
.UseStartup<Startup>()
.Build();
实际上,这就是我们能够访问与标准日志记录完全相同的信息所需的全部内容。
但是,有两个问题:我们这里没有HttpContext,所以我们无法获得关于这个持续时间实际属于哪个请求的信息。正如您在 HostingApplicationDiagnostics
中所见,此日志记录调用实际上仅在 the log level is at least Information
.
时进行
我们可以通过使用反射读取私有字段 _httpContext
来获取 HttpContext,但是对于日志级别我们无能为力。当然,我们正在创建一个记录器以从 一个特定的 记录调用中获取信息这一事实是一个超级 hack,无论如何可能不是一个好主意。
结论
所以,这一切都很糟糕。根本没有从 HostingApplicationDiagnostics
检索此信息的干净方法。我们还必须记住,诊断功能在启用时实际上只有 运行s。性能关键型应用程序可能会在某一时刻禁用它。无论如何,将此信息用于诊断以外的任何事情都不是一个好主意,因为它通常太脆弱了。
那么更好的解决方案是什么?在诊断上下文之外工作的解决方案? 一个运行早的简单中间件;就像您已经使用过一样。是的,这可能不那么准确,因为它会遗漏一些来自外部请求处理管道的路径,但它仍然是对实际应用程序代码 的准确测量 。毕竟,如果我们想衡量框架性能,我们无论如何都必须从外部衡量它:作为客户,提出请求(就像基准测试一样)。
顺便说一句。这也是 Stack Overflow 自己的 MiniProfiler works. You just register the middleware early 就是这样。
给定以下中间件:
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
的消息。所以这是我们必须更仔细地检查以弄清楚如何获取信息的 class。
诊断对象使用两个东西来报告诊断信息:记录器和 DiagnosticListener
。
诊断侦听器
DiagnosticListener
是一件有趣的事情:它基本上是一个通用的 event sink,您可以在其上引发事件。然后其他对象可以订阅它来监听这些事件。所以这对于我们的目的来说几乎是完美的!
HostingApplicationDiagnostics
使用的 DiagnosticListener
对象由 WebHost
传递,它实际上 gets resolved from dependency injection. Since it is registered by the WebHostBuilder
as a singleton,我们实际上可以从依赖注入中解析监听器并且订阅它的事件。所以让我们在 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
运行ning 了。我们的观察者需要实现 IObserver<KeyValuePair<string, object>>
。当事件发生时,我们将得到一个键值对,其中键是事件的标识符,值是通过 HostingApplicationDiagnostics
.
但在我们实现我们的观察者之前,我们实际上应该看看什么样的事件 HostingApplicationDiagnostics
实际引发。
不幸的是,当请求结束时,在诊断列表器上引发的事件刚刚通过the end timestamp, so we would also need to listen to the event that is raised at the beginning of the request to read the start timestamp. But that would introduce state into our observer which is something we want to avoid here. In addition, the actual event name constants are prefixed with Deprecated
,这可能表明我们应该避免使用这些.
首选方法是使用activities,这也与诊断观察器密切相关。活动显然是跟踪应用程序中出现的活动的状态。它们在某个时间点开始和停止,并且已经记录了它们自己 运行 的时间。所以我们可以让我们的观察者监听 activity 的停止事件,以便在它完成时得到通知:
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
来获取 activity。然而,似乎并没有办法只为单个请求获取 activity,例如来自传递的键值对。
所以我回去再次尝试我最初的想法,使用那些已弃用的事件。我理解它的方式是顺便说一句。它们之所以被弃用,是因为建议使用活动,而不是因为它们很快就会被删除(当然,我们正在处理实现细节和内部 class,因此这些事情可能随时更改)。为了避免并发问题,我们需要确保将状态存储在 HTTP 上下文中(而不是 class 字段):
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 Users Guid and Activity User Guide。
日志记录
我在上面的某处提到 HostingApplicationDiagnostics
也将信息报告给日志记录工具。当然:这毕竟是我们在控制台中看到的。如果我们 look at the implementation,我们可以看到这已经在这里计算出适当的持续时间。由于这是结构化日志记录,我们可以使用它来获取该信息。
所以让我们尝试编写一个自定义记录器来检查 that exact state object 并看看我们能做什么:
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() { }
}
}
不幸的是(你现在可能喜欢这个词了,对吧?),状态 class HostingRequestFinishedLog
是内部的,所以我们不能直接使用它。所以我们要用反射来识别它。但是我们只需要它的名字,然后我们就可以从只读列表中提取值。
现在我们需要做的就是向网络主机注册该记录器(提供程序):
WebHost.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.AddProvider(new RequestDurationLogger());
})
.UseStartup<Startup>()
.Build();
实际上,这就是我们能够访问与标准日志记录完全相同的信息所需的全部内容。
但是,有两个问题:我们这里没有HttpContext,所以我们无法获得关于这个持续时间实际属于哪个请求的信息。正如您在 HostingApplicationDiagnostics
中所见,此日志记录调用实际上仅在 the log level is at least Information
.
我们可以通过使用反射读取私有字段 _httpContext
来获取 HttpContext,但是对于日志级别我们无能为力。当然,我们正在创建一个记录器以从 一个特定的 记录调用中获取信息这一事实是一个超级 hack,无论如何可能不是一个好主意。
结论
所以,这一切都很糟糕。根本没有从 HostingApplicationDiagnostics
检索此信息的干净方法。我们还必须记住,诊断功能在启用时实际上只有 运行s。性能关键型应用程序可能会在某一时刻禁用它。无论如何,将此信息用于诊断以外的任何事情都不是一个好主意,因为它通常太脆弱了。
那么更好的解决方案是什么?在诊断上下文之外工作的解决方案? 一个运行早的简单中间件;就像您已经使用过一样。是的,这可能不那么准确,因为它会遗漏一些来自外部请求处理管道的路径,但它仍然是对实际应用程序代码 的准确测量 。毕竟,如果我们想衡量框架性能,我们无论如何都必须从外部衡量它:作为客户,提出请求(就像基准测试一样)。
顺便说一句。这也是 Stack Overflow 自己的 MiniProfiler works. You just register the middleware early 就是这样。