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

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

何时使用 ILoggerFactory 接口

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

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

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

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

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

首选源生成的日志记录

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);
}

前面的代码:

  • 定义一个名为 partial classLogMessages(其状态为 static),使其可用于定义 ILogger 类型的扩展方法。
  • 使用LogProductSaleDetails属性和LoggerMessage模板来修饰Message扩展方法。
  • 声明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 包提供 no-op 日志记录默认值。

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