面向 .NET 库创建者的日志记录指南

作为库创建者,公开日志记录是向使用者提供有关库内部工作原理的见解的好方法。 本指南可帮助你以与其他 .NET 库和框架一致的方式公开日志记录。 它还有助于避免可能不明显的常见性能瓶颈。

何时使用 ILoggerFactory 接口

编写发出日志的库时,需要一个 ILogger 对象来记录日志。 若要获取该对象,API 可以接受 ILogger<TCategoryName> 参数,也可以接受 ILoggerFactory,之后调用 ILoggerFactory.CreateLogger。 应首选哪种方法?

  • 如果需要一个日志记录对象,并且该对象可传递给多个类,以使所有这些类都可发出日志,请使用 ILoggerFactory。 建议每个类创建具有单独类别的日志,其名称与类相同。 为此,需要工厂为每个发出日志的类创建唯一 ILogger<TCategoryName> 对象。 常见示例包括库或公共构造函数的公共入口点 API,这些构造函数可能在内部创建帮助程序类。

  • 如果需要仅在一个类中使用且从未共享的日志记录对象,请使用 ILogger<TCategoryName>,其中 TCategoryName 是生成日志的类型。 一个常见示例是依赖项注入创建的类的构造函数。

如果要设计一个必须随时间推移保持稳定的公共 API,请记住,将来可能需要重构内部实现。 即使类最初不创建任何内部帮助程序类型,随着代码的发展,这些类型也会发生变化。 使用 ILoggerFactory 可以为任何新类创建新的 ILogger<TCategoryName> 对象,无需更改公共 API。

有关详细信息,请参阅如何应用筛选规则

首选源生成的日志记录

ILogger API 支持两种使用该 API 的方法。 可以调用 LoggerExtensions.LogErrorLoggerExtensions.LogInformation 等方法,也可以使用日志记录源生成器来定义强类型日志记录方法。 在大多数情况下,建议使用源生成器,因为它提供卓越的性能和更强的键入功能。 它还将特定于日志记录的问题(例如消息模板、ID 和日志级别)与调用代码隔离开来。 非源生成的方法主要适用于你愿意放弃这些优势以使代码更加简洁的场景。

using Microsoft.Extensions.Logging;

namespace Logging.LibraryAuthors;

internal static partial class LogMessages
{
    [LoggerMessage(
        Message = "Sold {Quantity} of {Description}",
        Level = LogLevel.Information)]
    internal static partial void LogProductSaleDetails(
        this ILogger logger,
        int quantity,
        string description);
}

前面的代码:

  • 定义一个名为 LogMessagespartial class(其状态为 static),使其可用于定义 ILogger 类型的扩展方法。
  • 使用 LoggerMessage 属性和 Message 模板修饰 LogProductSaleDetails 扩展方法。
  • 声明 LogProductSaleDetails,它扩展 ILogger 并接受 quantitydescription

提示

你可以在调试期间单步执行源生成的代码,因为它属于调用它的代码所在的程序集。

使用 IsEnabled 来避免昂贵的参数评估

在某些情况下,评估参数的成本可能很高。 在上述示例的基础上展开,假设 description 参数是计算成本高昂的 string。 正在销售的产品可能获得易记的产品描述,并依赖于数据库查询或从文件中读取。 在这些情况下,可以指示源生成器跳过 IsEnabled 保护,并在呼叫站点手动添加 IsEnabled 保护。 这使得用户能够确定调用保护的位置,并确保仅在真正需要时才评估计算成本可能很昂贵的参数。 考虑下列代码:

using Microsoft.Extensions.Logging;

namespace Logging.LibraryAuthors;

internal static partial class LogMessages
{
    [LoggerMessage(
        Message = "Sold {Quantity} of {Description}",
        Level = LogLevel.Information,
        SkipEnabledCheck = true)]
    internal static partial void LogProductSaleDetails(
        this ILogger logger,
        int quantity,
        string description);
}

调用 LogProductSaleDetails 扩展方法时,会手动调用 IsEnabled 保护,并且仅在需要时进行成本高昂的参数评估。 考虑下列代码:

if (_logger.IsEnabled(LogLevel.Information))
{
    // Expensive parameter evaluation
    var description = product.GetFriendlyProductDescription();

    _logger.LogProductSaleDetails(
        quantity,
        description);
}

有关详细信息,请参阅编译时日志记录源生成.NET 中的高性能日志记录

在日志记录中避免字符串内插

常见的错误是使用字符串内插来生成日志消息。 日志记录中的字符串内插会对性能造成问题,因为即使未启用相应的 LogLevel,也会评估字符串。 使用日志消息模板、格式设置和参数列表,而不是字符串内插。 有关详细信息,请参阅 .NET 中的日志记录:日志消息模板

使用 no-op 日志记录默认值

有时,当使用公开需要 ILoggerILoggerFactory 的日志记录 API 的库时,你可能不想提供记录器。 在这些情况下,Microsoft.Extensions.Logging.Abstractions NuGet 包提供无操作日志记录默认值。

如果未提供任何 ILoggerFactory,库使用者可以默认为 null 日志记录。 使用 null 日志记录不同于将类型定义为可为 null (ILoggerFactory?),因为类型为非 null。 这些便利的类型不会记录任何内容,本质上是 no-op(无操作)。 请考虑在适用情况下使用任何可用的抽象类型: