第17章 使用日志记录监视和排除错误(ASP.NET Core in Action, 2nd Edition)

2023-03-05,,

第3部分 扩展应用程序

我们在第1部分和第2部分中介绍了大量内容:我们查看了您将用于构建传统服务器渲染的 Razor Pages 应用程序以及 Web API 的所有主要功能组件。在第3部分中,我们将讨论六个不同的主题,这些主题基于您目前所学的内容:日志记录、安全性、自定义组件、与第三方HTTP API的交互、后台服务和测试。将日志记录添加到应用程序中是通常要等到发现生产中的问题后才能进行的活动之一。从一开始就添加合理的日志记录将帮助您快速诊断并修复出现的错误。第17章介绍了 ASP.NET Core 内置的日志框架。您将看到如何使用它将日志消息写入各种位置,无论是控制台、文件还是第三方远程日志服务。

正确保护您的应用程序是当今网络开发的重要组成部分。即使您觉得应用程序中没有任何敏感数据,您也必须确保遵守安全最佳实践来保护用户免受攻击。在第18章中,我将描述一些常见的漏洞,攻击者如何利用这些漏洞,以及如何保护应用程序。

在第1部分中,您了解了中间件管道,并了解了它对所有 ASP.NET Core 应用程序的重要性。在第19章中,您将学习如何创建自己的自定义中间件以及简单的端点,以便在不需要 RazorPages 或 WebAPI 控制器的全部功能时使用。您还将学习如何处理实际应用程序中经常出现的一些复杂的鸡和蛋配置问题。最后,您将学习如何用第三方替代品替换内置依赖注入容器。

在第20章中,您将学习如何创建用于使用 RazorPages 和 API 控制器的自定义组件。您将学习如何创建自定义标记帮助程序和验证属性,我将介绍一个新组件——视图组件——用于使用 Razor 视图渲染封装逻辑。您还将学习如何用替代方法替换 ASP.NET Core 中默认使用的基于属性的验证框架。

你构建的大多数应用程序都不是独立设计的。您的应用程序需要与第三方 API 交互是非常常见的,无论这些 API 是用于发送电子邮件、获取汇率或付款的 API。在第21章中,您将学习如何使用 IHttpClientFactory 抽象与第三方 API 交互,以简化配置、添加瞬时故障处理和避免常见陷阱。

本书主要讨论 HTTP 流量服务,包括使用 RazorPages 的服务器渲染网页和移动和单页应用程序常用的 WebAPI。然而,许多应用程序需要长时间运行的后台任务,这些任务按计划执行作业或处理队列中的项目。在第22章中,我将展示如何在 ASP.NET Core 应用程序中创建这些长时间运行的后台任务。我还将展示如何创建仅具有后台任务而没有任何 HTTP 处理的独立服务,以及如何将其安装为 Windows 服务或 Linux 系统守护程序。

第23章是最后一章,介绍了测试应用程序。测试在应用程序开发中的确切作用有时会引发哲学争论,但在第23章中,我将坚持使用 xUnit 测试框架测试应用程序的实用性。您将看到如何为应用程序创建单元测试,如何使用内存数据库提供程序测试依赖于 EF Core 的代码,以及如何编写可以同时测试应用程序多个方面的集成测试。

本章重点

了解日志消息的组成部分
将日志写入多个输出位置
使用过滤控制不同环境中的日志详细信息
使用结构化日志记录使日志可搜索

日志记录是那些似乎没有必要的主题之一,直到你迫切需要它!没有什么比发现一个只能在生产中重现的问题更令人沮丧的了,然后发现没有日志可以帮助您调试它。

日志记录是在应用程序中记录事件或活动的过程,通常涉及将记录写入控制台、文件、Windows事件日志或其他系统。您可以在日志消息中记录任何内容,但通常有两种不同类型的消息:

Informational messages(信息消息)—— 发生了一个标准事件:用户登录、产品放入购物车或在博客应用程序上创建了一篇新文章。
Warnings and errors(警告和错误)—— 发生错误或意外情况:用户在购物车中的总金额为负数,或发生异常。

历史上,在大型应用程序中进行日志记录的一个常见问题是,每个库和框架都会以稍微不同的格式生成日志(如果有的话)。当你的应用程序出现错误,而你正试图诊断它时,这种不一致性会使你更难将应用程序中的点连接起来,以获得全貌并了解问题。

幸运的是,ASP.NET Core包含了一个新的通用日志记录接口,您可以插入其中。它在ASP.NET Core框架代码本身以及第三方库中使用,您可以轻松地使用它在自己的代码中创建日志。使用ASP.NET Core日志框架,您可以控制来自代码的每个部分(包括框架和库)的日志的详细程度,并且可以将日志输出写入插入框架的任何目标。

在本章中,我将详细介绍ASP.NET Core日志框架,并解释如何使用它来记录事件和诊断自己应用程序中的错误。在第17.1节中,我将描述日志框架的体系结构。您将了解DI如何使库和应用程序轻松创建日志消息,以及将这些日志写入多个目标。

在第17.2节中,您将学习如何使用ILogger接口在应用程序中编写自己的日志消息。我们将分解典型日志记录的解剖结构,并查看其属性,如日志级别、类别和消息。

只有当您能够阅读日志时,编写日志才有用,因此在17.3节中,您将学习如何将日志提供程序添加到应用程序中。日志记录提供程序控制应用程序写入日志消息的位置。这可以是控制台、文件,甚至是外部服务。我将向您展示如何添加将日志写入文件的日志记录提供程序,以及如何在应用程序中配置一个名为Serilog的流行第三方日志记录提供。

日志记录是任何应用程序的重要组成部分,但确定日志记录的数量是一个棘手的问题。一方面,您希望提供足够的信息,以便能够诊断任何问题。另一方面,您不希望在日志中填充数据,从而在需要时很难找到重要信息。更糟糕的是,一旦您在生产环境中运行,开发中足够的内容可能会太多。

在第17.4节中,我将解释如何从应用程序的各个部分(如ASP.NET Core基础设施库)过滤日志消息,以便日志提供商只编写重要消息。这使您能够在开发中的大量日志记录和仅在生产中写入重要日志之间保持平衡。

在本章的最后一节中,我将介绍结构化日志的一些好处,这是一种可以与ASP.NET Core日志框架的一些提供程序一起使用的日志记录方法。结构化日志包括将数据作为键值对附加到日志消息中,以便于搜索和查询日志。例如,您可以在应用程序生成的每个日志消

息中附加一个唯一的客户ID。与以不一致的方式记录客户ID作为日志消息的一部分相比,使用此方法查找与用户相关联的所有日志消息要简单得多。

本章开始时,我们将深入了解日志记录所涉及的内容,以及为什么您未来的自己会感谢您在应用程序中有效地使用日志记录。然后,我们将查看ASP.NET Core日志框架的各个部分,这些部分将直接用于应用程序,以及它们是如何组合在一起的。

17.1 在生产应用程序中有效使用日志记录

假设您刚刚将一个新应用程序部署到生产环境中,当客户打电话说他们使用您的应用程序时收到错误消息。您如何确定导致问题的原因?你可以问客户他们正在采取什么步骤,并可能尝试自己重新创建错误,但如果这不起作用,你就只能在代码中搜索,试图找出错误,而无需其他操作。

日志记录可以提供快速诊断问题所需的额外上下文。可以说,最重要的日志记录了错误本身的详细信息,但导致错误的事件在诊断错误原因时同样有用。

向应用程序添加日志记录有很多原因,但通常,原因分为以下三类之一:

出于检查或分析原因进行日志记录,以跟踪事件发生的时间
日志记录错误
记录非错误事件,以在发生错误时提供事件的面包屑式跟踪

