很多人可能对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]:跟踪应用接收的每一次请求的更多相关文章

  1. ASP.NET Core 6框架揭秘实例演示[28]:自定义一个服务器

    作为ASP.NET Core请求处理管道的"龙头"的服务器负责监听和接收请求并最终完成对请求的响应.它将原始的请求上下文描述为相应的特性(Feature),并以此将HttpCont ...

  2. ASP.NET Core 6框架揭秘实例演示[07]:文件系统

    ASP.NET Core应用具有很多读取文件的场景,如读取配置文件.静态Web资源文件(如CSS.JavaScript和图片文件等).MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件.这些文 ...

  3. ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式

    .NET的配置支持多样化的数据源,我们可以采用内存的变量.环境变量.命令行参数.以及各种格式的配置文件作为配置的数据来源.在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源 ...

  4. ASP.NET Core 6框架揭秘实例演示[09]:配置绑定

    我们倾向于将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定.除了将配置树叶子节点配置节的绑定为某种标量对象外,我们还可以直接将一个配置 ...

  5. ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式

    依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...

  6. ASP.NET Core 6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式

    在整个软件开发维护生命周期内,最难的不是如何将软件系统开发出来,而是在系统上线之后及时解决遇到的问题.一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根 ...

  7. ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法

    一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根据当前的运行状态预知未来可能发生的问题,并将问题扼杀在摇篮中.诊断跟踪能够帮助我们有效地纠错和排错&l ...

  8. ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]

    <诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...

  9. ASP.NET Core 6框架揭秘实例演示[14]:日志的进阶用法

    为了对各种日志框架进行整合,微软创建了一个用来提供统一的日志编程模式的日志框架.<日志的基本编程模式>以实例演示的方式介绍了日志的基本编程模式,现在我们来补充几种"进阶" ...

随机推荐

  1. Solution -「Gym 102798E」So Many Possibilities...

    \(\mathcal{Description}\)   Link.   给定非负整数序列 \(\{a_n\}\) 和 \(m\),每次随机在 \(\{a\}\) 中取一个非零的 \(a_i\)(保证存 ...

  2. Dubbo基础一之实战初体验

    本以为写这个小作文没什么难度的,可是好像并不是.前段时间重心放在驾考科目二,就想着小作文科二考过了再写也不是事,因为都实战过了.今天想着写却发现脑袋里啥都想不起来了,得翻项目和笔记回忆一下.所以还是那 ...

  3. Spring Boot自动配置实战

    上篇讲述了Spring Boot自动配置的原理,本篇内容就是关于该核心原理的实际应用.需求即当某个类存在的时候,自动配置这个类的bean并且这个bean的属性可以通过application.prope ...

  4. JAVA8学习——Stream底层的实现一(学习过程)

    Stream底层的实现 Stream接口实现了 BaseStream 接口,我们先来看看BaseStream的定义 BaseStream BaseStream是所有流的父类接口. 对JavaDoc做一 ...

  5. Spring是什么? 核心总结

    Spring是一个开源框架,它由Rod Johnson创建.它是为了解决企业应用开发的复杂性而创建的. Spring使用基本的JavaBean来完成以前只可能由EJB完成的事情.  然而,Spring ...

  6. 自创Web框架之过度Django框架

    目录 自创Web框架之过度Django框架 软件开发架构 HTTP协议 Web框架之"撸起袖子加油干" Web框架之通过wsgiref加油干 封装优化处理 动静网页 jinjia2 ...

  7. 已经安装的nginx增加额外配置步骤

    这里以安装第三方ngx_http_google_filter_module模块为例nginx的模块是需要重新编译nginx,而不是像apache一样配置文件引用.so1. 下载第三方扩展模块ngx_h ...

  8. python-利用faker模块生成测试数据

    Python-利用faker模块生成测试数据 1.前言: Faker模块是一个生成伪数据的第三方模块,他提供了一系列方法,使用非常方便,在做自动化测试时,注册信息,用这个模块生成测试数据就体现了它的好 ...

  9. Seastar 教程(一)

    介绍 我们在本文档中介绍的Seastar是一个 C++ 库,用于在现代多核机器上编写高效的复杂服务器应用程序. 传统上,用于编写服务器应用程序的编程语言库和框架分为两个不同的阵营:专注于效率的阵营和专 ...

  10. 实践GoF的23种设计模式:SOLID原则(上)

    摘要:本文以我们日常开发中经常碰到的一些技术/问题/场景作为切入点,示范如何运用设计模式来完成相关的实现. 本文分享自华为云社区<实践GoF的23种设计模式:SOLID原则(上)>,作者: ...