最佳实践
遵循这些最佳实践,以充分利用 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 取决于两点
- 您正在构建的应用程序的类型。
- 您想记录日志的位置。
一般而言:
- 如果您正在构建一个具有 依赖注入 (DI) 的应用程序(例如 ASP.NET Core 和 .NET Worker),在大多数情况下,您应该使用 DI 提供的 logger,只有在您想在 DI 日志记录管道可用之前或在 DI 日志记录管道被释放之后进行日志记录的特殊情况下,才需要注意。请参阅 .NET 官方文档 和 5 分钟上手 OpenTelemetry .NET 日志 - ASP.NET Core 应用程序 教程了解更多信息。
- 如果您正在构建一个没有 DI 的应用程序,请创建一个 LoggerFactory 实例,并配置 OpenTelemetry 与之配合工作。请参阅 5 分钟上手 OpenTelemetry .NET 日志 - 控制台应用程序 教程了解更多信息。
使用点分隔的 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);
}
在使用 LoggerMessageAttribute 时,无需传递显式的 EventId。在代码生成期间,将根据方法名称的哈希值自动分配一个持久化的 EventId。
如果您需要记录复杂对象,请使用 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 调用不会带来任何性能提升。例如
以下代码中的 logger.IsEnabled(LogLevel.Information) 调用不会带来任何性能提升。
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);
}
在使用编译时源生成器时,检测到的第一个 Exception 参数会自动获得特殊处理。它 **不应** 包含在消息模板中。有关详细信息,请参阅:Log method anatomy。
在使用日志扩展方法时,您应该使用专用的重载来记录异常。
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 来跟踪进度或提供反馈和建议。
日志编辑
日志可能包含敏感信息,如密码和信用卡号,需要进行适当的编辑以防止隐私和安全事件。请查看 日志编辑 教程了解更多信息。