第一个原因很简单。例如,您可能需要记录用户每次登录的时间,或者您可能需要跟踪特定 API 方法被调用的次数。日志记录是记录应用程序行为的一种简单方法,每次发生有趣的事件时都会向日志中写入消息。

我发现日志记录的第二个原因是最常见的。当一个应用程序运行得很好时,日志通常完全不受影响。当出现问题,客户打电话来时,日志就变得非常宝贵。一组好的日志可以帮助您了解应用程序中导致错误的条件,包括错误本身的上下文,以及以前请求中的上下文。

提示:即使有大量的日志记录,除非您定期查看日志,否则您可能不会意识到您的应用程序存在问题。对于任何中大型应用程序,这都是不切实际的,因此 Raygun 或 Sentry (https://sentry.io)等监控服务(https://raygun.io)对于快速通知您问题非常有用。

如果这听起来像是很多工作,那么你很幸运。ASP.NET Core 为您做了大量的“面包屑”日志记录,这样您就可以专注于创建高质量的日志消息,在诊断问题时提供最大的价值。

17.1.1 使用自定义日志消息突出显示问题

ASP.NET Core 在其库中使用日志记录。根据您配置应用程序的方式,您可以访问每个请求和 EF Core 查询的详细信息,甚至无需向自己的代码添加额外的日志消息。在图17.1中,您可以看到在配方应用程序中查看单个配方时创建的日志消息。

图17.1 ASP.NET Core Framework 库始终使用日志记录。单个请求生成多个日志消息,这些消息描述了请求通过应用程序的流程。

这为您提供了许多有用的信息。您可以看到请求的 URL、调用的 RazorPage 和页面处理程序、EFCore 数据库命令、调用的操作结果和响应。当您在本地工作时,无论是生产应用程序中的错误还是开发中的功能,这些信息在尝试隔离问题时都是非常宝贵的。

这种基础设施日志记录可能很有用,但您自己创建的日志消息可能具有更大的价值。例如,您可以从图17.1中的日志消息中找出错误的原因——我们试图查看一个配方,其配方的 RecipeId 未知为5,但这远非显而易见。如果在发生这种情况时向应用程序显式添加日志消息,如图17.2所示,那么问题就更加明显了。

图17.2 您可以编写自己的日志。这些通常对识别应用程序中的问题和有趣事件更有用。

此自定义日志消息很容易脱颖而出,并清楚地说明了问题(具有请求 ID 的配方不存在)和导致问题的参数/变量(ID 值为5)。将类似的日志消息添加到您自己的应用程序将使您更容易诊断问题、跟踪重要事件,并大致了解您的应用程序正在做什么。

希望您现在有动力将日志记录添加到应用程序中,因此我们将深入了解其中的细节。在第17.1.2节中,您将看到如何创建日志消息以及如何定义日志消息的写入位置。我们将在第17.2节和第17.3节中详细讨论这两个方面;不过,首先,我们将从 ASP.NET Core 日志框架的整体来看它们的适用性。

17.1.2 ASP.NET Core 日志抽象

ASP.NET Core 日志框架由许多日志抽象(接口、实现和助手类)组成,其中最重要的如图17.3所示:

ILogger——这是您将在代码中与之交互的接口。它有一个 Log() 方法,用于编写日志消息。
ILoggerProvider——这用于创建 ILogger 的自定义实例,具体取决于提供程序。“控制台”ILoggerProvider 将创建一个写入控制台的 ILogger,而“文件”ILogger Provider 将创建写入文件的 ILogger。
ILoggerFactory——这是 ILoggerProvider 实例和代码中使用的 ILogger 之间的粘合剂。您可以向 ILoggerFactory 注册 ILoggerProvider 实例,并在需要 ILogger 时调用 ILoggerFactory 上的 CreateLogger()。工厂创建了一个 ILogger,该 ILogger 包装每个提供程序,因此当您调用 Log() 方法时,日志将写入每个提供程序。

图17.3中的设计使添加或更改应用程序写入日志消息的位置变得容易,而无需更改应用程序代码。以下列表显示了添加将日志写入控制台的 ILoggerProvider 所需的所有代码。

清单17.1在 Program.cs 中向 IHost 添加控制台日志提供程序

public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) => new HostBuilder()
.ConfigureLogging(builder =>builder.AddConsole()) //在HostBuilder上使用ConfigureLogging扩展方法添加新的提供程序。
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}

图17.3 ASP.NET Core日志框架的组件。您向ILoggerFactory注册日志记录提供程序,该工厂用于创建ILogger的实现。您将日志写入ILogger,ILogger使用ILogger实现将日志输出到控制台或文件。此设计允许您将日志发送到多个位置,而无需在创建日志消息时配置这些位置。

注意:控制台记录器默认添加到CreateDefaultBuilder方法中,如第17.3节所示。

除了 IHostBuilder 上的此配置之外,您不直接与 ILoggerProvider 实例交互。相反,您可以使用 ILogger 的实例编写日志,这将在下一节中看到。

17.2 向应用程序添加日志消息

在本节中,我们将详细介绍如何在自己的应用程序中创建日志消息。您将学习如何创建 ILogger 的实例,以及如何使用它向现有应用程序添加日志记录。最后,我们将查看组成日志记录的财产,它们的含义,以及您可以使用它们做什么。

与 ASP.NET Core 中的几乎所有内容一样,日志记录可以通过DI获得。要将日志记录添加到您自己的服务中,只需要注入 ILogger<T>的实例,其中T是您的服务类型。

注意:当您注入 ILogger<T>时,DI 容器会间接调用 ILoggerFactory.CreateLogger<T>() 来创建图17.3所示的包装 ILogger。在第17.2.2节中,如果您愿意,您将看到如何直接使用 ILoggerFactory。ILogger<T>接口还实现了非通用 ILogger 接口,但添加了其他方便方法。

您可以使用注入的 ILogger 实例创建日志消息,并将其写入每个配置的 ILoggerProvider。下面的列表显示了如何将 ILogger<>实例注入到上一章中配方应用程序的 Index.cshtml Razor Page 的 PageModel 中,以及如何编写日志消息,指示找到了多少配方。

清单17.2 将 ILogger 注入类并编写日志消息

public class IndexModel : PageModel
{
private readonly RecipeService _service;
private readonly ILogger<IndexModel> _log; //使用实现 ILogger 的 DI 注入通用 ILogger
public ICollection<RecipeSummaryViewModel> Recipes { get; set; } public IndexModel( RecipeService service, ILogger<IndexModel> log)
{
_service = service;
_log = log;
} public void OnGet()
{
Recipes = _service.GetRecipes();
_log.LogInformation(
"Loaded {RecipeCount} recipes", Recipes.Count); //这将写入信息级日志。RecipeCount 变量在消息中被替换。
}
}

在此示例中,您将使用 ILogger 上的众多扩展方法之一来创建日志消息 LogInformation()。 ILogger 上有许多扩展方法,可让您轻松地为消息指定 LogLevel。

定义: 日志的日志级别是它的重要性,由 LogLevel 枚举定义。每条日志消息都有一个日志级别。

您还可以看到传递给 LogInformation 方法的消息有一个由大括号 {RecipeCount} 指示的占位符,并且您将一个附加参数 Recipes.Count 传递给记录器。记录器将在运行时用参数替换占位符。占位符按位置与参数匹配,因此如果您包括两个占位符,例如,第二个占位符与第二个参数匹配。

提示:您可以使用普通的字符串插值来创建日志消息;例如,$“Loaded {Recipes.Count} recipes”。但我建议始终使用占位符,因为它们为记录器提供了可用于结构化日志记录的附加信息,如您将在 17.5 节中看到的那样。

当 IndexModel 中的 OnGet 页面处理进程执行时,ILogger 将消息写入任何已配置的日志记录提供进程。日志消息的确切格式因提供者而异,但图 17.4 显示了控制台提供者如何显示清单 17.2 中的日志消息。

图 17.4 写入默认控制台提供进程的示例日志消息。日志级别类别提供有关消息的重要性和生成位置的信息。事件 ID 提供了一种标识类似日志消息的方法。

消息的确切呈现方式会因日志写入位置而异,但每条日志记录最多包含六个共同元素:

日志级别——日志的日志级别是它的重要程度,由LogLevel枚举定义。
事件类别——类别可以是任意字符串值,但通常设置为创建日志的类的名称。对于 ILogger,类型 T 的全称是类别。
Message——这是日志消息的内容。它可以是静态字符串,也可以包含变量的占位符,如清单 17.2 所示。占位符由大括号 {} 指示,并替换为提供的参数值。
Parameters——如果消息包含占位符,它们与提供的参数相关联。对于清单 17.2 中的示例,Recipes.Count 的值被分配给名为 RecipeCount 的占位符。一些记录器可以提取这些值并将它们公开在您的日志中,如您将在 17.5 节中看到的那样。
Exception——如果发生异常,可以将异常对象连同消息和其他参数一起传递给日志记录函数。除了消息本身之外,记录器还将记录异常。
EventId——这是一个可选的整型错误标识符,可用于快速查找一系列日志消息中所有相似的日志。当用户尝试加载不存在的食谱时,您可以使用 1000 的 EventId,而当用户尝试访问他们无权访问的食谱时,您可以使用 1001 的 EventId。如果您不提供 EventId,将使用值 0。

并非每条日志消息都会包含所有这些元素——例如,您不会总是有异常或参数。将这些元素作为附加方法参数的日志记录方法有多种重载。除了这些可选元素之外,每条消息至少会有一个级别、类别和消息。这些是日志的主要特征,因此我们将依次查看每个特征。

17.2.1 日志级别:日志信息有多重要?

无论何时使用 ILogger 创建日志,都必须指定日志级别。这表示日志消息的严重程度或重要性,它是过滤提供商写入哪些日志以及事后查找重要日志消息的重要因素。

当用户开始编辑食谱时,您可能会创建一个信息级别的日志。这对于跟踪应用进程的流程和行为很有用,但这并不重要,因为一切都很正常。但是,如果在用户尝试保存配方时抛出异常,您可能会创建警告或错误级别的日志。

日志级别通常通过使用 ILogger 接口上的几种扩展方法之一来设置,如清单 17.3 所示。此示例在执行 View 方法时创建一个信息级别的日志,如果找不到所请求的配方,则会创建一个警告级别的错误。

清单 17.3 在 ILogger 上使用扩展方法指定日志级别

private readonly ILogger _log;
public async IActionResult OnGet(int id) //ILogger 实例使用构造函数注入到控制器中。
{
//写入信息级别日志消息
_log.LogInformation("Loading recipe with id {RecipeId}", id); Recipe = _service.GetRecipeDetail(id); if (Recipe is null)
{
//写入警告级别日志消息
_log.LogWarning("Could not find recipe with id {RecipeId}", id);
return NotFound();
}
return Page();
}

LogInformation 和 LogWarning 扩展方法分别创建日志级别为 Information 和 Warning 的日志消息。有六个日志级别可供选择,此处从最重要到最不重要排序:

Critical——对于可能导致应用进程无法正常运行的灾难性故障,例如内存不足异常或硬盘空间不足或服务器着火。
Error——对于无法优雅处理的错误和异常;例如,在 EF Core 中保存编辑后的实体时抛出异常。操作失败,但应用进程可以继续为其他请求和用户运行。
Warning——当出现意外或错误情况时,您可以解决。您可能会为已处理的异常或未找到实体时记录警告,如清单 17.3 所示。
Information——用于跟踪正常的申请流程;例如,在用户登录时记录,或者他们在您的应用进程中查看特定页面时记录。通常,当您需要了解导致错误消息的步骤时,这些日志消息会提供上下文。
Debug——用于跟踪在开发过程中特别有用的详细信息。通常这只有短期用处。
Trace——用于跟踪非常详细的信息,其中可能包含密码或密钥等敏感信息。它很少使用,框架库根本不使用它。

将这些日志级别想象成一个金字塔,如图 17.5 所示。随着日志级别的降低,消息的重要性会下降,但频率会上升。通常,您会在应用进程中发现许多调试级别的日志消息,但(希望)很少有关键或错误级别的消息。

图 17.5 日志级别的金字塔。水平面接近金字塔底部的原木使用频率更高,但不太重要。水平接近顶部的日志应该很少见,但很重要。

当我们在 17.4 节中查看过滤时,这个金字塔形状将变得更有意义。当应用进程处于生产状态时,您通常不想记录应用进程生成的所有调试级别消息。庞大的消息量会让您难以整理,最终可能会用“一切正常!”的消息填满您的磁盘。此外,不应在生产中启用跟踪消息,因为它们可能会泄露敏感数据。通过过滤掉较低的日志级别,您可以确保在生产中生成合理数量的日志,但可以访问开发中的所有日志级别。

一般来说,高级别的日志比低级别的日志更重要,所以Warning日志比Information日志更重要,但还有一个方面要考虑。日志来自何处,或谁创建了日志,是与每条日志消息一起记录的关键信息,称为类别。

17.2.2 日志类别:哪个组件创建了日志

除了日志级别,每条日志消息也有一个类别。您可以为每条日志消息独立设置日志级别,但类别是在您创建 ILogger 实例时设置的。与日志级别一样,该类别对于过滤特别有用,您将在 17.4 节中看到。它被写入每条日志消息,如图 17.6 所示。

图 17.6 每条日志消息都有一个关联的类别,该类别通常是创建日志的组件的类名。默认控制台日志记录提供进程输出每个日志的日志类别。

类别是一个字符串,因此您可以将其设置为任何内容,但约定是将其设置为使用 ILogger 的类型的完全限定名称。在第 17.2 节中,我通过将 ILogger 注入 RecipeController 来实现这一点;通用参数 T 用于设置 ILogger 的类别。

或者,您可以将 ILoggerFactory 注入到您的方法中,并在创建 ILogger 实例时传递显式类别。这允许您将类别更改为任意字符串。

清单 17.4 注入 ILoggerFactory 以使用自定义类别

public class RecipeService
{
private readonly ILogger _log;
public RecipeService(ILoggerFactory factory) //直接注入 ILoggerFactory 而不是 ILogger
{
_log = factory.CreateLogger("RecipeApp.RecipeService"); //在调用 CreateLogger 时将类别作为字符串传递
}
}

还有一个带有通用参数的 CreateLogger() 重载,该参数使用提供的类来设置类别。如果清单 17.4 中的 RecipeService 位于 RecipeApp 命名空间中,则 CreateLogger 调用可以等效地写为

_log = factory.CreateLogger<RecipeService>();

同样,此调用创建的最终 ILogger 实例与直接注入 ILogger 而不是 ILoggerFactory 相同。

提示:除非您出于某种原因使用高度自定义的类别,否则最好将 ILogger 注入到您的方法中,而不是 ILoggerFactory。

每个日志条目的最后必填部分非常明显:日志消息。在最简单的级别上,这可以是任何字符串,但值得仔细考虑哪些信息对记录有用——任何有助于您稍后诊断问题的信息。

17.2.3 格式化消息和捕获参数值

