ASP.NET Core 6框架揭秘实例演示[26]:跟踪应用接收的每一次请求
很多人可能对ASP.NET Core框架自身记录的诊断日志并不关心,其实这些日志对纠错排错和性能监控提供了很有用的信息。如果需要创建一个APM(Application Performance Management)系统来监控ASP.NET Core应用处理请求的性能及出现的异常,我们完全可以将HostingApplication对象记录的日志作为收集的原始数据。实际上,目前很多APM(如OpenTelemetry.NET 、Elastic APM和SkyWalking APM等)针对都是利用这种方式收集分布式跟踪日志的。(本篇提供的实例已经汇总到《ASP.NET Core 6框架揭秘-实例演示版》)
[S1701]ASP.NET针对请求的诊断日志(源代码)
[S1702]收集DiagnosticSource输出的日志(源代码)
[S1703]收集EventSource输出的日志(源代码)
[S1701]ASP.NET针对请求的诊断日志
为了确定什么样的信息会被作为诊断日志记录下来,我们通过一个简单的实例演示将HostingApplication对象写入的诊断日志输出到控制台上。HostingApplication对象会将相同的诊断信息以三种不同的方式进行记录,其中包含第8章“诊断日志(中篇)”介绍的日志系统。如下的演示程序利用WebApplicationBuilder的Logging属性得到返回的ILoggingBuilder对象,并调用它的AddSimpleConsole扩展方法为默认注册的ConsoleLoggerProvider开启了针对日志范围的支持。我们最后调用IApplicationBuilder接口的Run扩展方法注册了一个中间件,该中间件在处理请求时会利用依赖注入容器提取出用于发送日志事件的ILogger<Program>对象,并利用它写入一条Information等级的日志。如果请求路径为“/error”,那么该中间件会抛出一个InvalidOperationException类型的异常。
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddSimpleConsole(options => options.IncludeScopes = true);
var app = builder.Build();
app.Run(HandleAsync);
app.Run(); static Task HandleAsync(HttpContext httpContext)
{
var logger = httpContext.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogInformation($"Log for event Foobar");
if (httpContext.Request.Path == new PathString("/error"))
{
throw new InvalidOperationException("Manually throw exception.");
}
return Task.CompletedTask;
}
在启动程序之后,我们利用浏览器采用不同的路径(“/foobar”和“/error”)向应用发送了两次请求,控制台上会输出如图17-4所示的七条日志。由于开启了日志范围的支持,所以输出的日志都会携带日志范围的信息,日志范围提供了很多有用的分布式跟踪信息,比如Trace ID、Span ID、Parent Span ID以及请求的ID和路径等。请求ID(Request ID),它由当前的连接ID和一个序列号组成。从图1可以看出,两次请求的ID分别是“0HMG97FD188VR:00000002”和“0HMG97FD188VR:00000003”。由于采用的是长连接,并且两次请求共享同一个连接,所以它们具有相同的连接ID(“0HMG97FD188VR”)。同一连接的多次请求将一个自增的序列号(“00000002”和“00000003”)作为唯一标识。
图1 捕捉HostingApplication记录的诊断日志
对于两次请求输出的七条日志,类别为“Program”的日志是应用程序自行写入的,HostingApplication写入日志的类别为“Microsoft.AspNetCore.Hosting.Diagnostics”。对于第一次请求的三条日志消息,第一条是在开始处理请求时写入的,我们利用这条日志获知请求的HTTP版本(HTTP/1.1)、HTTP方法(GET)和请求URL。对于包含主体内容的请求,请求主体内容的媒体类型(Content-Type)和大小(Content-Length)也会一并记录下来。当请求处理结束后第三条日志被输出,日志承载的信息包括请求处理耗时(9.9482毫秒)和响应状态码(200)。如果响应具有主体内容,对应的媒体类型同样会被记录下来。
对于第二次请求,由于我们人为抛出了异常,所以异常的信息被写入日志。如果足够仔细,就会发现这条等级为Error的日志并不是由HostingApplication对象写入的,而是作为服务器的KestrelServer写入的,因为该日志采用的类别为“Microsoft.AspNetCore.Server.Kestrel”。
[S1702]收集DiagnosticSource输出的日志
HostingApplication采用的三种日志形式还包括基于DiagnosticSource对象的诊断日志,所以我们可以通过注册诊断监听器来收集诊断信息。如果通过这种方式获取诊断信息,就需要预先知道诊断日志事件的名称和内容荷载的数据结构。通过查看HostingApplication类型的源代码,我们会发现它针对“开始请求”、“结束请求”和“未处理异常”这三类诊断日志事件会采用如下的命名方式。
- 开始请求:Microsoft.AspNetCore.Hosting.BeginRequest。
- 结束请求:Microsoft.AspNetCore.Hosting.EndRequest。
- 未处理异常:Microsoft.AspNetCore.Hosting.UnhandledException。
至于针对诊断日志消息的内容荷载(Payload)的结构,上述三类诊断事件具有两个相同的成员,分别是表示当前请求上下文的HttpContext和通过一个Int64整数表示的当前时间戳,对应的数据成员的名称分别为“httpContext”和“timestamp”。对于未处理异常诊断事件,它承载的内容荷载还包括抛出异常,对应的成员名称为“exception”。我们的演示程序定义了如下这个的DiagnosticCollector类型作为诊断监听器,它定义针对上述三个诊断事件的监听方法。
public class DiagnosticCollector
{
[DiagnosticName("Microsoft.AspNetCore.Hosting.BeginRequest")]
public void OnRequestStart(HttpContext httpContext, long timestamp)
{
var request = httpContext.Request;
Console.WriteLine($"\nRequest starting {request.Protocol} {request.Method} {request.Scheme}://{request.Host}{request.PathBase}{request.Path}");
httpContext.Items["StartTimestamp"] = timestamp;
} [DiagnosticName("Microsoft.AspNetCore.Hosting.EndRequest")]
public void OnRequestEnd(HttpContext httpContext, long timestamp)
{
var startTimestamp = long.Parse(httpContext.Items["StartTimestamp"]!.ToString());
var timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;
var elapsed = new TimeSpan((long)(timestampToTicks * (timestamp - startTimestamp)));
Console.WriteLine($"Request finished in {elapsed.TotalMilliseconds}ms {httpContext.Response.StatusCode}");
}
[DiagnosticName("Microsoft.AspNetCore.Hosting.UnhandledException")]
public void OnException(HttpContext httpContext, long timestamp, Exception exception)
{
OnRequestEnd(httpContext, timestamp);
Console.WriteLine($"{exception.Message}\nType:{exception.GetType()}\nStacktrace: {exception.StackTrace}");
}
}
针对“开始请求”事件的OnRequestStart方法输出了当前请求的HTTP版本、HTTP方法和URL。为了能够计算整个请求处理的耗时,它将当前时间戳保存在HttpContext上下文的Items集合中。针对“结束请求”事件的OnRequestEnd方法将这个时间戳从HttpContext上下文中提取出来,结合当前时间戳计算出请求处理耗时,该耗时和响应的状态码最终会被写入控制台。针对“未处理异常”诊断事件的OnException方法则在调用OnRequestEnd方法之后将异常的消息、类型和跟踪堆栈输出到控制台上。如下所示的演示程序中利用WebApplication的Services提供的依赖注入容器提取出注册的DiagnosticListener对象,并调用它的SubscribeWithAdapter扩展方法将DiagnosticCollector对象注册为订阅者。我们调用Run扩展方法注册了一个中间件,该中间件会在请求路径为“/error”的情况下抛出异常。
using App;
using System.Diagnostics; var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
var app = builder.Build();
var listener = app.Services.GetRequiredService<DiagnosticListener>();
listener.SubscribeWithAdapter(new DiagnosticCollector());
app.Run(HandleAsync);
app.Run(); static Task HandleAsync(HttpContext httpContext)
{
var listener = httpContext.RequestServices.GetRequiredService<DiagnosticListener>();
if (httpContext.Request.Path == new PathString("/error"))
{
throw new InvalidOperationException("Manually throw exception.");
}
return Task.CompletedTask;
}
待演示实例正常启动后,可以采用不同的路径(“/foobar”和“/error”)对应用程序发送两个请求,服务端控制台会以图2所示的形式输出DiagnosticCollector对象收集的诊断信息。
图2 利用注册的诊断监听器获取诊断日志
[S1703]收集EventSource输出的日志
HostingApplication在处理每个请求的过程中还会利用名称为“Microsoft.AspNetCore.Hosting”EventSource对象发出相应的日志事件。这个EventSource对象来回在在启动和关闭应用程序时发出相应的事件。涉及的五个日志事件对应的名称如下:
- 启动应用程序:HostStart。
- 开始处理请求:RequestStart。
- 请求处理结束:RequestStop。
- 未处理异常:UnhandledException。
- 关闭应用程序:HostStop。
如下所示的演示程序利用创建的EventListener对象来监听上述五个日志事件。如代码片段所示,我们定义了派生于抽象类EventListener的DiagnosticCollector类型,并在启动应用前创建了这个对象,我们通过注册它的EventSourceCreated事件开启了针对上述EventSource的监听。注册的EventWritten事件会将监听到的事件名称的负载内容输出到控制台上。
using System.Diagnostics.Tracing; var listener = new DiagnosticCollector();
listener.EventSourceCreated += (sender, args) =>
{
if (args.EventSource?.Name == "Microsoft.AspNetCore.Hosting")
{
listener.EnableEvents(args.EventSource, EventLevel.LogAlways);
}
};
listener.EventWritten += (sender, args) =>
{
Console.WriteLine(args.EventName);
for (int index = 0; index < args.PayloadNames?.Count; index++)
{
Console.WriteLine($"\t{args.PayloadNames[index]} = {args.Payload?[index]}");
}
}; var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
var app = builder.Build();
app.Run(HandleAsync);
app.Run(); static Task HandleAsync(HttpContext httpContext)
{
if (httpContext.Request.Path == new PathString("/error"))
{
throw new InvalidOperationException("Manually throw exception.");
}
return Task.CompletedTask;
} public class DiagnosticCollector : EventListener { }
以命令行的形式启动这个演示程序后,从图3所示的输出结果可以看到名为HostStart的事件被发出。然后我们采用目标地址“http://localhost:5000/foobar”和“http:// http://localhost:5000/error”对应用程序发送两个请求,从输出结果可以看出,应用程序针对前者的处理过程会发出RequestStart事件和RequestStop事件,针对后者的处理则会因为抛出的异常发出额外的事件UnhandledException。输入“Ctrl+C”关闭应用后,名称为HostStop的事件被发出。对于通过EventSource发出的五个事件,只有RequestStart事件会将请求的HTTP方法(GET)和路径(“/foobar”和“/error”)作为负载内容,其他事件都不会携带任何负载内容。
图3 利用注册EventListener监听器获取诊断日志
ASP.NET Core 6框架揭秘实例演示[26]:跟踪应用接收的每一次请求的更多相关文章
- ASP.NET Core 6框架揭秘实例演示[28]:自定义一个服务器
作为ASP.NET Core请求处理管道的"龙头"的服务器负责监听和接收请求并最终完成对请求的响应.它将原始的请求上下文描述为相应的特性(Feature),并以此将HttpCont ...
- ASP.NET Core 6框架揭秘实例演示[07]:文件系统
ASP.NET Core应用具有很多读取文件的场景,如读取配置文件.静态Web资源文件(如CSS.JavaScript和图片文件等).MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件.这些文 ...
- ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式
.NET的配置支持多样化的数据源,我们可以采用内存的变量.环境变量.命令行参数.以及各种格式的配置文件作为配置的数据来源.在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源 ...
- ASP.NET Core 6框架揭秘实例演示[09]:配置绑定
我们倾向于将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定.除了将配置树叶子节点配置节的绑定为某种标量对象外,我们还可以直接将一个配置 ...
- ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式
依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...
- ASP.NET Core 6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式
在整个软件开发维护生命周期内,最难的不是如何将软件系统开发出来,而是在系统上线之后及时解决遇到的问题.一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根 ...
- ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法
一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根据当前的运行状态预知未来可能发生的问题,并将问题扼杀在摇篮中.诊断跟踪能够帮助我们有效地纠错和排错&l ...
- ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]
<诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...
- ASP.NET Core 6框架揭秘实例演示[14]:日志的进阶用法
为了对各种日志框架进行整合,微软创建了一个用来提供统一的日志编程模式的日志框架.<日志的基本编程模式>以实例演示的方式介绍了日志的基本编程模式,现在我们来补充几种"进阶" ...
随机推荐
- ◆JAVA加密解密-DES
DES算法提供CBC, OFB, CFB, ECB四种模式,MAC是基于ECB实现的. 一.数据补位 DES数据加解密就是将数据按照8个字节一段进行DES加密或解密得到一段8个字节的密文或者明文,最后 ...
- python——平时遇到问题记录
# pyhs2安装 #centos yum install groupinstall 'development tools' yum install python34-devel yum instal ...
- Java访问修饰符和三大特征(封装,继承和多态)
一.访问修饰符基本介绍: java提供四种访问控制修饰符号,用于控制方法和属性(成员变量)的访问权限(范围): 1.公开级别:用public修饰,对外公开2.受保护级别:用protected修饰,对子 ...
- CentOS卸载自带的JDK
一般在配置JDK之前要卸载CentOS自带的openjdk,接下来演示如何卸载自带的openjdk 进入root模式 查看jdk是否已经安装jdk rpm -qa | grep jdk 3. 卸载op ...
- linxu 查看运行日志
journalctl - 检索 systemd 日志 journalctl 可用于检索 systemd(1) 日志(由 systemd-journald.service(8) 记录). 如果不带任何参 ...
- 微服务从代码到k8s部署应有尽有系列(一)
从本篇文章开始,我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 实战项目地址:https://github.com/Mikaelemmmm/go-zer ...
- Solution -「BZOJ 3331」压力
\(\mathcal{Description}\) Link. 给定一个 \(n\) 个点 \(m\) 条边的连通无向图,并给出 \(q\) 个点对 \((u,v)\),令 \(u\) 到 \ ...
- .NET core实现一个简易的事件协调器(saga)
在领域驱动设计中,由于领域边界的存在,以往的分层设计中业务会按照其固有的领域知识被切分到不同的限界中,并且引入了领域事件这一概念来降低单个业务的复杂度,通过非耦合的事件驱动来完成复杂的业务.但是事件驱 ...
- 记一次慢查询优化sql
sql语句优化(慢查询日志) 最近,旧系统向新系统迁移工程刚刚结束.开发完成后,测试阶段也是好好休息了一把.接到一个需求,由于内部员工使用的网站部分功能加载时间很长,所以需要去优化系统的一些功能.大致 ...
- 深入剖析CVE-2021-40444-Cabless利用链
背景 CVE-2021-40444为微软MHTML远程命令执行漏洞,攻击者可通过传播Microsoft Office文档,诱导目标点击文档从而在目标机器上执行任意代码.该漏洞最初的利用思路是使用下载c ...