最佳实践

了解使用 OpenTelemetry .NET 进行日志记录的最佳实践

遵循这些最佳实践,以充分利用 OpenTelemetry .NET 进行日志记录。

日志 API

ILogger

.NET 通过 Microsoft.Extensions.Logging.ILogger 接口(包括 ILogger<TCategoryName>)支持高性能、结构化日志记录,以帮助监控应用程序行为和诊断问题。

包版本

使用最新稳定版本的 Microsoft.Extensions.Logging 包中的 ILogger 接口(包括 ILogger<TCategoryName>),无论使用的是哪个 .NET 运行时版本。

  • 如果您正在使用最新稳定版本的 OpenTelemetry .NET SDK,您无需担心 Microsoft.Extensions.Logging 包的版本,因为通过包依赖项,它已经为您处理好了。
  • 3.1.0 版本开始,.NET 运行时团队在 Microsoft.Extensions.Logging 上保持了高标准的向后兼容性,即使在主版本更新期间也是如此,因此兼容性在此处不是问题。

获取 Logger

要使用 ILogger 接口,您需要先获取一个 logger。如何获取 logger 取决于两点

  • 您正在构建的应用程序的类型。
  • 您想记录日志的位置。

一般而言:

使用点分隔的 UpperCamelCase 作为日志类别名称,这使得 过滤日志 更加方便。常见的做法是使用完全限定的类名,如果需要进一步的分类,则添加子类别名称。请参阅 .NET 官方文档 了解更多信息。例如

loggerFactory.CreateLogger<MyClass>(); // this is equivalent to CreateLogger("MyProduct.MyLibrary.MyClass")
loggerFactory.CreateLogger("MyProduct.MyLibrary.MyClass"); // use the fully qualified class name
loggerFactory.CreateLogger("MyProduct.MyLibrary.MyClass.DatabaseOperations"); // append a subcategory name
loggerFactory.CreateLogger("MyProduct.MyLibrary.MyClass.FileOperations"); // append another subcategory name

避免频繁创建 logger。虽然 logger 的开销并不大,但它们仍然会消耗 CPU 和内存,并且旨在在整个应用程序中重复使用。

写入日志消息

使用结构化日志记录。

  • 结构化日志记录比非结构化日志记录更有效。
    • 过滤和编辑可以针对单个键值对进行,而不是整个日志消息。
    • 存储和索引更有效。
  • 结构化日志记录使日志的管理和消费更加容易。

例如

var food = "tomato";
var price = 2.99;

logger.LogInformation("Hello from {food} {price}.", food, price);

避免使用字符串插值。例如

var food = "tomato";
var price = 2.99;

logger.LogInformation($"Hello from {food} {price}.");

使用 编译时日志源生成 模式以获得最佳性能。例如

var food = "tomato";
var price = 2.99;

logger.SayHello(food, price);

internal static partial class LoggerExtensions
{
    [LoggerMessage(Level = LogLevel.Information, Message = "Hello from {food} {price}.")]
    public static partial void SayHello(this ILogger logger, string food, double price);
}

如果您需要记录复杂对象,请使用 Microsoft.Extensions.Telemetry.Abstractions 中的 LogPropertiesAttribute。有关更多详细信息,请查看 记录复杂对象 教程。

避免使用 LoggerExtensions 中的扩展方法,这些方法没有针对性能进行优化。例如

var food = "tomato";
var price = 2.99;

logger.LogInformation("Hello from {food} {price}.", food, price);

在使用 ILogger.IsEnabled 时,保持高标准。

日志 API 针对大多数 logger 对某些日志级别 **禁用** 的场景进行了高度优化。在记录日志之前进行额外的 IsEnabled 调用不会带来任何性能提升。例如

var food = "tomato";
var price = 2.99;

if (logger.IsEnabled(LogLevel.Information)) // do not do this, there is no perf gain
{
    logger.SayHello(food, price);
}

internal static partial class LoggerExtensions
{
    [LoggerMessage(Level = LogLevel.Information, Message = "Hello from {food} {price}.")]
    public static partial void SayHello(this ILogger logger, string food, double price);
}

当参数的评估成本很高时,IsEnabled 可以带来性能优势。例如,在以下代码中,如果 logger 未启用,则会跳过 Database.GetFoodPrice 的调用