无论何时创建日志条目,都必须提供一条消息。这可以是您喜欢的任何字符串,但正如您在清单 17.2 中看到的,您还可以在消息字符串中包含用大括号 {} 指示的占位符:

_log.LogInformation("Loaded {RecipeCount} recipes", Recipes.Count);

在您的日志消息中包含一个占位符和一个参数值可以有效地创建一个键值对,某些日志记录提供进程可以将其存储为与日志关联的附加信息。先前的日志消息会将 Recipes.Count 的值分配给键 RecipeCount,日志消息本身是通过用参数值替换占位符生成的,以提供以下内容(其中 Recipes.Count=3):

"Loaded 3 recipes"

您可以在日志消息中包含多个占位符,它们将与传递给日志方法的附加参数相关联。格式字符串中占位符的顺序必须与您提供的参数的顺序相匹配。

警告:您必须至少向日志方法传递与消息中的占位符一样多的参数。如果你没有传递足够的参数,你会在运行时得到一个异常。

例如,日志消息

_log.LogInformation("User {UserId} loaded recipe {RecipeId}", 123, 456)

将创建参数 UserId=123 和 RecipeId=456。除了格式化日志消息“用户 123 加载配方 456”之外,结构化日志记录提供进程还可以存储这些值。这使得在日志中搜索特定 UserId 或 RecipeId 变得更加容易。

定义:结构化或语义日志记录附加结构到日志消息,使它们更容易搜索和过滤。它不仅存储文本,还存储额外的上下文信息,通常作为键值对。 JSON 是用于结构化日志消息的通用格式,因为它具有所有这些属性。

并非所有日志记录提供进程都使用语义日志记录。例如,默认的控制台日志记录提供进程不会——消息被格式化为替换占位符,但无法通过键值搜索控制台。

但是,即使您最初不使用结构化日志记录,我也建议您像使用结构化日志一样编写日志消息,并使用明确的占位符和参数。这样,如果您决定稍后添加结构化日志记录提供进程,您将立即看到好处。此外,我发现考虑以这种方式可以记录的参数会提示您记录更多参数值,而不仅仅是一条日志消息。没有什幺比看到“由于重复键无法插入记录”这样的消息但没有记录键值更令人沮丧的了!

提示:一般来说,我是 C# 6 的内插字符串的粉丝,但是当占位符和参数也有意义时,不要将它们用于您的日志消息。使用占位符而不是内插字符串将为您提供相同的输出消息,但也会创建可在以后搜索的键值对。

我们已经研究了很多如何在您的应用进程中创建日志消息,但我们没有关注这些日志的写入位置。在下一节中,我们将了解内置的 ASP.NET Core 日志记录提供进程、它们的配置方式以及如何使用第三方提供进程替换默认设置。

17.3 使用日志提供进程控制日志的写入位置

在本节中,您将了解如何通过向应用进程添加额外的 ILoggerProvider 来控制日志消息的写入位置。例如,除了现有的控制台记录器提供进程之外,您还将看到如何添加一个简单的文档记录器提供进程,将您的日志消息写入文档。在第 17.3.2 节中,您将学习如何使用开源 Serilog 库将默认日志记录基础架构完全替换为替代实现。

到目前为止,我们一直在将所有日志消息写入控制台。如果您在本地运行过任何 ASP.NET Core 示例应用进程,您可能已经看到写入控制台窗口的日志消息。

注意:如果您使用 Visual Studio 并使用 IIS Express 选项(默认)进行调试,您将看不到控制台窗口(尽管日志消息被写入调试输出窗口)。出于这个原因,我通常确保从调试工具栏的下拉列表中选择应用进程名称,而不是 IIS Express。

在调试时将日志消息写入控制台非常有用,但在生产环境中用处不大。没有人会监视服务器上的控制台窗口,并且日志不会保存在任何地方或无法搜索。显然,您需要在其他地方编写生产日志。

正如您在 17.1 节中看到的,日志记录提供进程控制 ASP.NET Core 中日志消息的目的地。他们获取您使用 ILogger 接口创建的消息,并将它们写入输出位置,具体取决于提供进程。

注意:这个名字总是让我印象深刻——日志提供者有效地使用你创建的日志消息并将它们输出到目的地。你大概可以从图 17.3 中看出名字的由来,但我仍然觉得它有点违反直觉。

Microsoft 已经为 ASP.NET Core 编写了多个第一方日志提供进程,它们在 ASP.NET Core 中开箱即用。这些包括

Console provider —— 将消息写入控制台,如您所见。
Debug provider——例如,当您在 Visual Studio 或 Visual Studio Code 中调试应用进程时,将消息写入调试窗口。
EventLog provider——将消息写入Windows事件日志。仅在 Windows 上运行时输出日志消息,因为它需要特定于 Windows 的 API。
EventSource provider——使用 Windows 事件跟踪(ETW)或 Linux 上的 LTTng 跟踪写入消息。

还有许多第三方日志记录提供进程实现,例如 Azure App Service 提供进程、elmah.io 提供进程和 Elasticsearch 提供进程。最重要的是,还有与其他预先存在的日志记录框架(如 NLog 和 Serilog)的集成。看看你最喜欢的 .NET 日志库或服务是否有 ASP.NET Core 的提供进程总是值得的,大多数情况下都是如此。

您可以使用 HostBuilder 在 Program.cs 中为您的应用进程配置日志记录提供进程。 CreateDefaultBuilder 帮助器方法会自动为您的应用进程配置控制台和调试提供进程,但您可能希望更改或添加这些内容。

当您需要为您的应用进程自定义日志记录时,您有两种选择:

使用你自己的 HostBuilder 实例,而不是 Host.CreateDefaultBuilder,并显式配置它。
在 CreateDefaultBuilder 之后添加一个额外的 ConfigureLogging 调用。

如果您只需要自定义日志记录,则后一种方法更简单。但是,如果您发现您还想自定义由 CreateDefaultBuilder 创建的 HostBuilder 的其他方面(例如您的应用进程配置设置),则可能值得放弃 CreateDefaultBuilder 方法并改为创建您自己的实例。

在第 17.3.1 节中,我将展示如何向您的应用进程添加一个简单的第三方日志记录提供进程,该提供进程将日志消息写入文档,以便您的日志持久化。在部分

17.3.2 我将展示如何更进一步,将 ASP.NET Core 中的默认 ILoggerFactory 替换为使用流行的开源 Serilog 库的替代实现。

17.3.1 向您的应用进程添加新的日志记录提供进程

在本节中,我们将添加一个写入滚动文档的日志记录提供进程,以便我们的应用进程每天将日志写入一个新文档。我们也将继续使用控制台和调试提供进程进行日志记录,因为在本地开发时它们比文档提供进程更有用。

要在 ASP.NET Core 中添加第三方日志记录提供进程,请执行以下步骤:

    将日志记录提供进程 NuGet 包添加到解决方案中。我将使用一个名为 NetEscapades.Extensions.Logging.RollingFile 的提供进程,它在 NuGet 和 GitHub 上可用。您可以使用 Visual Studio 中的 NuGet 包管理器将其添加到您的解决方案中,或使用 .NET CLI 通过运行
    dotnet add package NetEscapades.Extensions.Logging.RollingFile
    从您的应用进程的项目文档夹。
    注意:此包是一个简单的文档日志记录提供进程,可从 https://github.com/andrewlock/NetEscapades.Extensions.Logging 获得。它基于 Azure App Service 日志提供进程。如果您想对日志进行更多控制,例如指定文档格式,请考虑改用 Serilog,如第 17.3.2 节所述。
    使用 IHostBuilder.ConfigureLogging() 扩展方法添加日志提供进程。您可以通过调用 AddFile() 添加文档提供进程,如下一个清单所示。 AddFile() 是日志记录提供进程包提供的扩展方法,用于简化将提供进程添加到您的应用进程的过程。

