日志记录的主要目的是跟踪软件与其用户之间的各种交互:状态更改、对内部资源的访问、响应用户操作而触发的事件处理程序、内部模块引发的异常等。由于此活动监视任务是在应用程序运行时执行的,因此通常会显示(或记录)每个日志条目,并带有表示记录事件发生的时刻的时间戳值。
在本章中,在简要概述了日志记录的概念和重要性之后,我们将学习如何使用 ILogger 接口以及实现它的一些第三方组件为 Web API 项目创建结构化日志记录机制。
7.1 应用程序日志记录概述在计算机科学中,日志是指系统执行的操作(在执行时)的顺序和时间顺序记录。这些操作可以由多个参与者执行:用户、系统管理员、自动计划任务和由系统本身发起的任务。

扩展后,术语日志还指存储这些记录的文件(或文件集)。当有人要求我们“签出日志”时,我们知道日志是指日志文件。但是,我们为什么要使用这个动词?它从何而来?
7.1.1 从船到计算机当电子产品不存在时,术语原木仅用于指代一块木头。但它作为录音的同义词的使用并不是从计算机或信息技术开始的;它起源于18世纪左右。在当时的航海术语中,原木是指一块绑在绳子上的木头,该绳子被留在船上漂浮。绳子上有一系列的结,水手们可以随时数数以测量船的速度;速度由水面上方的节数决定。这种基本而有效的测量仪器是为什么今天船速仍然以节表示的原因。
然而,这种技术远非完美。水手们知道船的速度可能受到几个内部和外部因素的影响,例如帆或发动机性能,大气条件以及风力和风向。出于这个原因,定期重复称为日志记录的基于日志的结测量任务。每个记录活动都与其他相关信息(例如时间、风和天气)一起记录在一个特殊的登记册中。此寄存器(通常称为日志)是正确日志文件的第一个示例。正如我们所看到的,这种独特的测量技术不仅为现代伐木概念提供了名称,而且还提供了基础。
计算机术语中日志概念的采用发生在 1960 年代初,并导致引入了相关定义,包括登录和注销。从那一刻起,日志记录在 IT 行业的重要性呈指数级增长,导致一个新的市场部门(日志管理)的诞生,该部门在 1 年价值超过 2 亿美元,并且逐年增长。这种扩展显然是由 IT 安全性、合规性、审计、监控、商业智能和数据分析框架日益增长的重要性决定的,这些框架严重依赖系统和应用程序日志。
7.1.2 为什么我们需要日志?我们可能想知道日志记录系统可以为我们的 Web API 做什么,或者为什么我们应该关心实现它。解决此问题的最佳方法是枚举应用程序中实际上可以从日志记录系统中受益的最重要方面:
稳定性 - 日志记录使我们能够及时检测和调查错误和/或未处理的异常,从而促进修复它们的过程并最大限度地减少应用程序的停机时间。安全性 - 日志记录系统的存在有助于我们确定系统是否已受到损害,如果是,则在多大程度上受到损害。日志记录还允许我们识别攻击者发现的漏洞以及攻击者通过利用这些漏洞能够执行的恶意操作。业务连续性 — 日志记录系统可以使系统管理员在异常行为变得严重之前意识到异常行为,和/或可以通过警报、电子邮件和其他基于实时通知的警报流程及时通知他们崩溃。合规性要求 — 大多数国际 IT 安全法规、标准和准则都需要精确的日志记录策略。近年来,通过在欧盟境内引入《通用数据保护条例》(GDPR)和其他地方的其他数据保护条例,这种方法得到了进一步加强。重要的是要了解“读取”日志(并采取相应行动)的行为不必由人类执行。良好的日志记录实践始终需要手动检查和监视系统,该系统可以配置为在发生常见事件(重新启动有缺陷或无法正常工作的服务、启动维护脚本等)时自动应用一些预设的补救措施。大多数现代 IT 安全软件都提供高度自动化的安全运营中心 (SOC) 框架和/或安全、编排、自动化和响应 (SOAR) 集成解决方案。综上所述,我们可以很容易地看到,日志管理不仅是控制Web(和非Web)应用程序的有用工具,而且是保证其整体可靠性的基本要求以及IT安全的基本资产。
7.2 ASP.NET 日志记录既然我们已经认识到日志记录的重要性,那么让我们使用 .NET 框架的工具来管理应用程序的这一重要方面。这些工具是通过 ILogger 接口提供的标准化通用日志记录 API,它允许我们通过一系列内置和/或第三方日志记录提供程序记录我们想要记录的事件和活动。
从技术上讲,ILogger API 不是 .NET 的一部分;它位于由Microsoft发布和维护的外部Microsoft.Extensions.Logging NuGet 包中。但是,此包隐式包含在所有 ASP.NET Core应用程序中,包括我们用于创建MyBGList应用程序的 ASP.NET Core Web API项目模板。我们可以通过查看BoardGamesController的源代码来轻松确认这一事实。
打开 Controllers/BoardGamesController.cs 文件,然后搜索对 ILogger 接口的引用。我们应该在控制器类的私有属性中找到它:
private readonly ILogger<BoardGamesController> _logger;
如我们所见,该接口接受泛型类型(TCategoryName)。类型的名称将用于确定日志类别,这是一个字符串值,可帮助我们对来自各个类的日志条目进行分类。
如果我们向下滚动一点,我们会看到 ILogger<BoardGamesController> 对象实例是通过标准依赖注入 (DI) 模式在类构造函数中获取的,我们现在应该习惯了:
public BoardGamesController( ApplicationDbContext context, ILogger<BoardGamesController> logger) { _context = context; _logger = logger;}
因为我们所有的控制器都派生自这个代码库,所以同样的_logger私有属性(和注入模式)也应该存在于我们的 DomainsController 和 MechanicsController 中。我们已经可以使用 ILogger API。为了演示,让我们花几分钟时间执行一个简单的测试。
7.2.1 快速日志记录测试使 BoardGamesController.cs 文件保持打开状态,向下滚动到 Get 操作方法的实现,并添加以下单行代码(粗体):
public async Task<RestDTO<BoardGame[]>> Get( [FromQuery] RequestDTO<BoardGameDTO> input) { _logger.LogInformation("Get method started."); ❶ // ... rest of code omitted
❶ 我们的第一次伐木尝试
如果我们在编写此代码时查看Visual Studio的IntelliSense功能给出的建议,我们会注意到API提供了几种日志记录方法:LogTrace,LogDebug,LogInformation(我们使用的),LogWarning,LogError和LogCritical,所有这些都对应于可用的各种日志级别。这些方法是通用 Log 方法的快捷方式,它允许我们显式指定 LogLevel 值。我们可以使用此值来设置每个日志条目的严重性级别,在 LogLevel 枚举类定义的几个选项中进行选择。
在调试模式下启动项目;通过选择“查看>输出”打开“Visual Studio 输出”窗口;并通过 SwaggerUI 或 https://localhost:40443/BoardGames 执行 BoardGamesController 的 Get 方法。如果一切按计划进行,在收到 HTTP 响应之前,我们应该在“输出”窗口中看到以下日志条目(以及其他几行):
MyBGList.Controllers.BoardGamesController: Information: Get method started.
这是我们第一次日志记录尝试的结果。如我们所见,日志条目包含类别(控制器的完全限定类名)、选择的 Log级别(信息)以及我们在实现它时指定的日志消息。
如果我们查看“输出”窗口中的其他行,我们可以看到与EntityFrameworkCore相关的其他几个日志。我们不应该感到惊讶,因为我们使用的日志记录 API 与所有其他 .NET 和 ASP.NET Core 中间件和服务相同的 API 以及采用它的任何第三方包。很高兴知道这一事实,因为我们应用于应用程序的日志记录行为的每个配置调整不仅会影响我们的自定义日志条目,还会影响我们的组件提供的日志记录功能。现在我们已经体验了 ILogger API 在实践中的工作方式,让我们回顾一下它的核心概念,从 LogLevel 枚举类开始。
7.2.2 日志级别LogLevel 枚举类型是 Microsoft.Extensions.Logging 命名空间的一部分,并按严重性顺序定义以下可用级别:
跟踪 (0) - 与应用程序内部活动相关的信息,仅对低级别调试或系统管理任务有用。从不使用此日志级别,因为它可以轻松包含机密数据(例如配置信息、加密密钥的使用以及任何人都不应查看或记录的其他敏感信息)。调试 (1) - 与开发相关的信息(变量值、堆栈跟踪、执行上下文等),这些信息对于交互式分析和调试非常有用。在生产环境中,应始终禁用此日志级别,因为记录可能包含不应披露的信息。信息 (2) - 描述与系统正常行为相关的事件或活动的信息性消息。此日志级别通常不包含敏感或不可披露的信息,但通常在生产中禁用它,以防止日志记录过于冗长,这可能会导致日志文件(或表)过大。警告 (3) - 有关异常或意外行为的信息,以及不会改变应用程序正常流程的任何其他事件或活动。错误 (4) - 有关可能已停止、中断或以其他方式阻碍特定任务或活动的标准执行流的非关键事件或活动的信息性消息。严重 (5) — 有关阻止应用程序启动或确定不可逆和/或不可恢复的应用程序崩溃的关键事件或活动的信息性消息。无 (6) - 不记录任何信息。通常,此级别用于禁用日志记录。7.2.3 日志记录配置我们在第 2 章中偶然发现了这些 LogLevel 值,当时我们第一次查看我们的 appSettings 文件。现在是时候解释它们的含义了。打开 appsettings.json 文件,并查看根级“日志记录”键,如以下代码片段所示(省略不相关的部分):
"Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }
这些设置为我们的应用程序配置 ILogger API 的日志记录行为。更准确地说,它们根据 LogLevel 枚举类的数值(从跟踪 (0) 到 无 (6) 指定每个类别的最低记录级别。
如我们所见,代表默认回退设置的默认类别设置为“信息”,因此它将记录信息和更高级别(因此包括警告、错误和严重)。但是,另一个设置将覆盖 Microsoft.AspNetCore 类别的这些默认规则,该类别已配置为仅记录“警告”和更高级别。此覆盖的目的是排除 Microsoft.AspNetCore 类别的信息级别,从而减少有关该命名空间的几个不必要的信息的日志记录。
更一般地说,类别设置遵循一组基于级联和特异性概念的简单规则(最具体的规则是将使用的规则):
每个类别设置还应用于其所有嵌套(子)类别。例如,Microsoft.AspNetCore 类别设置的日志记录设置也将应用于以 Microsoft.AspNetCore. 开头的所有类别(如 Microsoft.AspNetCore.Http),除非它们被特定规则覆盖。应用于特定类别的设置始终覆盖为与同一命名空间相关的任何顶级类别以及默认回退类别配置的设置。例如,如果我们为 Microsoft.AspNetCore.Http 类别添加值为“错误”的设置键,则会删除该类别的所有警告日志事件的日志记录,从而覆盖 Microsoft.AspNetCore 父类别的设置。请务必记住,我们可以使用 appsettings.<EnvironmentName>.json 文件设置特定于环境的日志记录配置设置,就像我们在第 2 章中创建 UseDeveloperExceptionPage 设置时所做的那样。此方法允许我们为开发环境提供详细的日志记录行为,同时在生产环境中强制实施更严格(和机密)的方法。
假设我们希望(或被要求)限制生产环境的日志记录详细程度,同时增加开发日志的粒度。以下是我们需要做的:
生产 - 仅记录除 MyBGList 之外的所有类别的警告和更高级别,我们还希望在其中记录信息级别。开发 - 记录所有类别的信息和更高级别,但 Microsoft.AspNetCore 除外,我们只对警告和更高级别感兴趣,MyBGList,我们希望在调试级别及更高级别记录所有内容。首先要做的是打开 appsettings.json 文件,我们将在其中放置生产环境设置,并使用粗体代码更新它(省略不相关的部分):
"Logging": { "LogLevel": { "Default": "Warning", "MyBGList": "Information" } }
然后我们可以继续进行应用程序设置。Development.json 文件,我们需要通过以下方式更新(省略不相关的部分):
"Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", "MyBGList": "Debug" }}
现在我们已经熟悉了日志记录配置设置,我们可能想要了解所有这些日志记录的位置,因为到目前为止,我们只在“输出”窗口中看到它们。若要了解详细信息,我们需要了解日志记录提供程序。
7.2.4 日志记录提供程序日志记录提供程序是存储或显示日志的组件。它根据配置设置接收 ILogger API 发送的日志记录,并将它们保留在某个位置。
以下部分概述了内置日志记录提供程序,即框架提供的提供程序。有多少,它们做什么,我们如何配置它们?然后,我们将查看一些第三方日志记录提供程序,我们可以使用它们来扩展 ILogger API 的功能。
内置日志记录提供程序
下面是 .NET 框架通过 Microsoft.Extensions.Logging 命名空间提供的默认日志记录提供程序的列表:
控制台 - 将日志输出到控制台。此提供程序允许我们在控制台窗口中查看日志消息,当我们执行应用程序时 ASP.NET Core 打开以启动 MyBGList.exe 进程。调试 - 使用 System.Diagnostics.Debug 类提供的 WriteLine 方法输出日志。连接调试器后,此提供程序允许我们在 Visual Studio 输出窗口中查看日志消息,以及将它们存储在日志文件或寄存器中(取决于操作系统和调试器设置)。EventSource - 将日志输出为运行时事件跟踪,以便事件源平台(如 Windows 事件跟踪或下一代 Linux 跟踪工具包)可以获取它们。事件日志 - 将日志写入 Windows 事件日志(仅适用于 Windows 操作系统)。另外三个日志记录提供程序可以将日志发送到各种 Microsoft Azure 数据存储:
AzureAppServicesFile - 将日志写入 Azure 应用服务文件系统中的文本文件(需要事先设置和配置)AzureAppServicesBlob - 将日志写入 Azure 存储帐户中的 Blob 存储(需要事先设置和配置)应用程序见解 - 将日志写入 Azure 应用程序见解服务(需要事先设置和配置)警告Azure 提供程序不是运行时库的一部分,必须作为其他 NuGet 包安装。但是,由于这些包由Microsoft维护和交付,因此它们被视为内置提供程序之一。
多个日志记录提供程序可以同时附加到 ILogger API(已启用),允许我们同时在多个位置存储和/或显示我们的日志。这正是所有 ASP.NET Core Web 应用模板附带的配置发生的情况,包括我们用于创建 MyBGList Web API 项目的模板,该项目默认添加以下日志记录提供程序:控制台、调试、事件源和事件日志(仅限 Windows)。
负责这些默认设置的配置行是在程序.cs文件开头调用的WebApplication.CreateBuilder方法。如果我们想改变这种行为,我们可以使用一个方便的ILoggingBuilder接口实例,该实例由该方法返回的WebApplicationBuilder对象提供。
假设我们要删除 EventSource 和 EventLog 提供程序,同时保留其他提供程序。打开 Program.cs 文件,找到前面的方法,并添加以下调用的行(粗体):
var builder = WebApplication.CreateBuilder(args);builder.Logging .ClearProviders() ❶ .AddSimpleConsole() ❷ .AddDebug(); ❸
❶ 删除所有已注册的日志记录提供程序
❷ 添加控制台日志记录提供程序
❸ 添加调试日志记录提供程序
在摆脱这些日志记录提供程序之前,我们必须删除所有预配置的提供程序(使用 ClearProviders 方法)。然后我们只添加回我们想要保留的那些。
配置提供程序
大多数内置日志记录提供程序可以通过其 add 方法的专用重载以编程方式进行配置,该方法接受选项对象。下面介绍了如何使用 HH:mm:ss 格式和 UTC 时区配置控制台提供程序为其日志条目添加时间戳:
builder.Logging .ClearProviders() .AddSimpleConsole(options => ❶ { options.SingleLine = true; options.TimestampFormat = "HH:mm:ss "; options.UseUtcTimestamp = true; }) .AddDebug();
❶ 添加了基于选项的配置
我们可以立即检查新行为,方法是在调试模式下启动我们的应用程序,执行 BoardGamesController 的 Get 方法,我们在其中放置自定义日志消息,并查看托管 MyBGList.exe 进程的命令提示符窗口。如果我们正确执行了所有操作,我们应该找到具有新外观的日志消息:
15:36:52 info: MyBGList.Controllers.BoardGamesController[0] Get method started.
HH:mm:ss 时间戳现在在行首清晰可见。但是,基于选项的配置方法不是为最新版本的 .NET 配置日志记录提供程序的推荐方法。除非我们有特定需求,否则最好使用更通用的方法,即使用 appsettings.json 文件的“日志记录”部分,即我们用于配置 LogLevel 的相同部分。
让我们从基于选项的配置设置切换到此新方法。首先要做的是回滚我们在 AddSimpleConsole 方法中所做的更改,按以下方式将其恢复为其无参数重载:
builder.Logging .ClearProviders() .AddSimpleConsole() ❶ .AddDebug();
❶ 删除了基于选项的配置
然后我们可以打开 appsettings.json 文件并添加以下控制台部分块(省略不相关的部分):
"Logging": { "LogLevel": { "Default": "Warning", "MyBGList": "Information" }, "Console": { ❶ "FormatterOptions": { "SingleLine": true, "TimestampFormat": "HH:mm:ss ", "UseUtcTimestamp": true } } }
❶ 使用 appsettings.json 文件的控制台日志记录提供程序
我们可以使用相同的技术来覆盖特定日志记录提供程序的 LogLevel。以下是我们如何通过将 MyBGList 类别的 LogLevel 设置为仅针对控制台日志记录提供程序的“警告”来限制其冗长程度:
"Logging": { "LogLevel": { "Default": "Warning", "MyBGList": "Information" }, "Console": { "LogLevel": { ❶ "MyBGList": "Warning" }, "FormatterOptions": { "TimestampFormat": "HH:mm:ss ", "UseUtcTimestamp": true } } }
❶ 控制台提供程序的日志级别设置覆盖
通过查看此代码可以看到,我们可以通过在日志记录提供程序配置中创建新的 LogLevel 子部分来覆盖通用 LogLevel 部分中指定的设置。我们可以通过从头开始执行我们的应用程序(或热重载它)并查看与以前相同的命令提示符窗口来确认覆盖是否有效。我们不应该看到我们的自定义日志消息,因为它属于我们不再为该类别记录的 Log级别。
警告基于appsettings的配置方法可能很方便,因为它允许我们自定义所有提供程序的日志记录行为,和/或设置特定于环境的规则,而无需更新程序.cs文件。但这种方法还需要一些练习和研究,因为每个提供程序都有特定的配置设置。例如,调试日志记录提供程序没有 TimestampFormat 和 UseUctTimestamp 设置,因此如果我们尝试在 LogLevel:Debug 部分中使用这些值,则会忽略这些值。当我们配置第三方日志记录提供程序时,这方面将变得明显。
让我们从 appsettings.json 文件中删除 Logging:Console:LogLevel 部分(以防我们添加了它),这样它就不会妨碍我们即将进行的测试。
7.2.5 事件 ID 和模板如果我们再次查看控制台日志记录提供程序在 MyBGList.exe 控制台窗口中写入的日志消息,我们会注意到类名称后面的方括号内存在一个数值。该数字在我们的自定义日志记录消息中恰好是 [0](零),表示日志的事件 ID。我们可以将其视为上下文信息,可用于对一组具有共同点的事件进行分类,无论其类别如何。假设我们希望(或被要求)对事件 ID 为 50110 的 BoardGamesController 的 Get 方法相关的所有日志进行分类。以下各节展示了我们如何实现该任务。
设置自定义事件 ID
要采用这样的约定,我们需要替换当前的实现,切换到 LogInformation 的方法重载,该方法重载接受给定的事件 ID 作为其第一个参数(以粗体更新代码):
_logger.LogInformation(50110, "Get method started.");
如果我们在调试模式下启动我们的应用程序并检查 MyBGList.exe 控制台窗口,我们会看到事件 ID 代替以前的值 0:
15:43:15 info: MyBGList.Controllers.BoardGamesController[50110] Get method started.
对日志消息进行分类可能很有用,因为它允许我们在需要执行某些检查和/或审核活动时将它们分组在一起(或过滤它们)。但是,每次都必须手动设置这些数字可能会很不方便,因此让我们创建一个 CustomLogEvents 类来集中定义它们。
在 Visual Studio 的“解决方案资源管理器”窗口中,创建一个新的 /Constants/ 根文件夹,并在其中添加新的 CustomLogEvents.cs 类文件。然后用以下清单的内容填充新文件。
清单 7.1 自定义日志事件类
namespace MyBGList.Constants{ public class CustomLogEvents { public const int BoardGamesController_Get = 50110; public const int BoardGamesController_Post = 50120; public const int BoardGamesController_Put = 50130; public const int BoardGamesController_Delete = 50140; public const int DomainsController_Get = 50210; public const int DomainsController_Post = 50220; public const int DomainsController_Put = 50230; public const int DomainsController_Delete = 50240; public const int MechanicsController_Get = 50310; public const int MechanicsController_Post = 50320; public const int MechanicsController_Put = 50330; public const int MechanicsController_Delete = 50340; }}
现在我们可以回到我们的 BoardGamesController 类,并将数字文字值替换为我们创建的用于引用它的常量:
using MyBGList.Constants; ❶ // ... nonrelevant code omitted _logger.LogInformation(CustomLogEvents.BoardGamesController_Get, ❷ "Get method started.");
❶ 新的必需命名空间
❷ 新常量而不是文字值
新的实现更具可读性且不易出错,因为我们不必在代码中为每个事件 ID 键入数字(并冒着键入错误的风险)。
注意并非所有日志记录提供程序都显示事件 ID,并且并非所有日志记录提供程序都将其放在行尾的方括号中。调试提供程序不显示它,其他结构化提供程序(例如 Azure 提供程序)将其保留在我们可以选择显示或不显示的特定列中。
使用消息模板
ILogger API 支持可用于构建日志消息的模板语法,采用类似于字符串提供的字符串格式化技术。设置 C# 方法的格式。但是,我们可以使用名称,而不是使用数字来设置占位符。而不是这样做
string.Format("This is a {0} level log", logLevel);
我们可以这样做:
_logger.LogInformation("This is a {logLevel} level log", logLevel);
这两种方法之间的区别在于后者更易于阅读;我们立即了解占位符的用途。如果我们需要在模板中使用多个占位符,我们将它们对应的变量按正确的顺序放置,如以下示例所示:
_logger.LogInformation( "This is a {logLevel} level log of the {catName} category.", logLevel, categoryName);
我们特意为 categoryName 参数使用 {catName} 占位符,以阐明每个占位符将从与其顺序(而不是其名称)对应的参数接收其值。
7.2.6 异常日志记录ILogger API 还可用于记录异常,这要归功于接受异常作为参数的所有日志记录扩展方法提供的一些专用重载。此功能对于我们的 MyBGList 应用程序来说很方便,因为我们通过集中式端点处理异常。
在第6章中,能够记录我们的异常是导致我们通过ExceptionHandlerMiddleware实现集中式日志记录处理方法的主要原因之一。现在是时候将这种能力变成现实了。
打开 Program.cs 文件并向下滚动到处理 /error 路由的最小 API 的 MapGet 方法。我们需要提供一个可用于记录异常详细信息的 ILogger 实例。为了获取 ILogger 实例,我们可以考虑通过以下方式使用依赖注入来注入 ILogger 接口实例:
app.MapGet("/error", [EnableCors("AnyOrigin")] [ResponseCache(NoStore = true)] (HttpContext context, ILogger logger)
然后,我们可以在 MapGet 方法的实现中使用记录器变量来执行我们的日志记录任务。但是由于我们位于 Program.cs 文件中,因此我们可以改用 WebApplication 对象提供的默认 ILogger 实例。此实例可通过应用局部变量的 Logger 属性访问,该变量与我们用于将中间件添加到请求管道的变量相同。下面介绍了如何利用此属性来实现异常日志记录更改请求:
// Minimal APIapp.MapGet("/error", [EnableCors("AnyOrigin")] [ResponseCache(NoStore = true)] (HttpContext context) => { var exceptionHandler = context.Features.Get<IExceptionHandlerPathFeature>(); var details = new ProblemDetails(); details.Detail = exceptionHandler?.Error.Message; details.Extensions["traceId"] = System.Diagnostics.Activity.Current?.Id ?? context.TraceIdentifier; details.Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1"; details.Status = StatusCodes.Status500InternalServerError; app.Logger.LogError( ❶ exceptionHandler?.Error, "An unhandled exception occurred."); return Results.Problem(details); });
❶ 异常记录
当我们在这里时,我们可以为此特定任务创建新的事件 ID,以便我们能够筛选与异常相关的所有错误日志条目。切换到 /Constants/CustomLogEvents.cs 文件,并在类的开头添加以下常量,就在现有常量上方:
public const int Error_Get = 50001;
现在我们可以切换到 Program.cs 文件,并使用接受 eventId 参数的覆盖来更改 LogError 方法实现:
using MyBGList.Constants; // ... nonrelevant code omitted app.Logger.LogError( CustomLogEvents.Error_Get, ❶ exceptionHandler?.Error, "An unhandled exception occurred.");
❶ 自定义事件 ID
现在我们已经对 ILogger API 有了信心,我们准备讨论第三方日志记录提供程序,它允许我们将这些日志保存在结构化数据存储中,例如我们的数据库管理系统 (DBMS)。但是,首先,我们将花费一些宝贵的时间来检查结构化和非结构化日志记录之间的区别。
7.3 非结构化日志记录与结构化日志记录我们简要回顾过的所有内置日志记录提供程序(Azure 应用程序见解提供程序除外,我们将在后面讨论)都有一个共同的特征:它们使用原始字符串存储(或显示)日志信息。换句话说,日志记录具有以非结构化方式存储(或显示)的文本数据的外观。以下列表总结了非结构化、半结构化和结构化数据之间的差异:
非结构化数据 - 未使用预定义数据模型组织的原始数据,无论是数据库架构、带列的电子表格文件、结构化 XML/JSON 文件,还是允许在字段之间拆分记录数据相关部分的任何其他内容,换句话说,纯文本记录。半结构化数据 - 不驻留在 DBMS 中但带有一些组织属性的数据,这些属性可以更轻松地分析、解析和/或处理内容(例如为实际 DBMS 设定种子)。半结构化数据的一个很好的例子是我们在第5章中使用的棋盘游戏CSV文件,用于为MyBGList数据库播种。结构化数据 - 已组织到可寻址存储库(如 DBMS)中的数据,无需我们对其进行解析即可进行有效分析。结构化数据的一个典型示例是一组记录,例如自第 5 章以来填充我们的 MyBGList 表的记录。毫无疑问,我们可以毫不怀疑地说,到目前为止,由于控制台的内置提供程序,我们一直在使用的日志记录属于非结构化家族。这是坏事吗?不一定,前提是我们完全了解此日志记录策略的优点和缺点。
7.3.1 非结构化日志记录的优缺点非结构化日志记录方法无疑具有相关优势,包括可访问性。我们需要一个控制台或文本编辑器来查看这些记录并充分理解它们的内容。此外,由于记录通常一个接一个地存储,因此日志审查阶段通常相当快速和方便,尤其是当我们需要访问最新条目时,可能是因为我们知道触发我们正在寻找的日志的事件发生在不久前。
不幸的是,当事情变得更加复杂时,这些好处往往会消失。如果我们需要检索特定的日志条目,而不知道相关事件发生的时间,甚至不知道它是否发生,我们可能很难找到它。我们唯一的工具是基于文本的搜索工具,例如记事本查找功能和Linux grep命令。当这些日志文件达到临界质量时,此任务可能会变得更加麻烦,如果我们激活特别详细的 LogLevel ,这可能会很快发生。我们都知道访问和浏览这些巨大的文件是多么困难,更不用说对它们执行搜索操作了。从数千个非结构化日志条目中检索特定信息很容易成为一项令人沮丧的任务;这绝对不是完成工作的快速便捷方法。
注意此问题在 IT 生态系统中广为人知,以至于它导致了几个日志管理工具的诞生,这些工具引入非结构化日志记录,然后使用标准或用户定义的模式、规则或架构聚合、解析和/或规范化它们。这些工具包括Graylog,LogDNA,Splunk和NewRelic。
简而言之,大多数内置日志记录提供程序生成的非结构化日志记录真正缺乏的是一项功能,它允许我们通过一个或多个参数(例如日志级别、事件 ID、事务编号以及开始/结束日期和时间)以实用的方式过滤这些记录 - 换句话说,查询它们。我们可以通过添加结构化日志记录提供程序来克服此限制,该提供程序允许我们将这些记录写入允许结构化数据存储的服务(如 DBMS)的日志记录提供程序。
7.3.2 结构化日志记录的优势以下是结构化日志记录策略的主要优点:
我们不需要依靠文本编辑器工具来手动模式读取日志。我们不需要编写代码来解析或处理自动模式下的日志。我们可以使用相关字段查询日志记录,也可以将它们与外部数据聚合。例如,我们可以发出 JOIN 查询,仅提取由用户操作触发的日志以及一些相关的用户数据(例如用户 ID、名称和电子邮件地址)。我们可以提取和/或转换其他格式的日志,可能仅包含相关信息和/或省略不应披露的信息。由于 DBMS 的索引功能,性能优势使检索过程更加高效,即使我们正在处理大量记录也是如此。这些好处是使用预定义的数据结构存储日志记录的直接结果。一些间接的好处也值得考虑,这取决于任何结构化日志记录提供商都依赖于最终将存储数据的数据存储服务。这些好处因我们选择的数据存储类型而异。我们可以通过多种方式存储日志:
在我们现有的 MyBGList 数据库中,以便我们可以使用 EF Core 访问它,并将所有与应用程序相关的数据保存在一个集中的位置在同一 DBMS 实例 (SQL Server) 上的单独数据库中,以便我们可以将日志记录与棋盘游戏数据在逻辑上分开,同时仍然能够通过 EF Core 在一定程度上访问它们在位于其他位置(如第三方服务或 DBMS)的外部存储库中,以便日志记录完全独立于应用程序的基础结构,因此对故障、泄漏、篡改等更具弹性正如我们所看到的,所有这些选项都很重要。我们甚至可以考虑混合这些日志记录策略,因为 ILogger 接口支持多个日志记录提供程序,并且大多数第三方提供程序支持多个输出目标。
提示我们还可以保留之前配置的内置日志记录提供程序(控制台和调试),除非我们要删除或替换它们。这种方法将允许我们同时设置结构化和非结构化日志记录。
这是足够的理论。让我们看看如何将这些有价值的工具添加到我们的工具箱中。
7.3.3 应用程序见解日志记录提供程序如前所述,我们可用于以结构化格式存储事件日志的唯一内置替代方法是应用程序见解日志记录提供程序。在本节中,我们将简要了解如何为 MyBGList Web API 设置此日志记录提供程序。此任务要求我们在 Microsoft Azure 中创建一个帐户,Azure 是托管应用程序见解服务并使其可用的云提供商。但是,首先,我将简要介绍 Azure 和应用程序见解,并讨论基于云的服务(如应用程序见解)可以为现代 Web 应用程序(如我们的 MyBGList Web API)带来的好处。
Azure Microsoft简介
Microsoft Azure(通常称为MS Azure或简称Azure)是由Microsoft拥有和维护的公共云计算平台。它于2008年宣布,2010年正式发布为Windows Azure,并于2014年更名。该平台依赖于全球Microsoft数据中心的庞大网络。它使用大规模虚拟化技术,通过软件即服务 (SaaS)、平台即服务 (PaaS) 和基础架构即服务 (IaaS) 交付模型提供 600 多种服务。它遵循其最著名的竞争对手使用的基于订阅的方法:亚马逊网络服务(AWS)和谷歌云平台(GCP)。
Azure 服务可以通过一组 API 进行管理,这些 API 可通过可用于各种编程语言的 Web 和/或托管类库直接访问。2015年底,Microsoft还发布了Azure门户,这是一个基于Web的管理界面,允许用户在可视化图形用户界面(GUI)中管理大多数服务。Azure 门户的发布极大地帮助 Azure 增加了其市场份额,根据 Canalys 的数据,21 年第一季度的市场份额达到 2022%,AWS 为 33%,GCP 为 8%。
提示Canalys报告可在 http://mng.bz/pdqK 获得。
应用程序洞察简介
Azure 提供的服务包括 Azure Monitor,这是一个全面的解决方案,可用于从任何受支持的基于云的本地环境或服务(包括 Web 应用程序)收集和分析日志、审核跟踪和其他与性能相关的输出数据。专用于引入、监视和分析实时 Web 应用程序生成的日志和信息性状态消息的 Azure Monitor 功能称为应用程序见解。
注意我们可以将Application Insights视为某种Google Analytics。但是,它不是监视 Web 应用程序的页面视图和会话,而是监视和分析其日志和状态消息。
内置日志记录提供程序中存在应用程序见解提供程序也是我们选择处理Azure而不是AWS和GCP的主要原因。
创建 Azure 实例
现在,我们了解了 Azure 和应用程序见解的基础知识,可以继续服务设置。若要使用应用程序见解服务,我们需要有一个有效的 Azure 帐户。幸运的是,该服务提供了一个基本的定价计划,在我们的日志数据变得大量之前,该计划几乎是免费的。
提示要创建免费帐户,请转到 https://azure.microsoft.com/en-us/free。
首先要做的是登录到 Azure 门户。然后,使用主仪表板顶部的搜索文本框查找和访问应用程序见解服务。单击屏幕左上角的“创建”按钮以创建新的应用程序见解实例,如图 7.1 所示。
图7.1 创建新实例
创建过程很简单,只需要我们设置几个参数:
订阅 - 要在其中创建此实例的 Azure 订阅。(如果您没有订阅,系统会要求您创建一个免费试用版 30 天。资源组 - 要在其中创建此实例的 Azure 资源组。名称 - 实例名称。区域 - 要在其中创建实例的地理区域。资源模式 - 如果希望此应用程序见解实例具有自己的环境,请选择“经典”;如果要将其与现有 Log Analytics 工作区集成,请选择“基于工作区”。为简单起见,请选择“经典”(图 7.2)。图7.2 配置设置
单击“查看 + 创建”以查看这些设置,然后单击“创建”以完成实例部署过程。部署完成后,单击“转到资源”,这会转到新的 Application Insights 实例的管理仪表板,我们可以在其中检索连接字符串(图 7.3)。
图 7.3 检索应用程序见解连接字符串
在配置应用程序见解日志记录提供程序时,我们将需要该值,以便它能够将日志事件发送到该实例。让我们将其存储在适当的位置。
存储连接字符串
出于开发目的存储应用程序见解连接字符串的好地方是 secrets.json 文件,我们从第 4 章开始就一直在使用它。在“Visual Studio 解决方案资源管理器”窗口中,右键单击项目的根节点,然后从上下文菜单中选择“管理用户机密”。以下是我们可以添加到现有内容中的部分块,位于 ConnectionString 键下方(删除了不相关的部分):
"Azure": { "ApplicationInsights": { "ConnectionString": "<INSERT_CONNECTION_STRING_HERE>" ❶ } }
❶ 将连接字符串放在此处
现在,我们知道在配置日志记录提供程序时将检索连接字符串的位置。但是,首先,我们必须安装它。
安装 NuGet 包
现在,我们在 Azure 中有了 MyBGList 应用程序见解实例和可用的连接字符串,我们可以安装应用程序见解日志记录提供程序 NuGet 包。与往常一样,我们可以使用Visual Studio的NuGet GUI,Package Manager Console窗口或.NET命令行界面(CLI)。下面是使用 .NET CLI 安装它们的命令:
> dotnet add package Microsoft.Extensions.Logging.ApplicationInsights --version 2.21.0 > dotnet add package Microsoft.ApplicationInsights.AspNetCore ➥ --version 2.21.0
注意示例中指定的版本是本文中可用的最新稳定版本。我强烈建议也使用该版本,以避免处理重大更改、不兼容问题等。
第一个包包含提供程序本身,第二个包需要使用 appsettings.json 文件对其进行配置,就像我们之前对控制台日志记录提供程序所做的那样。安装软件包后,我们可以配置提供程序。
配置日志记录提供程序
配置部分发生在我们应用程序的程序.cs文件中。打开该文件,找到我们定义日志记录提供程序的部分,然后按以下方式将新提供程序添加到循环中:
builder.Logging .ClearProviders() .AddSimpleConsole() .AddDebug() .AddApplicationInsights( ❶ telemetry => telemetry.ConnectionString = builder .Configuration["Azure:ApplicationInsights:ConnectionString"], loggerOptions => { });
❶ 添加应用程序见解日志记录提供程序
新的配置行将激活我们的 MyBGList Web API 的应用程序见解日志记录提供程序。我们现在需要做的就是看看它是否有效。
测试应用程序见解事件日志
在调试模式下启动项目,并导航到 /BoardGames 终结点以触发一些事件日志。然后返回到 Azure 中应用程序见解服务的主仪表板,并从右侧菜单中选择“调查>事务”。如果我们正确执行了所有操作,则应将应用程序的事件日志视为 TRACE 事件类型,如图 7.4 所示。
图7.4 显示为TRACE事件类型的应用程序日志
如我们所见,事件日志可以以结构化格式访问。我们甚至可以使用 GUI 创建一些查询,例如过滤严重性级别等于或大于给定值的条目。这是对非结构化日志记录的极大改进!
但是,此技术有一个主要限制:我们添加的日志记录提供程序(顾名思义)仅限于 Azure 的应用程序见解服务。我们将无法将这些结构化日志存储在其他任何位置,例如在我们现有的 SQL Server 数据库或任何其他 DBMS 中。若要实现此功能,我们必须使用为这些类型的输出目标提供支持的第三方日志记录提供程序。
7.4 第三方日志记录提供程序本节介绍Serilog,这是一个用于NuGet上可用的.NET应用程序的开源日志记录库,它允许我们将应用程序日志存储在几个流行的DBMS(包括SQL Server和MariaDB)以及第三方服务中。
注意我们选择 Serilog 而不是其他替代方案,因为它是最受欢迎的第三方日志记录提供商之一,在撰写本文时,GitHub 上的下载量超过 480.1 亿次,拥有 000,2 颗星,并且是开源的(Apache 0.<> 许可证)。
在了解了Serilog的工作原理之后,我们将了解如何将其安装在MyBGList Web API项目中,并使其与我们已经拥有的内置日志记录提供程序一起工作。
7.4.1 蚤码概述我们必须了解的第一件事是,Serilog 不仅仅是一个实现 Microsoft.Extensions.Logging.ILogger 接口以及其他日志记录提供程序的日志记录提供程序。这是一个功能齐全的日志记录系统,可以设置为以两种不同的方式工作:
作为日志记录 API - 将 .NET 日志记录实现(包括 ILogger 接口)替换为其自己的本机接口作为日志记录提供程序 - 实现Microsoft扩展日志记录 API(并使用几个附加功能对其进行扩展),而不是替换它这两种架构方法之间的主要区别在于,第一种需要在我们所有的代码库中设置对Serilog接口的依赖,从而取代我们迄今为止使用的ILogger接口。虽然这个要求不一定是坏的,但我认为保留Microsoft日志记录 API 通常是更好的选择,因为它更适合典型 ASP.NET Core Web 应用程序的模块化结构,从而确保系统更灵活。出于这个原因,我们将遵循第二种方法。无论我们选择如何设置它,Serilog 都比大多数其他日志记录提供程序提供了两个主要优势:
扩充器 - 一组包,可用于自动向日志事件(ProcessId、ThreadId、MachineName、EnvironmentName 等)添加其他信息。接收器 - 一系列输出目标,例如 DBMS、基于云的存储库和第三方服务由于 Serilog 是使用模块化体系结构构建的,因此所有扩充器和接收器都可以通过专用的 NuGet 包获得,我们可以在需要时与库核心包一起安装。
7.4.2 安装服务器日志为简单起见,我们理所当然地认为,在我们的方案中,我们希望使用 Serilog 将日志事件存储在 SQL Server 数据库中。我们需要安装以下软件包:
Serilog.AspNetCore - 包括核心 Serilog 包、集成到 ASP.NET Core 配置和托管基础结构、一些基本的扩充器和接收器,以及记录 HTTP 请求所需的中间件Serilog.Sinks.MSSqlServer - 用于在 SQL Server 中存储事件日志的接收器为此,我们可以在包管理器控制台中使用 NuGet GUI 或以下命令:
> dotnet add package Serilog.AspNetCore --version 6.0.1> dotnet add package Serilog.Sinks.MSSqlServer --version 6.0.0
提示这些版本是撰写本文时可用的最新稳定版本。我强烈建议也使用它们,以避免处理重大更改、不兼容问题等。
7.4.3 配置服务器日志配置发生在我们应用程序的程序.cs文件中。我们将使用由Serilog.AspNetCore包提供的UseSerilog扩展方法,该方法将Serilog设置为主日志记录提供程序,并使用一些基本设置设置SQL Server接收器。打开 Program.cs 文件,并在构建器下方添加以下代码行。我们之前添加的日志记录配置设置:
builder.Logging ❶ .ClearProviders() .AddSimpleConsole() .AddDebug() .AddApplicationInsights( telemetry => telemetry.ConnectionString = builder .Configuration["Azure:ApplicationInsights:ConnectionString"], loggerOptions => { }); builder.Host.UseSerilog((ctx, lc) => { ❷ lc.ReadFrom.Configuration(ctx.Configuration); lc.WriteTo.MSSqlServer( connectionString: ctx.Configuration.GetConnectionString("DefaultConnection"), sinkOptions: new MSSqlServerSinkOptions { TableName = "LogEvents", AutoCreateSqlTable = true }); }, writeToProviders: true);
❶ 内置日志记录提供程序配置
❷ 蝷 蝴蝶配置
这段代码要求我们在文件顶部添加以下命名空间引用:
using Serilog;using Serilog.Sinks.MSSqlServer;
请注意,我们指定了与 EF Core 一起使用的相同 SQL Server 连接字符串。我们这样做是因为在这种情况下,我们希望使用我们已经使用的相同MyBGList数据库来存储与棋盘游戏相关的数据。
注意从理论上讲,我们可以强制执行关注点分离方法并创建一个专用的日志记录数据库。这两种方法都是完全可行的,尽管它们各有利弊,可能使它们在各种情况下或多或少可行。为简单起见,我们假设将所有内容保存在单个数据库中是我们方案的有效选择。
如果我们查看代码,我们会看到我们命名了将托管日志记录的表(“LogEvents”)。此外,我们将 SQL Server 接收器配置为在它不存在的情况下自动创建它。此功能是该接收器的一个很好的内置功能,因为它允许我们委派任务而无需手动创建表,这将涉及选择错误数据类型的风险。自动生成的表将具有以下结构:
[Id] [int] IDENTITY(1,1) NOT NULL[Message] [nvarchar](max) NULL[MessageTemplate] [nvarchar](max) NULL[Level] [nvarchar](max) NULL[TimeStamp] [datetime] NULL[Exception] [nvarchar](max) NULL[Properties] [nvarchar](max) NULL
最后但并非最不重要的一点是,我们将 writeToProviders 参数设置为 true。此选项可确保 Serilog 不仅将日志事件传递到其接收器,还将日志事件传递到通过 Microsoft.Extensions.Logging API 注册的日志记录提供程序,例如我们之前配置的内置提供程序。此设置默认为 false,因为 Serilog 默认行为是关闭这些提供程序并将其替换为等效的 Serilog 接收器。但是,我们不想强制实施此行为,因为我们希望了解如何将内置日志记录提供程序和第三方日志记录提供程序配置为并行工作。配置部分已经结束,大部分;现在是时候测试我们到目前为止所做的工作了。
7.4.4 测试服务器日志同样,在调试模式下启动 MyBGList 项目,并执行 /BoardGame 端点以触发一些日志事件。这一次,我们需要启动SQL Server Management Studio(SSMS)并连接到MyBGList数据库,而不是查看命令提示符输出。如果一切正常,我们应该看到一个全新的 LogEvents 表,其中包含结构化的事件日志,如图 7.5 所示。
图 7.5 包含事件日志的日志事件选项卡
如果我们仔细查看表中记录的日志事件(在图 7.5 的右下角),我们甚至会看到与我们添加到 BoardGameController 的“自定义”日志事件条目相关的日志记录。我们的测试是成功的。我们有一个结构化的日志记录引擎,将我们的日志条目记录在我们的数据库中。
7.4.5 改进日志记录行为在本节中,我们将了解如何利用库提供的许多功能进一步改进基于 Serilog 的结构化日志记录行为:添加列、配置最低日志记录级别、使用 Serilog 模板语法自定义日志消息、丰富日志和添加接收器。
添加列
如果我们使用 SSMS(图 7.5)仔细查看填充 LogEvents 表的日志条目,我们会注意到缺少一些内容:用于存储日志记录的源上下文(发起日志记录的类的命名空间)或 EventId 的列。原因很简单:Serilog 不是将这些值记录在专用列中,而是将它们存储在“属性”列中。如果我们查看自定义日志条目的该列的值,我们会看到这两个值都在那里,与其他属性一起包装在 XML 结构中(省略不相关的部分):
<propertykey='EventId'> ❶ <structure type=''> <property key='Id'> 50110 ❷ </property> </structure> </property> <property key='SourceContext'> ❶ MyBGList.Controllers.BoardGamesController ❷ </property>
❶ 物业名称
❷ 房产价值
请注意,EventId 是一个复杂属性,因为它可以包含不同类型的值(int、string 等),而 SourceContext 属性承载一个简单的字符串(发起日志记录的类的命名空间)。将这些值存储在此 XML 半结构化格式中对于大多数方案来说可能已经足够了。但是 Serilog 还允许我们将这些属性存储在它们自己的单独列中。这些功能是通过可在配置设置中指定的可选 columnOptions 参数启用的,因此我们可以添加 AdditionalColumns 的集合并将它们映射到这些属性。
假设我们要将 SourceContext 属性存储在专用列中。以下是我们如何使用 columnOptions 参数来执行此操作:
builder.Host.UseSerilog((ctx, lc) => { lc.ReadFrom.Configuration(ctx.Configuration); lc.WriteTo.MSSqlServer( connectionString: ctx.Configuration.GetConnectionString("DefaultConnection"), sinkOptions: new MSSqlServerSinkOptions { TableName = "LogEvents", AutoCreateSqlTable = true }, columnOptions: new ColumnOptions() ❶ { AdditionalColumns = new SqlColumn[] { new SqlColumn() ❷ { ColumnName = "SourceContext", PropertyName = "SourceContext", DataType = System.Data.SqlDbType.NVarChar } } } ); }, writeToProviders: true);
❶ 配置列选项可选参数
❷ 为源上下文属性添加列
现在我们需要从 MyBGList 数据库中删除 LogEvents 表,以便 Serilog 能够使用新的 SourceContext 列重新生成它。然后,我们需要在调试模式下启动 MyBGList 项目,访问 /BoardGame 终结点以触发日志记录,并在 SSMS 中查看结果。
如果一切按预期工作,我们应该看到新的 SourceContext 列包含各种命名空间,如图 7.6 所示。(如果您无法阅读文本,请不要担心;该图仅显示在哪里可以找到新列。
图 7.6 包含新“源上下文”列的日志事件表
现在我们可以查看这些命名空间,我们注意到另一个问题。为什么我们使用来自 Microsoft.AspNetCore 命名空间的信息级别记录这些日志记录?如果我们看一下应用程序设置。Development.json 文件,我们看到我们使用 Logging:LogLevel 配置键选择了它们:
"Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning "MyBGList": "Debug" } },
粗体行清楚地表明我们排除了日志级别低于警告的所有事件日志,其中包括信息日志。如果是这样的话,为什么我们仍然记录它们?
答案很简单。尽管 Serilog 可以通过 appsettings .json 文件进行配置,就像内置日志记录提供程序一样,但它使用自己的配置部分,这也取代了默认记录器实现使用的日志记录部分。
注意自第 2 章以来,我们在 appsettings.json 文件中拥有的 Logging:LogLevel 部分现在毫无用处,以至于我们可以将其删除(除非我们想禁用 Serilog 并回滚到默认记录器)。也就是说,我们将保留该部分以供参考。
新的配置部分称为“Serilog”,语法略有不同。在下一节中,我们将了解如何配置它。
配置最低级别
为了模仿我们在日志记录部分中配置的相同 LogLevel 行为,我们需要在现有“日志记录”部分下方的 appsettings.json 文件中添加新的顶级“Serilog”部分。方法如下:
"Logging": { // omissis... }, "Serilog": { "MinimumLevel": { "Default": "Warning", "Override": { "MyBGList": "Information" } } }
这是应用程序设置的相应部分。开发.json 文件:
"Logging": { // omissis... }, "Serilog": { "MinimumLevel": { "Default": "Information", "Override": { "Microsoft.AspNetCore": "Warning", "MyBGList": "Debug" } } }
MinimumLevel 也可以通过以下方式在程序.cs文件中以编程方式配置(基于选项的方法):
builder.Host.UseSerilog((ctx, lc) => { lc.MinimumLevel.Is(Serilog.Events.LogEventLevel.Warning); ❶ lc.MinimumLevel.Override( ❷ "MyBGList", Serilog.Events.LogEventLevel.Information); // ... omissis ...
❶ 默认最低级别值
❷ 覆盖特定源的值
如我们所见,我们可以为所有日志事件源配置默认行为,然后配置一些基于命名空间的覆盖,例如默认记录器设置。
警告为了确定日志级别,Serilog 不使用 Microsoft.Extensions.Logging.LogLevel 枚举。它使用专有的Serilog.Events.LogEventLevel enum,其名称略有不同:详细,调试,信息,警告,错误和致命。两个枚举之间最相关的区别是Serilog对应项中没有跟踪和无,以及已重命名为致命的临界级别。
“最低级别”部分中指定的设置将应用于所有接收器。如果我们需要为特定接收器重写它们,我们可以通过以下方式使用 appsettings.json 文件中的 restrictedToMinimumLevel 设置(基于配置的方法):
"Serilog": { "WriteTo": [{ "Name": "MSSqlServer", "Args": { "restrictedToMinimumLevel": "Warning", ❶ } }] }
❶ 为此接收器设置最低日志事件级别
或者我们可以通过以下方式使用程序.cs文件(基于选项的方法):
lc.WriteTo.MSSqlServer( restrictedToMinimumLevel: ❶ Serilog.Events.LogEventLevel.Information // ... omissis ...
❶ 为此接收器设置最低日志事件级别
对于示例项目,我们将保留所有接收器的默认行为。因此,我们不会在代码中使用 restrictedToMinimumLevel 设置。
消息模板语法
现在我们已经恢复了日志记录级别配置,我们可以探索另一个很棒的 Serilog 功能:扩展消息模板语法。默认记录器语法提供了一个重载,允许我们使用标准的 .NET 复合格式设置功能,这意味着而不是编写这个
_logger.LogInformation(CustomLogEvents.BoardGamesController_Get, "Get method started at " + DateTime.Now.ToString("HH:mm"));
我们可以写这样的东西
_logger.LogInformation(CustomLogEvents.BoardGamesController_Get, "Get method started at {0}", DateTime.Now.ToString("HH:mm"));
或者这个:
_logger.LogInformation(CustomLogEvents.BoardGamesController_Get, "Get method started at {0:HH:mm}", DateTime.Now);
.NET 撰写格式设置功能功能强大,但它在可读性方面存在重大缺点,尤其是当我们有很多占位符时。因此,此功能经常被忽略,而倾向于字符串内插(在 C# 版本 6 中引入),它为格式化字符串提供了更易读、更方便的语法。以下是我们如何通过使用字符串插值实现与以前相同的日志记录条目:
_logger.LogInformation(CustomLogEvents.BoardGamesController_Get, $"Get method started at {DateTime.Now:HH:mm}");
提示有关 .NET 撰写格式设置功能的其他信息,请查看 http://mng.bz/eJBq。有关 C# 字符串内插功能的其他信息,请参阅 http://mng.bz/pdZw。
Serilog 使用消息模板语法扩展了 .NET 撰写格式设置功能,该语法不仅修复了可读性问题,而且还提供了其他优势。了解改进的语法如何工作的最快方法是查看我们如何使用它来编写以前的日志条目:
_logger.LogInformation(CustomLogEvents.BoardGamesController_Get, "Get method started at {StartTime:HH:mm}.", DateTime.Now);
如我们所见,消息模板语法允许我们使用基于字符串的占位符而不是数字占位符,从而提高了可读性。但这还不是全部:所有占位符也将自动被视为(和存储)为属性,因此我们将在属性列(在 XML 结构内)中找到它们。此方便的功能对于在日志表中执行基于查询的查找非常有用,因为所有这些值都将以半结构化 (XML) 方式记录。
提示我们甚至可以通过使用columnOptions配置参数将值存储在专用列中,就像我们之前对SourceContext所做的那样,从而以结构化的方式记录它们。
由于这种强大的占位符到属性功能,使用 Serilog 的消息模板语法编写日志消息通常比使用 C# 字符串内插功能编写日志消息更可取。
添加扩充器
现在我们已经了解了 Serilog 模板功能的基础知识,我们可以通过使用 Serilog 提供的一些扩充器为日志记录提供有关应用程序上下文的其他信息。假设我们要将以下信息添加到日志中:
执行计算机的名称(对于 Windows 系统,相当于 %COMPUTERNAME%,对于 macOS 和 Linux 系统,相当于 $HOSTNAME)执行线程的唯一 ID为了满足此请求,我们可以通过以下方式编写日志消息:
_logger.LogInformation(CustomLogEvents.BoardGamesController_Get, "Get method started [{MachineName}] [{ThreadId}].", ❶ Environment.MachineName, ❷ Environment.CurrentManagedThreadId); ❸
❶ 添加占位符
❷ 检索计算机名称
❸ 检索线程 ID
此方法检索这些值,并且由于消息模板语法提供的占位符到属性功能,将它们记录在日志记录的“属性”列中。但是我们将被迫为每个日志条目重复此代码。此外,无论我们是否希望它们在那里,这些值也将出现在“消息”列中。
Serilog的丰富者的目的是以透明的方式实现相同的结果。若要实现它们,我们需要安装以下 NuGet 包:
> dotnet add package Serilog.Enrichers.Environment --version 2.2.0> dotnet add package Serilog.Enrichers.Thread --version 3.1.0
然后我们可以激活它们,通过以下方式修改程序.cs文件中的Serilog配置(省略不相关的部分):
builder.Host.UseSerilog((ctx, lc) => { lc.ReadFrom.Configuration(ctx.Configuration); lc.Enrich.WithMachineName(); ❶ lc.Enrich.WithThreadId(); ❷
❶ 添加更丰富的环境
❷ 添加线程丰富器
现在,将在我们所有的日志记录中自动创建 MachineName 和 ThreadId 属性。同样,我们可以选择将它们保留在属性列(半结构化)中,或者通过添加几列以结构化格式存储它们,就像我们对 SourceContext 所做的那样。
添加其他接收器
在完成我们的Serilog旅程之前,让我们添加另一个水槽。假设我们要将日志事件写入自定义文本文件。我们可以通过使用Serilog.Sinks.File轻松实现此要求,Serilog.Sinks.File是一个将日志事件写入一个或多个可自定义文本文件的接收器。与往常一样,首先要做的是安装相关的 NuGet 包:
> dotnet add package Serilog.Sinks.File --version 5.0.0
接下来,打开 Program.cs 文件,并按以下方式将接收器添加到 Serilog 配置:
builder.Host.UseSerilog((ctx, lc) => { lc.ReadFrom.Configuration(ctx.Configuration); lc.Enrich.WithMachineName(); lc.Enrich.WithThreadId(); lc.WriteTo.File("Logs/log.txt", ❶ rollingInterval: RollingInterval.Day); ❷ // ... non-relevant parts omitted ...
❶ 添加接收器,指定文件路径和名称
❷ 配置接收器
这些设置将指示接收器在 /Logs/ 文件夹中创建 log.txt 文件(如果不存在,则创建该文件),滚动间隔为一天。滚动间隔是接收器创建新文件来存储日志的间隔。一天的间隔意味着我们每天将有一个文件。
注意滚动间隔也会影响文件名,因为接收器(根据其默认行为)将相应地为它们添加时间戳。我们的日志文件名为 log<yyyyMMdd>.txt,例如 log20220518.txt、log20220519.txt 等。
测试接收器所需要做的就是在调试模式下启动项目,等待它加载,然后签出项目的根文件夹。如果一切按预期进行,我们应该找到一个新的 /Logs/ 文件夹,其中包含一个 log<yyyyMMdd>.txt 文件,其中日志的格式如下:
2022-05-18 04:07:57.736 +02:00 [INF] Now listening on: https://localhost:404432022-05-18 04:07:57.922 +02:00 [INF] Now listening on: http://localhost:400802022-05-18 04:07:57.934 +02:00 [INF] Application started. Press Ctrl+C to shut down.2022-05-18 04:07:57.939 +02:00 [INF] Hosting environment: Development
日志条目通过接收器提供的默认输出模板写入。如果我们想自定义默认模板,我们可以使用 outputTemplate 配置属性。假设我们要包含不久前用于丰富日志的 MachineName 和 ThreadId 属性。以下是我们如何实现这一目标:
lc.WriteTo.File("Logs/log.txt", outputTemplate: ❶ "{Timestamp:HH:mm:ss} [{Level:u3}] " + "[{MachineName} #{ThreadId}] " + "{Message:lj}{NewLine}{Exception}", rollingInterval: RollingInterval.Day);
❶ 定义自定义输出模板
自定义模板会产生以下结果:
04:35:11 [INF] [PYROS #1] Now listening on: https://localhost:4044304:35:11 [INF] [PYROS #1] Now listening on: http://localhost:4008004:35:11 [INF] [PYROS #1] Application started. Press Ctrl+C to shut down.04:35:11 [INF] [PYROS #1] Hosting environment: Development
如我们所见,现在每个日志条目中都存在 MachineName 和 ThreadId 属性值。
提示由于篇幅原因,我不会进一步深入研究Serilog。要查找有关它的其他信息,以及它的丰富器、接收器和消息模板语法,请查看库官方 wiki https://github.com/serilog/serilog/wiki。
7.5 练习是时候用我们的产品所有者给出的通常的假设任务分配列表来挑战自己了。
注意练习的解决方案可在 GitHub 的 /Chapter_07/Exercises/ 文件夹中找到。若要测试它们,请将 MyBGList 项目中的相关文件替换为该文件夹中的文件,然后运行应用。
7.5.1 JSON 控制台日志记录将内置的简单控制台日志记录提供程序替换为内置的 JSON 控制台日志记录提供程序。
7.5.2 日志记录提供程序配置将 JSON 控制台日志记录提供程序的时间戳格式设置为仅记录小时和分钟(无秒),使用 UTC 时区和基于选项的方法。
7.5.3 异常日志记录的新属性在异常处理程序最小 API 方法中,使用 Serilog 的消息模板语法将新的错误消息定制属性添加到错误日志消息中。新属性必须包含异常的消息值。
7.5.4 新的Serilog丰富器通过添加 ThreadName 属性来丰富当前日志配置。然后修改 Serilog 的文件接收器,以便将 ThreadName 值写入紧跟在 ThreadId 值之后,用空格分隔。
7.5.5 新的Serilog接收器在现有文件接收器下添加另一个文件接收器,并满足以下要求:
文件名 - /日志/错误.txt输出模板 - 与现有文件接收器相同要记录的日志级别 - 仅错误和致命滚动间隔 - 每天总结日志记录是以结构化、半结构化和/或非结构化格式跟踪应用程序内发生的所有事件并将它们发送到一个或多个显示和/或存储通道的过程。该术语起源于航海领域,自 1960 年代以来一直在 IT 中使用。应用程序日志记录使我们能够检测和修复错误、未经处理的异常、错误和漏洞,从而提高应用程序的稳定性和安全性。此外,它极大地帮助我们在异常行为变得严重之前识别它们,这对于业务连续性至关重要。此外,这是大多数国际 IT 安全法规、标准和准则的要求。.NET Framework 通过 Microsoft.Extensions.Logging 包和 ILogger 接口提供标准化的通用日志记录 API,这允许我们通过一系列内置和/或第三方日志记录提供程序记录要记录的事件和活动。大多数 .NET 内置提供程序使用原始字符串(非结构化格式)存储日志事件。尽管这种日志记录技术有一些好处,但它远非理想,尤其是在处理大量日志记录时。在这种情况下,切换到结构化格式通常是明智的,因为它允许我们查询这些记录以获取相关信息,而不必解析和/或处理它们。借助 Azure 应用程序见解日志记录提供程序,可以通过 Microsoft 维护的外部 NuGet 包获得的 Azure 应用程序见解日志记录提供程序,因此可以在 .NET 中采用结构化日志记录策略。但是,应用程序见解仅在 Azure 生态系统中工作,这可能会成为可访问性和自定义方面的障碍。在 .NET 中实现结构化日志记录的一种很好的替代方法是 Serilog,它是 NuGet 上可用的 .NET 应用程序的开源日志记录库。Serilog 可用于将应用程序日志存储在多个流行的 DBMS 以及第三方服务和其他目标中。可以使用围绕接收器构建的模块化体系结构来定义存储目标,接收器是可以独立安装和配置的输出处理程序集。其他值得注意的Serilog功能包括其他列,可用于向日志事件表添加其他结构化属性。扩充器,使用其他上下文信息扩展日志数据。强大的消息模板语法,允许我们以方便、可读的方式自定义日志消息和属性。