if (logger.IsEnabled(LogLevel.Information))
{
    logger.SayHello(food, Database.GetFoodPrice(food));
}

尽管 IsEnabled 在上述场景中可以带来一些性能优势,但对于大多数用户来说,它可能会导致更多问题。例如,代码的性能现在取决于哪个 logger 被启用,更不用说参数的评估可能具有显著的副作用,而这些副作用现在取决于日志配置。

在使用编译时源生成器时,请使用专用参数来记录异常。例如

var food = "tomato";
var price = 2.99;

try
{
    // Execute some logic

    logger.SayHello(food, price);
}
catch (Exception ex)
{
    logger.SayHelloFailure(ex, food, price);
}

internal static partial class LoggerExtensions
{
    [LoggerMessage(Level = LogLevel.Information, Message = "Hello from {food} {price}.")]
    public static partial void SayHello(this ILogger logger, string food, double price);

    [LoggerMessage(Level = LogLevel.Error, Message = "Could not say hello from {food} {price}.")]
    public static partial void SayHelloFailure(this ILogger logger, Exception exception, string food, double price);
}

在使用日志扩展方法时,您应该使用专用的重载来记录异常。

var food = "tomato";
var price = 2.99;

try
{
    // Execute some logic

    logger.LogInformation("Hello from {food} {price}.", food, price);
}
catch (Exception ex)
{
    logger.LogError(ex, "Could not say hello from {food} {price}.", food, price);
}

避免将异常详细信息添加到消息模板中。例如

您应该使用正确的 Exception API,因为 OpenTelemetry 规范 定义了专用的属性 来描述 Exception 详细信息。以下示例显示了 **不应** 做的事情。在这些情况下,详细信息不会丢失,但专用的属性也不会被添加。

var food = "tomato";
var price = 2.99;

try
{
    // Execute some logic

    logger.SayHello(food, price);
}
catch (Exception ex)
{
    logger.SayHelloFailure(food, price, ex.Message);
}

internal static partial class LoggerExtensions
{
    [LoggerMessage(Level = LogLevel.Information, Message = "Hello from {food} {price}.")]
    public static partial void SayHello(this ILogger logger, string food, double price);

    // BAD - Exception should not be part of the message template. Use the dedicated parameter.
    [LoggerMessage(Level = LogLevel.Error, Message = "Could not say hello from {food} {price} {message}.")]
    public static partial void SayHelloFailure(this ILogger logger, string food, double price, string message);
}
var food = "tomato";
var price = 2.99;

try
{
    // Execute some logic

    logger.LogInformation("Hello from {food} {price}.", food, price);
}
catch (Exception ex)
{
    // BAD - Exception should not be part of the message template. Use the dedicated parameter.
    logger.LogError("Could not say hello from {food} {price} {message}.", food, price, ex.Message);
}

LoggerFactory

在许多情况下,您可以直接使用 ILogger,而无需直接与 Microsoft.Extensions.Logging.LoggerFactory 进行交互。本节内容适用于需要显式创建和管理 LoggerFactory 的用户。

避免频繁创建 LoggerFactory 实例,LoggerFactory 的开销相当大,并且旨在在整个应用程序中重复使用。对于大多数应用程序,每个进程一个 LoggerFactory 实例就足够了。

如果您自己创建了 LoggerFactory 实例,请管理其生命周期。

  • 如果您在应用程序结束前忘记释放 LoggerFactory 实例,由于缺少适当的刷新,日志可能会丢失。
  • 如果您过早地释放了 LoggerFactory 实例,任何后续与该 logger factory 相关的日志 API 调用都可能成为 no-op(即,不会发出任何日志)。

日志关联

在 OpenTelemetry 中,日志会自动与 跟踪 相关联。请查看 日志关联 教程了解更多信息。

日志过滤

.NET 团队计划在 .NET 9 的时间范围内涵盖更高级的过滤和采样功能,请使用此 运行时 issue 来跟踪进度或提供反馈和建议。

日志编辑

日志可能包含敏感信息,如密码和信用卡号,需要进行适当的编辑以防止隐私和安全事件。请查看 日志编辑 教程了解更多信息。


最后修改日期:2025年8月14日: 添加其余dotnet内容 (#7543) (79e067c8)