清单 17.5 将第三方日志记录提供进程添加到 IHostBuilder

public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
} public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args) // CreateDefaultBuilder 方法正常配置控制台和调试提供进程。
.ConfigureLogging(builder => builder.AddFile()) //将新的文档日志记录提供进程添加到记录器工厂
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}

注意: 添加新的提供者不会替换现有的提供者。清单 17.5 使用 CreateDefaultBuilder 辅助方法,因此控制台和调试日志记录提供进程已经添加。要删除它们,请在 ConfigureLogging 方法的开头调用构建器 .ClearProviders(),或使用自定义 HostBuilder。

配置文档日志记录提供进程后,您可以运行应用进程并生成日志。每次您的应用进程使用 ILogger 实例写入日志时,ILogger 都会将消息写入所有已配置的提供进程,如图 17.7 所示。控制台消息很方便,但您还可以将日志的持久记录存储在文档中。

提示: 默认情况下,滚动文档提供进程会将日志写入应用进程的子目录。您可以使用 AddFile() 的重载指定其他选项,例如文档名和文档大小限制。对于生产,我建议使用更成熟的日志记录提供进程,例如 Serilog。

图 17.7 使用 ILogger 记录消息将使用所有已配置的提供进程写入日志。例如,这允许您将方便的消息记录到控制台,同时将日志保存到文档中。

清单 17.5 的关键要点是提供者系统使得将现有的日志框架和提供者与 ASP.NET Core 日志抽象集成变得容易。无论您选择在应用进程中使用哪个日志记录提供进程,原则都是相同的:在 IHostBuilder 上调用 ConfigureLogging 并使用扩展方法添加新的日志记录提供进程,例如 AddConsole() 或本例中的 AddFile()。

在某些情况下,将应用进程消息记录到文档中可能很有用,这肯定比在生产环境中记录到不存在的控制台窗口要好,但它可能仍然不是最佳选择。

如果你在生产中发现了一个错误,你需要快速查看日志以了解发生了什幺,例如,你需要登录到远程服务器,在磁盘上找到日志文档,并通过它们来查找错误问题。如果您有多个 Web 服务器,那幺在开始解决错误之前,您将需要完成一项艰巨的工作来获取所有日志。不好玩。再加上文档许可或驱动器空间问题的可能性,文档日志记录似乎不那么吸引人了。

相反,最好将日志发送到与应用进程分开的集中位置。该位置的确切位置取决于您;关键是您的应用进程的每个实例都将其日志发送到同一个位置,与应用进程本身分开。

如果您在 Azure 上运行您的应用进程,您可以免费获得集中式日志记录,因为您可以使用 Azure 应用进程服务提供商收集日志。或者,您可以将日志发送到第三方日志聚合器服务,例如 Loggr (http://loggr.net/)、elmah.io (https://elmah.io/) 或 Seq (https://getseq.net/)。您可以在 NuGet 上为这些服务中的每一个找到 ASP.NET Core 日志记录提供进程,因此添加它们与添加您已经看到的文档提供进程的过程相同。

另一个流行的选择是使用开源 Serilog 库同时写入各种不同的位置。在下一节中,我将展示如何在您的应用进程中用 Serilog 替换默认的 ILoggerFactory 实现,从而为您的日志写入位置提供广泛的可能选项。

17.3.2 用 Serilog 替换默认的 ILoggerFactory

在本节中,我们将配方应用中的默认 ILoggerFactory 替换为使用 Serilog 的实现。Serilog (https://serilog.net) 是一个开源项目,可以将日志写入许多不同的位置,例如文档、控制台、Elasticsearch 集群或数据库。这类似于默认 ILoggerFactory 获得的功能,但由于 Serilog 的成熟,您可能会发现您可以写入更多地方。

注:Elasticsearch是一个基于REST的搜索引擎,通常用于聚合日志。您可以在 www.elastic.co/elasticsearch/ 找到更多信息。

Serilog 早于 ASP.NET Core,但由于围绕 ILoggerFactory 和 ILoggerProvider 的日志记录抽象,您可以轻松地与 Serilog 集成,同时仍在应用进程代码中使用 ILogger 抽象。

Serilog 使用与 ASP.NET Core 日志记录抽象类似的设计理念 —— 将日志写入中央日志记录对象,并将日志消息写入多个位置,例如控制台或文档。 Serilog 将这些位置中的每一个都称为接收器。

注:有关可用接收器的完整列表,请参阅 https://github.com/serilog/serilog/wiki/Provided-Sinks。在撰写本文时,有93种不同的 sinks!

当您将 Serilog 与 ASP.NET Core 一起使用时,您通常会将默认的 ILoggerFactory 替换为包含单个日志记录提供进程 SerilogLoggerProvider 的自定义工厂。这个提供者可以写入多个位置,如图 17.8 所示。此配置与标准 ASP.NET Core 日志记录设置有点不同,但它可以防止 Serilog 的功能与默认 LoggerFactory 的等效功能冲突,例如过滤(请参阅第 17.4 节)。

提示:如果您熟悉 Serilog,则可以使用本节中的示例轻松地将有效的 Serilog 配置与 ASP.NET Core 日志基础结构集成。

图 17.8 将 Serilog 与 ASP.NET Core 一起使用时的配置与默认日志记录配置的比较。您可以使用这两种方法实现相同的功能,但您可能会发现 Serilog 提供了用于添加额外功能的其他库。

在本节中,我们将添加一个接收器以将日志消息写入控制台,但使用 Serilog 日志记录提供进程而不是内置控制台提供进程。设置完成后,添加额外的接收器以写入其他位置是微不足道的。将 Serilog 日志记录提供进程添加到应用进程涉及三个步骤:

在本部分中,我们将添加一个接收器以将日志消息写入控制台,但使用 Serilog 日志记录提供进程而不是内置控制台提供进程。设置完成此设置后,添加其他接收器以写入其他位置是微不足道的。将 Serilog 日志记录提供进程添加到应用进程涉及三个步骤:

    将所需的 Serilog NuGet 包添加到解决方案中。
    创建 Serilog 记录器,并为其配置所需的接收器。
    在 IHostBuilder 上调用 UseSerilog() 以将默认的 ILoggerFactory 实现替换为 SerilogLoggerFactory。这将自动配置 Serilog 提供进程并挂接已配置的 Serilog 记录器。

若要将 Serilog 安装到 ASP.NET Core 应用中,需要为所需的任何接收器添加基本 NuGet 包和 NuGet 包。您可以通过Visual Studio NuGet GUI,使用PMC或使用.NET CLI执行此操作。若要添加 Serilog ASP.NET Core 包和用于写入控制台的接收器,请运行以下命令:

dotnet add package Serilog.ASP.NET Core 
dotnet add package Serilog.Sinks.Console

这会将必要的 NuGet 包添加到项目文档并还原它们。接下来,创建一个 Serilog 记录器,并通过添加控制台接收器将其配置为写入控制台,如清单 17.6 所示。这是最基本的 Serilog 配置,但您也可以在此处添加额外的接收器和配置。我还在对 CreateHostBuilder 的调用周围添加了一个 try-catch-finally 块,以确保在启动 Web 主机时出错或出现致命异常时仍会写入日志。最后,Serilog logger 工厂是通过在 IHostBuilder 上调用 UseSerilog() 来配置的。

注:您可以自定义 Serilog,因此值得查阅文档以了解可能的情况。可参考以下网址:https://github.com/serilog/serilog/wiki/Configuration-Basics。

清单 17.6 配置Serilog日志记录提供进程以使用控制台接收器

public class Program
{
public static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration() // 创建记录器配置用于配置Serilog记录器
.WriteTo.Console() // Serilog 会将日志写入控制台。
.CreateLogger(); // 这将在静态 Log.Logger 属性上创建一个 Serilog 记录器实例。
try
{
CreateHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
} public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args)
.UseSerilog() // 注册 SerilogLoggerFactory 并将 Log.Logger 连接为唯一的日志记录提供进程
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}

配置 Serilog 日志记录提供进程后,可以运行应用进程并生成一些日志。每次应用生成日志时,ILogger 都会将消息写入 Serilog 提供进程,后者将其写入每个接收器。在清单 17.6 中,您只配置了控制台接收器,因此输出如图 17.9 所示。Serilog 控制台接收器比内置控制台提供进程更能为日志着色,因此我发现直观地分析日志要容易一些。

提示:除此之外,Serilog还有许多很棒的功能。我最喜欢的功能之一是添加丰富器的能力。它们会自动向所有日志消息添加信息,例如进程 ID 或环境变量,这在诊断问题时非常有用。有关为 ASP.NET Core 应用进程配置 Serilog 的推荐方法的深入信息,请参阅 Serilog:http://mng.bz/Yqvz 的创建者 Nicholas Blumhardt 撰写的“在 ASP.NET Core 3 中设置 Serilog”文章。

图 17.9 使用 Serilog 提供进程和控制台接收器的示例输出。输出比内置控制台提供进程具有更多的着色,但默认情况下它不显示每个日志的日志类别。

注:如果要在控制台接收器中显示日志类别,可以自定义输出模板并添加 {SourceContext}。有关详细信息,请参阅 GitHub 上的自述文档:https://github.com/serilog/serilog-sinks-console#output-templates。

Serilog 允许您轻松地将其他接收器插入到应用进程中,其方式与使用默认 ASP.NET Core 抽象的方式大致相同。您是否选择使用 Serilog 或坚持使用其他提供商取决于您;功能集非常相似,尽管Serilog更成熟。无论您选择哪种方式,一旦您开始在生产环境中运行应用进程,您很快就会发现一个不同的问题:您的应用进程生成的日志消息数量庞大!

17.4 通过过滤更改日志详细程度

在本节中,您将看到如何减少写入记录器提供进程的日志消息的数量。您将学习如何应用基本级别的过滤器、过滤掉来自特定命名空间的消息以及使用特定于日志记录提供进程的过滤器。

如果您一直在尝试日志记录示例,您可能会注意到您收到了很多日志消息,即使是对于如图 17.2 中的单个请求也是如此:来自 Kestrel 服务器的消息,来自 EF Core 的消息,更不用说您自己的自定义消息了。当你在本地调试时,访问所有详细信息非常有用,但在生产中你会被噪音淹没,以至于很难挑选出重要的消息。

ASP.NET Core 包括在日志消息被写入之前过滤掉它们的能力,基于三件事的组合:

消息的日志级别
记录者的类别(创建日志的人)
记录器提供者(日志将被写入的地方)

您可以使用这些属性创建多个规则,并且对于创建的每个日志,将应用最具体的规则来确定是否应将日志写入输出。您可以创建以下三个规则:

默认最低日志级别为信息 — 如果没有其他规则适用,则只有日志级别为“信息”或以上的日志才会写入提供商。
对于以微软开头的类别,最低日志级别为警告 — 在以微软开头的命名空间中创建的任何记录器只会写入日志级别为警告或以上的日志。这将过滤掉您在图 17.6 中看到的嘈杂的框架消息。
对于控制台提供进程,最低日志级别为错误 — 写入控制台提供进程的日志必须具有最低日志级别错误。较低级别的日志不会写入控制台,尽管它们可能使用其他提供进程写入。

通常,日志筛选的目标是减少写入某些提供进程或从某些命名空间(基于日志类别)写入的日志数。图 17.10 显示了一组适用于控制台和文档日志记录提供进程的可能筛选规则。

图 17.10 将过滤规则应用于日志消息以确定是否应写入日志。对于每个提供者,选择最具体的规则。如果日志超过规则要求的最低级别,则提供者写入日志;否则它会丢弃它。

在此示例中,控制台记录进程显式限制在 Microsoft 命名空间中写入的日志为“警告”或更高版本,因此控制台记录进程将忽略显示的日志消息。相反,文档记录器没有显式限制 Microsoft 命名空间的规则,因此它使用配置的最低信息级别并将日志写入输出。

提示:在决定是否应写入日志消息时,仅选择单个规则;它们没有组合在一起。在图 17.10 中,规则 1 被认为比规则 5 更具体,因此日志将写入文档提供进程,即使两者在技术上都适用。

通常使用第 11 章中讨论的分层配置方法定义应用的日志记录规则集,因为这使您可以在开发和生产中运行时轻松使用不同的规则。您可以通过在 Program.cs 中配置日志记录时调用 AddConfiguration 来执行此操作,但 CreateDefaultBuilder() 也会自动为您执行此操作。

此清单显示了在配置自己的主机生成器(而不是使用 CreateDefaultBuilder 帮助进程方法)时如何向应用进程添加配置规则。

清单 17.7 在配置日志记录中加载日志记录配置

public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
} public static IHostBuilder CreateHostBuilder(string[] args) => new HostBuilder()
// 从 appsettings.json 加载配置值
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureAppConfiguration(config => config.AddJsonFile("appsettings.json"))
.ConfigureLogging((ctx, builder) =>
{
// 从“日志记录”部分加载日志筛选配置并添加到 ILoggingBuilder
builder.AddConfiguration( ctx.Configuration.GetSection("Logging"));
builder.AddConsole(); // 将控制台提供进程添加到应用
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}

在此示例中,我从单个文档 appsettings.json 加载了配置,其中包含我们所有的应用配置。日志记录配置专门包含在 IConfiguration 对象的日志记录部分中,该部分在调用 Configurelogging() 时可用。

提示:正如您在第 11 章中看到的,您可以从多个源(如 JSON 文档和环境变量)加载配置设置,并可以根据 IHostingEnvironment 有条件地加载它们。一种常见的做法是在 appsettings.json 中包含生产环境的日志记录设置,并在 appsettings 中包含本地开发环境的替代。开发.json.

配置的日志记录部分应类似于下面的清单,其中显示了如何定义图 17.10 中所示的规则。

示例 17.8 appsettings.json 的日志过滤配置部分

{
"Logging": {
// 如果没有适用于提供商的规则,则适用此规则
"LogLevel": {
"Default": "Debug",
"System": "Warning", "Microsoft": "Warning"
},
// 应用于文档提供进程的规则
"File": {
"LogLevel": {
"Default": "Information"
}
},
// 应用于控制台提供进程的规则
"Console": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Warning"
}
}
}
}

创建日志记录规则时,要牢记的重要一点是,如果您有任何特定于提供者的规则,这些规则将优先于 LogLevel 部分中定义的基于类别的规则。因此,对于清单 17.8 中定义的配置,如果您的应用进程仅使用文档或控制台日志记录提供进程,则 LogLevel 部分中的规则实际上永远不会适用。

如果您觉得这令人困惑,请不要担心,我也是。每当我设置日志记录时,我都会检查用于确定哪个规则适用于给定提供商和类别的算法,如下所示:

    选择给定提供商的所有规则。如果没有规则适用,请选择所有未定义提供者的规则(清单 17.8 中顶部的 LogLevel 部分)。
    从选择的规则中,选择具有最长匹配类别前缀的规则。如果没有选定的规则与类别前缀匹配,则选择默认(如果存在)。
    如果选择了多个规则,则使用最后一个。
    如果未选择任何规则,则使用全局最低级别 LogLevel:Default(在清单 17.8 中为 Debug)。

其中每个步骤(最后一个步骤除外)都会缩小日志消息的适用规则范围,直到只剩下一个规则。您在图 17.10 中看到这对 Microsoft 类别日志生效。图 17.11 更详细地显示了该过程。

图 17.11 从控制台提供进程的可用规则集中选择要应用的规则和信息级别日志。每个步骤都会减少应用的规则数量,直到只剩下一个规则。

警告:日志过滤规则不会合并在一起;选择单个规则。包含特定于提供商的规则将覆盖全局特定于类别的规则,因此我倾向于在我的应用进程中坚持特定于类别的规则,以使整套规则更容易理解。

通过一些有效的过滤,您的生产日志应该更易于管理,如图 17.12 所示。一般来说,我发现最好将来自 ASP.NET Core 基础结构和引用库的日志限制为警告或更高级别,同时将我的应用进程写入的日志保留在开发中的调试和生产中的信息中。

图 17.12 使用过滤减少写入的日志数量。在此示例中,类别筛选器已添加到 Microsoft 和系统命名空间,因此仅记录警告及更高级别的日志。这会增加与应用进程直接相关的日志比例。

这接近于 ASP.NET Core 模板中使用的默认配置。您可能会发现需要添加其他特定于类别的筛选器,具体取决于您使用的 NuGet 库以及它们写入的类别。找出答案的最佳方法通常是运行您的应用进程,看看您是否被无趣的日志消息淹没。

即使您的日志详细程度得到控制,如果您坚持使用默认的日志记录提供进程,例如文档或控制台记录器,从长远来看,您可能会后悔。这些日志提供进程工作得非常好,但是在查找特定错误消息或分析日志时,您的工作会很费力。在下一节中,您将看到结构化日志记录如何帮助解决这个问题。

17.5 结构化日志:创建可搜索的、有用的日志

在本节中,您将了解结构化日志记录如何使处理日志消息变得更加容易。您将学习将键值对附加到日志消息,以及如何使用结构化日志记录提供进程 Seq 存储和查询键值。最后,您将学习如何使用范围将键值对附加到块中的所有日志消息。

假设您已经将我们一直致力于生产的食谱应用进程推出。您已将日志记录添加到应用进程,以便您可以跟踪应用进程中的任何错误,并将日志存储在文档中。

一天,一位顾客打电话说他们无法查看他们的食谱。果然,当您查看日志消息时,您会看到一条警告:

warn: RecipeApplication.Controllers.RecipeController[12] Could not find recipe with id 3245

这激起了你的兴趣——为什幺会这样?这位客户以前发生过这种情况吗?这个食谱以前发生过吗?其他食谱有没有发生过?它经常发生吗?

你会如何回答这些问题?鉴于日志存储在文本文档中,您可能会开始在您选择的编辑器中进行基本的文本搜索,查找短语 Could not find recipe with id。根据您的记事本技能,您可能会以公平的方式回答您的问题,但这可能是一个费力、容易出错且痛苦的过程。

限制因素是日志存储为非结构化文本,因此文本处理是您唯一可用的选项。更好的方法是以结构化格式存储日志,以便可以轻松查询日志、筛选日志并创建分析。结构化日志可以以任何格式存储,但如今它们通常表示为 JSON。例如,同一配方警告日志的结构化版本可能如下所示:

{
  "eventLevel": "Warning",
  "category": "RecipeApplication.Controllers.RecipeController", 
  "eventId": "12",
  "messageTemplate": "Could not find recipe with {recipeId}",
  "message": "Could not find recipe with id 3245", "recipeId": "3245"
}

这种结构化的日志信息包含所有与非结构化版本相同的细节,但其格式可以让你轻松地搜索到特定的日志条目。它使你能简单地按事件级别过滤日志,或只显示与特定配方 ID 有关的日志。

注意:这只是一个结构化日志的例子。日志所使用的格式将根据所使用的日志提供者而变化,可能是任何东西。关键的一点是,日志的属性可以作为键值对使用。

为你的应用程序添加结构化日志需要一个能够创建和存储结构化日志的日志提供者。Elasticsearch 是一个流行的通用搜索和分析引擎,可以用来存储和查询你的日志。Serilog 包括一个向 Elasticsearch 写日志的汇,你可以按照 17.3.2 节中添加控制台汇的方式添加到你的应用程序中。

提示:如果你想了解更多关于 Elasticsearch 的信息,Doug Turnbull 和 John Berryman 的《Relevant Search》(Manning,2016)是一个不错的选择。它将展示你如何充分利用你的结构化日志。

Elasticsearch 是一个强大的生产规模的引擎,用于存储你的日志,但设置它并不适合胆小的人。即使你已经启动并运行了它,也有一个与查询语法相关的陡峭的学习曲线。如果你对结构化日志的需求感兴趣,Seq(https://getseq.net)是一个不错的选择。在下一节中,我将向你展示如何添加Seq作为结构化日志提供者,使分析你的日志更加容易。

17.5.1 向应用添加结构化日志记录提供进程

为了演示结构化日志记录的优势,在本部分中,你将配置一个应用以将日志写入 Seq。您将看到配置与非结构化提供进程基本相同,但结构化日志记录提供的可能性使其不费吹灰之力。

Seq 安装在服务器或本地计算机上,通过 HTTP 收集结构化日志消息,提供 Web 界面供您查看和分析日志。它目前可作为Windows应用进程或Linux Docker容器使用。您可以安装用于开发的免费版本,这将允许您尝试结构化日志记录。

注:您可以从 https://getseq.net/Download 下载 Windows 安装进程 Seq。

从应用的角度来看,添加 Seq 提供进程的过程应该很熟悉:

    使用 Visual Studio 或 .NET CLI 安装 Seq 日志记录提供进程

    dotnet add package Seq.Extensions.Logging

    在 Program.cs 的 ConfigureLogging 方法中添加 Seq 日志记录提供进程。若要添加 Seq 提供进程以及作为 CreateDefaultBuilder 一部分包含的控制台和调试提供进程,请使用

    Host.CreateDefaultBuilder(args)
        .ConfigureLogging(builder => builder.AddSeq())
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup();
        });

这就是将 Seq 添加到您的应用进程所需的全部。当您在本地环境中安装了 Seq 时,这会将日志发送到默认的本地 URL。 AddSeq() 扩展方法包括额外的重载以在您转向生产时自定义 Seq,但这就是您开始本地试验所需的全部内容。

如果您还没有,请在您的开发机器上安装 Seq 并导航到位于 http://localhost:5341 的 Seq 应用进程。在另一个选项卡中,打开您的应用进程并开始浏览并生成日志。回到 Seq,如果你刷新页面,你会看到一个日志列表,如图 17.13 所示。单击日志可将其展开并显示为该日志记录的结​​构化数据。

ASP.NET Core 通过将消息格式字符串中捕获的每个参数视为键值对来支持结构化日志记录。如果您使用以下格式字符串创建日志消息,

_log.LogInformation("Loaded {RecipeCount} recipes", Recipes.Count);

日志记录提供进程将创建一个值为 Recipes.Count 的 RecipeCount 参数。这些参数作为属性添加到每个结构化日志中,如图 17.13 所示。

图 17.13 串行 UI。日志以列表形式呈现。您可以查看单个日志的结构化日志记录详细信息、查看聚合日志的分析以及按日志属性搜索。

结构化日志通常比标准问题控制台输出更易于阅读,但当您需要回答特定问题时,它们的真正功能就来了。考虑之前的问题,您会看到此错误:

Could not find recipe with id 3245

您想了解问题的普遍程度。第一步是确定此错误发生了多少次,并查看它是否发生在任何其他食谱上。Seq 允许您过滤日志,但它也允许您创建 SQL 查询来分析数据,因此找到问题的答案只需几秒钟,如图 17.14 所示。

图 17.14 按顺序查询日志 结构化日志记录使日志分析(如此示例)变得容易。

注意:对于简单的查询,您不需要像 SQL 这样的查询语言,但它可以更轻松地挖掘数据。其他结构化日志提供者可能会提供 SQL 以外的查询语言,但原理与本 Seq 示例相同。

快速搜索显示您已经记录了 EventId.Id=12(我们感兴趣的警告的 EventId)的日志消息 13 次,并且每次违规的 RecipeId 都是 3245。这表明可能有问题特别是那个食谱,它为您指出了正确的方向来找到问题。

通常情况下,找出生产中的错误涉及像这样记录侦探工作以隔离问题发生的位置。结构化日志记录使这个过程变得更加容易,因此无论您选择 Seq、Elasticsearch 还是其他提供商,都值得考虑。

我已经描述了如何使用消息中的变量和参数将结构化属性添加到日志消息中,但是正如您在图 17.13 中所见,可见的属性远远多于消息中存在的属性。

范围提供了一种向日志消息添加任意数据的方法。它们在一些非结构化日志提供进程中可用,但在与结构化日志提供进程一起使用时它们会大放异彩。在本章的最后一节中,我将演示如何使用它们将额外数据添加到日志消息中。

17.5.2 使用作用域向日志添加其他属性

你经常会在应用中发现,有一组操作都使用相同的数据,这对于附加到日志非常有用。例如,您可能有一系列数据库操作,这些操作都使用相同的事务 ID,或者您可能使用相同的用户 ID 或配方 ID 执行多个操作。日志记录作用域提供了一种将相同数据关联到此类组中的每个日志消息的方法。

定义 日志记录范围用于通过将相同的数据添加到每个日志消息来对多个操作进行分组。

ASP.NET Core 中的日志记录作用域是通过调用 ILogger.BeginScope(T 状态)并提供要记录的状态数据来创建的。您可以在使用块中创建范围;在作用域块内写入的任何日志消息都将具有关联的数据,而外部的日志消息则不会。

17.9 使用 BeginScope 为日志消息添加作用域属性

_logger.LogInformation("No, I don't have scope");   // 在作用域块之外写入的日志消息不包括作用域状态。
using(_logger.BeginScope("Scope value")) // 调用 BeginScope 将启动一个范围块,其范围状态为“范围值”。
// 可以将任何内容作为作用域的状态传递。
using(_logger.BeginScope(new Dictionary<string, object>{{ "CustomValue1", 12345 } }))
{
_logger.LogInformation("Yes, I have the scope!"); // 在作用域块内写入的日志消息包括作用域状态。
} _logger.LogInformation("No, I lost it again");

范围状态可以是任何对象:例如,int、字符串或字典。由每个日志记录提供进程实现决定如何处理您在 BeginScope 调用中提供的状态,但通常它将使用 ToString() 进行串行化。

提示:我发现的作用域最常见的用途是将其他键值对附加到日志。若要在 Seq 和 Serilog 中实现此行为,需要传递 Dictionary 作为状态对象。

注:Serilog 和 Seq 的创建者 Nicholas Blumhardt 在他的博客“ILogger.BeginScope()”文章中有例子和理由:http://mng.bz/GxDD。

写入作用域块内的日志消息时,作用域状态将被捕获并作为日志的一部分写入,如图 17.15 所示。键值对的字典<>直接添加到日志消息 (CustomValue1) 中,其余状态值添加到 Scope 属性中。您可能会发现字典方法在两者中更有用,因为添加的属性更容易过滤,如图 17.14 所示。

图 17.15 使用作用域向日志添加属性。使用字典方法添加的范围状态将添加为结构化日志记录属性,但其他状态将添加到 Scope 属性中。通过添加属性,可以更轻松地将相关日志相互关联。

这就把我们带到了关于日志记录的本章的结尾。无论你是使用内置日志记录提供进程,还是选择使用第三方提供进程(如 Serilog 或 NLog),ASP.NET Core 都可以轻松获取应用代码的详细日志,还可以轻松获取构成应用基础结构的库(如 Kestrel 和 EF Core)的详细日志。无论您选择哪种方式,我都鼓励您添加比您认为需要的更多的日志 - 将来 - 在跟踪问题时,您会感谢我。

在下一章中,我们将详细介绍构建应用进程时应考虑的各种 Web 安全问题。 ASP.NET Core 会自动为您处理其中一些问题,但了解应用进程的漏洞所在非常重要,这样您就可以尽可能地缓解它们。

总结

日志记录对于快速诊断生产应用进程中的错误至关重要。您应该始终为您的应用进程配置日志记录,以便将日志写入持久位置。
您可以通过注入 ILogger 将日志记录添加到您自己的服务中,其中 T 是服务的名称。或者,注入 ILoggerFactory 并调用 CreateLogger()。
消息的日志级别表示消息的重要性,范围从 Trace 到 Critical。通常,您会创建许多低重要性日志消息和一些高重要性日志消息。
您可以通过使用 ILogger 的适当扩展方法来创建日志来指定日志的日志级别。要写入信息级别的日志,请使用 ILogger.LogInformation(message)。
日志类别表示哪个组件创建了日志。它通常设置为创建日志的类的完全限定名称,但您可以根据需要将其设置为任何字符串。 ILogger 的日志类别为 T。
您可以使用占位符值来格式化消息,类似于 string.Format 方法,但对参数使用有意义的名称。调用 logger.LogInfo(Loading Recipe with id {RecipeId}, 1234) 将创建一个日志读取 Loading Recipe with id 1234,但它也会捕获值 RecipeId=1234。这种结构化的日志记录使分析日志消息变得更加容易。
ASP.NET Core 包含许多开箱即用的日志记录提供进程。这些包括控制台、调试、事件日志和事件源提供进程。或者,您可以添加第三方日志记录提供进程。
您可以在ASP.NET Core 中配置多个ILoggerProvider 实例,这些实例定义了日志的输出位置。 CreateDefaultBuilder 方法添加控制台和调试提供进程,您可以通过调用 ConfigureLogging() 添加其他提供进程。
Serilog 是一个成熟的日志框架,包括对大量输出位置的支持。您可以使用 Serilog.ASP.NET Core 包将 Serilog 添加到您的应用进程。这会将默认的 ILoggerFactory 替换为特定于 Serilog 的版本。
您可以使用配置控制日志记录输出的详细程度。CreateDefaultBuilder 帮助进程使用日志记录配置部分来控制输出详细程度。与开发应用进程时相比,您通常会在生产中筛选出更多日志。
在决定是否输出日志消息时,只为每个日志记录提供进程选择一个日志过滤规则。根据日志记录提供进程和日志消息的类别选择最具体的规则。
结构化日志记录涉及记录日志,以便可以轻松查询和过滤日志,而不是输出到控制台的默认非结构化格式。这使得分析日志、搜索问题和识别模式变得更加容易。
您可以使用作用域块向结构化日志添加其他属性。作用域块是通过在 using 块中调用 ILogger.BeginScope(state) 来创建的。状态可以是任何对象,并添加到作用域块内的所有日志消息中。

第17章 使用日志记录监视和排除错误(ASP.NET Core in Action, 2nd Edition)的相关教程结束。

《第17章 使用日志记录监视和排除错误(ASP.NET Core in Action, 2nd Edition).doc》

下载本文的Word格式文档,以方便收藏与打印。