在 C# 和 .NET 中的日志记录

.NET 通过 ILogger API 支持高性能结构化日志记录,以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程序将日志写入不同的目标。 基本日志记录提供程序是内置的,并且还可以使用许多第三方提供程序。

开始使用

第一个示例演示了基本信息,但它仅适用于普通控制台应用。 此示例控制台应用依赖于以下 NuGet 包:

在下一部分中,你将了解如何通过考虑缩放、性能、配置和典型编程模式来改进代码。

using Microsoft.Extensions.Logging;

using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole());
ILogger logger = factory.CreateLogger("Program");
logger.LogInformation("Hello World! Logging is {Description}.", "fun");

上面的示例:

  • 创建 ILoggerFactoryILoggerFactory 可存储用于确定发送日志消息的位置的所有配置。 在这种情况下,请配置控制台日志记录提供程序,以便将日志消息写入控制台。
  • 创建类别名称为“Program”的 ILogger类别 是与 ILogger 对象记录的每个消息关联的 string。 它用于在搜索或筛选日志时将同一类(或类别)中的日志消息组合在一起。
  • 调用 LogInformation 以在 Information 级别记录消息。 日志级别可指示所记录事件的严重性,并用于筛选出不太重要的日志消息。 日志条目还包括消息模板 "Hello World! Logging is {Description}." 和键值对 Description = fun。 键名称(或占位符)来自模板中大括号内的单词,值则来自其余的方法参数。

本示例的此项目文件包括两个 NuGet 包:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
  </ItemGroup>

</Project>

提示

所有日志记录示例源代码都可以在示例浏览器中下载。 有关详细信息,请参阅浏览代码示例:.NET 中的日志记录

在非普通应用中进行记录

在不太简单的方案中记录时,应考虑对前面的示例进行一些更改:

  • 如果应用程序正在使用依赖关系注入 (DI) 或主机(如 ASP.NET 的 WebApplication通用主机),则应使用其各自 DI 容器中的 ILoggerFactoryILogger 对象,而不是直接创建它们。 有关详细信息,请参阅《与 DI 和主机集成》。

  • 记录编译时源生成通常是 ILogger 扩展方法(例如 LogInformation)的更好替代方法。 日志记录源生成可提供更好的性能、更强大的键入,并避免在整个方法中分布 string 常量。 代价是使用此技术需要编写稍多一些代码。

using Microsoft.Extensions.Logging;

internal partial class Program
{
    static void Main(string[] args)
    {
        using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole());
        ILogger logger = factory.CreateLogger("Program");
        LogStartupMessage(logger, "fun");
    }

    [LoggerMessage(Level = LogLevel.Information, Message = "Hello World! Logging is {Description}.")]
    static partial void LogStartupMessage(ILogger logger, string description);
}
  • 日志类别名称的建议做法是使用创建日志消息的类的完全限定名称。 这有助于将日志消息关联回生成它们的代码,并在筛选日志时提供良好的控制级别。 CreateLogger 接受 Type,以简化此命名操作的执行。
using Microsoft.Extensions.Logging;

internal class Program
{
    static void Main(string[] args)
    {
        using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole());
        ILogger logger = factory.CreateLogger<Program>();
        logger.LogInformation("Hello World! Logging is {Description}.", "fun");
    }
}
using Microsoft.Extensions.Logging;
using OpenTelemetry.Logs;

using ILoggerFactory factory = LoggerFactory.Create(builder =>
{
    builder.AddOpenTelemetry(logging =>
    {
        logging.AddOtlpExporter();
    });
});
ILogger logger = factory.CreateLogger("Program");
logger.LogInformation("Hello World! Logging is {Description}.", "fun");

与主机和依赖项注入的集成

如果应用程序正在使用依赖关系注入 (DI) 或主机(如 ASP.NET 的 WebApplication通用主机),则应使用 DI 容器中的 ILoggerFactoryILogger 对象,而不是直接创建它们。

从 DI 获取 ILogger

此示例使用 ASP.NET 最小 API获取托管应用中的 ILogger 对象:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<ExampleHandler>();

var app = builder.Build();

var handler = app.Services.GetRequiredService<ExampleHandler>();
app.MapGet("/", handler.HandleRequest);

app.Run();

partial class ExampleHandler(ILogger<ExampleHandler> logger)
{
    public string HandleRequest()
    {
        LogHandleRequest(logger);
        return "Hello World";
    }

    [LoggerMessage(LogLevel.Information, "ExampleHandler.HandleRequest was called")]
    public static partial void LogHandleRequest(ILogger logger);
}

上面的示例:

  • 创建了一个名为 ExampleHandler 的单一实例,并映射传入 Web 请求以运行 ExampleHandler.HandleRequest 函数。
  • 第 8 行定义了 ExampleHandler 的主要构造函数,这是在 C# 12 中添加的功能。 使用较旧样式 C# 构造函数同样能正常工作,但有些过于冗长。
  • 构造函数定义了类型为 ILogger<ExampleHandler> 的参数。 ILogger<TCategoryName> 派生自 ILogger,并指示 ILogger 对象所具有的类别。 DI 容器找到一个类别正确的 ILogger,并提供它作为构造函数参数。 如果尚不存在该类别的 ILogger,则 DI 容器会自动根据服务提供程序的 ILoggerFactory 创建它。
  • 构造函数中收到的 logger 参数用于在 HandleRequest 构造函数中进行记录。

主机提供的 ILoggerFactory

主机生成器会初始化默认配置,然后在生成主机时将已配置的 ILoggerFactory 对象添加到主机的 DI 容器。 在生成主机之前,可以通过 HostApplicationBuilder.LoggingWebApplicationBuilder.Logging 或其他主机上的类似 API 调整记录配置。 主机还会将默认配置源中的日志记录配置作为 appsettings.json 和环境变量应用。 有关详细信息,请参阅 .NET 中的配置

本示例扩展了上一个示例以自定义由 WebApplicationBuilder 提供的 ILoggerFactory。 它将 OpenTelemetry添加为通过 OTLP(OpenTelemetry 协议)传输日志的日志记录提供程序:

var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddOpenTelemetry(logging => logging.AddOtlpExporter());
builder.Services.AddSingleton<ExampleHandler>();
var app = builder.Build();

使用 DI 创建 ILoggerFactory

如果在不使用主机的情况下使用 DI 容器,则使用 AddLogging 配置 ILoggerFactory 并将其添加到容器。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

// Add services to the container including logging
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddConsole());
services.AddSingleton<ExampleService>();
IServiceProvider serviceProvider = services.BuildServiceProvider();

// Get the ExampleService object from the container
ExampleService service = serviceProvider.GetRequiredService<ExampleService>();

// Do some pretend work
service.DoSomeWork(10, 20);

class ExampleService(ILogger<ExampleService> logger)
{
    public void DoSomeWork(int x, int y)
    {
        logger.LogInformation("DoSomeWork was called. x={X}, y={Y}", x, y);
    }
}

上面的示例:

  • 创建了一个 DI 服务容器,其中包含配置为写入控制台的 ILoggerFactory
  • 向容器添加了单一实例 ExampleService
  • 从 DI 容器创建了一个 ExampleService 的实例,该容器也自动创建了要用作构造函数参数的 ILogger<ExampleService>
  • 调用了使用 ILogger<ExampleService> 将消息记录到控制台的 ExampleService.DoSomeWork

配置日志记录

日志记录配置是在代码中或通过外部源(例如配置文件和环境变量)设置的。 尽可能使用外部配置是有益的,因为它可以在不重新生成应用程序的情况下进行更改。 但某些任务(如设置日志记录提供程序)只能从代码进行配置。

在没有代码的情况下配置日志记录

对于使用主机的应用,日志记录配置通常由 appsettings.{Environment}.json 文件的 "Logging" 部分提供。 对于不使用主机的应用,外部配置源是显式设置的,或是在代码中配置的。

以下 appsettings.Development.json 文件由 .NET 辅助角色服务模板生成:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

在上述 JSON 中:

  • 指定了 "Default""Microsoft""Microsoft.Hosting.Lifetime" 日志级别类别。
  • "Default" 值应用于未指定的所有类别,从而有效地将所有类别的所有默认值设置为 "Information"。 可以通过为某个类别指定值来重写此行为。
  • "Microsoft" 类别适用于以 "Microsoft" 开头的所有类别。
  • "Microsoft" 类别在日志级别 Warning 或更高级别记录。
  • "Microsoft.Hosting.Lifetime" 类别比 "Microsoft" 类别更具体,因此 "Microsoft.Hosting.Lifetime" 类别在日志级别 "Information" 和更高级别记录。
  • 未指定特定的日志提供程序,因此 LogLevel 适用于所有启用的日志记录提供程序,但 Windows EventLog 除外。

Logging 属性可以具有 LogLevel 和日志提供程序属性。 LogLevel 指定要针对所选类别进行记录的最低级别。 在前面的 JSON 中,指定了 InformationWarning 日志级别。 LogLevel 表示日志的严重性,范围为 0 到 6:

Trace = 0、Debug = 1、Information = 2、Warning = 3、Error = 4、Critical = 5 和 None = 6。

指定 LogLevel 时,将为指定级别和更高级别的消息启用日志记录。 在前面的 JSON 中,记录了 Information 及更高级别的 Default 类别。 例如,记录了 InformationWarningErrorCritical 消息。 如果未指定 LogLevel,则日志记录默认为 Information 级别。 有关详细信息,请参阅日志级别

提供程序属性可以指定 LogLevel 属性。 提供程序下的 LogLevel 指定要为该提供程序记录的级别,并替代非提供程序日志设置。 请考虑使用以下 appsettings.json 文件:

{
    "Logging": {
        "LogLevel": {
            "Default": "Error",
            "Microsoft": "Warning"
        },
        "Debug": {
            "LogLevel": {
                "Default": "Information",
                "Microsoft.Hosting": "Trace"
            }
        },
        "EventSource": {
            "LogLevel": {
                "Default": "Warning"
            }
        }
    }
}

Logging.{ProviderName}.LogLevel 中的设置将替代 Logging.LogLevel 中的设置。 在前面的 JSON 中,Debug 提供程序的默认日志级别设置为 Information

Logging:Debug:LogLevel:Default:Information

前面的设置为每个 Logging:Debug: 类别(Microsoft.Hosting 除外)指定 Information 日志级别。 当列出特定类别时,该特定类别将替代默认类别。 在前面的 JSON 中,Logging:Debug:LogLevel 类别 "Microsoft.Hosting""Default" 替代 Logging:LogLevel 中的设置

可以为以下任何一项指定最低日志级别:

  • 特定提供程序:例如,Logging:EventSource:LogLevel:Default:Information
  • 特定类别:例如,Logging:LogLevel:Microsoft:Warning
  • 所有提供程序和所有类别:Logging:LogLevel:Default:Warning

低于最低级别的任何日志均不会执行以下操作

  • 传递到提供程序。
  • 记录或显示。

要阻止所有日志,请指定 LogLevel.NoneLogLevel.None 的值为 6,该值高于 LogLevel.Critical (5)。

如果提供程序支持日志作用域,则 IncludeScopes 将指示是否启用这些域。 有关详细信息,请参阅日志范围

以下 appsettings.json 文件包含所有内置提供程序的设置:

{
    "Logging": {
        "LogLevel": {
            "Default": "Error",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Warning"
        },
        "Debug": {
            "LogLevel": {
                "Default": "Information"
            }
        },
        "Console": {
            "IncludeScopes": true,
            "LogLevel": {
                "Microsoft.Extensions.Hosting": "Warning",
                "Default": "Information"
            }
        },
        "EventSource": {
            "LogLevel": {
                "Microsoft": "Information"
            }
        },
        "EventLog": {
            "LogLevel": {
                "Microsoft": "Information"
            }
        },
        "AzureAppServicesFile": {
            "IncludeScopes": true,
            "LogLevel": {
                "Default": "Warning"
            }
        },
        "AzureAppServicesBlob": {
            "IncludeScopes": true,
            "LogLevel": {
                "Microsoft": "Information"
            }
        },
        "ApplicationInsights": {
            "LogLevel": {
                "Default": "Information"
            }
        }
    }
}

在上述示例中:

  • 类别和级别不是建议的值。 提供该示例是为了显示所有默认提供程序。
  • Logging.{ProviderName}.LogLevel 中的设置将替代 Logging.LogLevel 中的设置。 例如,Debug.LogLevel.Default 中的级别将替代 LogLevel.Default 中的级别。
  • 将使用每个提供程序的别名。 每个提供程序都定义了一个别名;可在配置中使用该别名来代替完全限定的类型名称。 内置提供程序的别名包括:
    • Console
    • Debug
    • EventSource
    • EventLog
    • AzureAppServicesFile
    • AzureAppServicesBlob
    • ApplicationInsights

通过命令行、环境变量和其他配置设置日志级别

日志级别可以由任何配置提供程序设置。 例如,可以创建一个名为 Logging:LogLevel:Microsoft 且值为 Information 的持久性环境变量。

在给定日志级别值的情况下,创建并分配持久性环境变量。

:: Assigns the env var to the value
setx "Logging__LogLevel__Microsoft" "Information" /M

在命令提示符的新实例中,读取环境变量。

:: Prints the env var value
echo %Logging__LogLevel__Microsoft%

前面的环境设置会在环境中持续存在。 若要在使用通过 .NET 辅助角色服务模板创建的应用时测试这些设置,请在分配环境变量后,在项目目录中使用 dotnet run 命令。

dotnet run

提示

设置环境变量后,请重启集成开发环境 (IDE),以确保新添加的环境变量可用。

Azure 应用服务上,选择“设置”>“配置”页面上的“新应用程序设置”。 Azure 应用服务应用程序设置:

  • 已静态加密且通过加密的通道进行传输。
  • 已作为环境变量公开。

若要详细了解如何使用环境变量设置 .NET 配置值,请参阅环境变量

使用代码配置日志记录

要在代码中配置日志记录,请使用 ILoggingBuilder API。 这可以从不同的位置访问:

此示例演示如何设置控制台日志记录提供程序和多个筛选器

using Microsoft.Extensions.Logging;

using var loggerFactory = LoggerFactory.Create(static builder =>
{
    builder
        .AddFilter("Microsoft", LogLevel.Warning)
        .AddFilter("System", LogLevel.Warning)
        .AddFilter("LoggingConsoleApp.Program", LogLevel.Debug)
        .AddConsole();
});

ILogger logger = loggerFactory.CreateLogger<Program>();
logger.LogDebug("Hello {Target}", "Everyone");

在前面的示例中,AddFilter 用于调整针对各种类别而启用的日志级别AddConsole 用于添加控制台日志记录提供程序。 默认情况下,不会启用严重性为 Debug 的日志,但由于配置调整了筛选器,因此控制台上会显示调试消息“Hello Everyone”。

如何应用筛选规则

创建 ILogger<TCategoryName> 对象时,ILoggerFactory 对象将根据提供程序选择一条规则,将其应用于该记录器。 将按所选规则筛选 ILogger 实例写入的所有消息。 从可用规则中为每个提供程序和类别对选择最具体的规则。

在为给定的类别创建 ILogger 时,以下算法将用于每个提供程序:

  • 选择匹配提供程序或其别名的所有规则。 如果找不到任何匹配项,则选择提供程序为空的所有规则。
  • 根据上一步的结果,选择具有最长匹配类别前缀的规则。 如果找不到任何匹配项,则选择未指定类别的所有规则。
  • 如果选择了多条规则,则采用最后一条 。
  • 如果未选择任何规则,请使用 LoggingBuilderExtensions.SetMinimumLevel(ILoggingBuilder, LogLevel) 指定最小日志记录级别。

日志类别

创建 ILogger 对象时,将指定类别。 该类别包含在由此 ILogger 实例创建的每条日志消息中。 类别字符串是任意的,但约定将使用完全限定的类名称。 例如,在服务定义类似于以下对象的应用程序中,类别可能为 "Example.DefaultService"

namespace Example
{
    public class DefaultService : IService
    {
        private readonly ILogger<DefaultService> _logger;

        public DefaultService(ILogger<DefaultService> logger) =>
            _logger = logger;

        // ...
    }
}

如需进一步分类,约定可使用分层名称,方法是将子类别追加到完全限定的类名称,然后使用 LoggerFactory.CreateLogger 显式指定类别:

namespace Example
{
    public class DefaultService : IService
    {
        private readonly ILogger _logger;

        public DefaultService(ILoggerFactory loggerFactory) =>
            _logger = loggerFactory.CreateLogger("Example.DefaultService.CustomCategory");

        // ...
    }
}

在多个类/类型中使用时,使用固定名称调用 CreateLogger 很有用,这样可以按类别组织事件。

ILogger<T> 相当于使用 T 的完全限定类型名称来调用 CreateLogger

日志级别

下表列出了 LogLevel 值、方便的 Log{LogLevel} 扩展方法以及建议的用法:

LogLevel “值” 方法 描述
Trace 0 LogTrace 包含最详细的消息。 这些消息可能包含敏感的应用数据。 这些消息默认情况下处于禁用状态,并且不应在生产中启用
调试 1 LogDebug 用于调试和开发。 由于量大,请在生产中小心使用。
信息 2 LogInformation 跟踪应用的常规流。 可能具有长期值。
警告 3 LogWarning 对于异常事件或意外事件。 通常包括不会导致应用失败的错误或情况。
错误 4 LogError 表示无法处理的错误和异常。 这些消息表示当前操作或请求失败,而不是整个应用失败。
严重 5 LogCritical 需要立即关注的失败。 例如数据丢失、磁盘空间不足。
6 指定不应写入任何消息。

在上表中,LogLevel 按严重性由低到高的顺序列出。

Log 方法的第一个参数 LogLevel 指示日志的严重性。 大多数开发人员调用 Log{LogLevel} 扩展方法,而不调用 Log(LogLevel, ...)Log{LogLevel} 扩展方法会调用 Log 该方法并指定 LogLevel。 例如,以下两个日志记录调用功能相同,并生成相同的日志:

public void LogDetails()
{
    var logMessage = "Details for log.";

    _logger.Log(LogLevel.Information, AppLogEvents.Details, logMessage);
    _logger.LogInformation(AppLogEvents.Details, logMessage);
}

AppLogEvents.Details 为事件 ID,用常量 Int32 值隐式表示。 AppLogEvents 是一个公开各种命名的标识符常量并显示在日志事件 ID 部分中的类。

下面的代码会创建 InformationWarning 日志:

public async Task<T> GetAsync<T>(string id)
{
    _logger.LogInformation(AppLogEvents.Read, "Reading value for {Id}", id);

    var result = await _repository.GetAsync(id);
    if (result is null)
    {
        _logger.LogWarning(AppLogEvents.ReadNotFound, "GetAsync({Id}) not found", id);
    }

    return result;
}

在前面的代码中,第一个 Log{LogLevel} 参数 AppLogEvents.Read日志事件 ID。 第二个参数是消息模板,其中的占位符用于填写剩余方法形参提供的实参值。 稍后将在本文的消息模板部分介绍方法参数。

配置适当的日志级别并调用正确的 Log{LogLevel} 方法来控制写入特定存储介质的日志输出量。 例如:

  • 生产中:
    • TraceDebug 级别记录日志会产生大量详细的日志消息。 为了控制成本且不超过数据存储限制,请将 TraceDebug 级别消息记录到容量大、成本低的数据存储中。 考虑将 TraceDebug 限制为特定类别。
    • WarningCritical 级别的日志记录应该很少产生日志消息。
      • 成本和存储限制通常不是问题。
      • 很少有日志可以为数据存储选择提供更大的灵活性。
  • 在开发过程中:
    • 设置为 Warning
    • 在进行故障排除时,添加 TraceDebug 消息。 若要限制输出,请仅对正在调查的类别设置 TraceDebug

以下 JSON 设置了 Logging:Console:LogLevel:Microsoft:Information

{
    "Logging": {
        "LogLevel": {
            "Microsoft": "Warning"
        },
        "Console": {
            "LogLevel": {
                "Microsoft": "Information"
            }
        }
    }
}

日志事件 ID

每个日志可指定一个事件标识符,EventId 是一个具有 Id 和可选 Name 只读属性的结构。 示例源代码使用 AppLogEvents 类来定义事件 ID:

using Microsoft.Extensions.Logging;

internal static class AppLogEvents
{
    internal static EventId Create = new(1000, "Created");
    internal static EventId Read = new(1001, "Read");
    internal static EventId Update = new(1002, "Updated");
    internal static EventId Delete = new(1003, "Deleted");

    // These are also valid EventId instances, as there's
    // an implicit conversion from int to an EventId
    internal const int Details = 3000;
    internal const int Error = 3001;

    internal static EventId ReadNotFound = 4000;
    internal static EventId UpdateNotFound = 4001;

    // ...
}

提示

若要详细了解如何将 int 转换为 EventId,请参阅EventId.Implicit(Int32 to EventId) 运算符

事件 ID 与一组事件相关联。 例如,与从存储库读取值相关的所有日志都可能是 1001

日志记录提供程序可将事件 ID 记录在 ID 字段中,记录在日志记录消息中,或者不进行记录。 调试提供程序不显示事件 ID。 控制台提供程序在类别后的括号中显示事件 ID:

info: Example.DefaultService.GetAsync[1001]
      Reading value for a1b2c3
warn: Example.DefaultService.GetAsync[4000]
      GetAsync(a1b2c3) not found

一些日志记录提供程序将事件 ID 存储在一个字段中,该字段允许对 ID 进行筛选。

日志消息模板

每个日志 API 都使用一个消息模板。 消息模板可包含要填写参数的占位符。 请在占位符中使用名称而不是数字。 占位符的顺序(而非其名称)决定了为其提供值的参数。 在以下代码中,消息模板中的参数名称不按顺序排列:

string p1 = "param1";
string p2 = "param2";
_logger.LogInformation("Parameter values: {p2}, {p1}", p1, p2);

上面的代码按顺序通过参数值创建日志消息:

Parameter values: param1, param2

注意

请注意,在单个消息模板中使用多个占位符时,这些占位符是基于序号的。 这些名称不用于将参数与占位符对齐。

此方法允许日志记录提供程序实现语义或结构化日志记录。 参数本身会传递给日志记录系统,而不仅仅是格式化的消息模板。 这使日志记录提供程序可以将参数值存储为字段。 请考虑使用以下记录器方法:

_logger.LogInformation("Getting item {Id} at {RunTime}", id, DateTime.Now);

例如,登录到 Azure 表存储时:

  • 每个 Azure 表实体都可以有 IDRunTime 属性。
  • 具有属性的表简化了对记录数据的查询。 例如,查询可以找到特定 RunTime 范围内的所有日志,而不必分析文本消息中的时间。

日志消息模板格式设置

日志消息模板支持占位符格式设置。 模板可随意指定给定类型参数的任何有效格式。 例如,请考虑使用以下 Information 记录器消息模板:

_logger.LogInformation("Logged on {PlaceHolderName:MMMM dd, yyyy}", DateTimeOffset.UtcNow);
// Logged on January 06, 2022

在上一示例中,DateTimeOffset 实例是与记录器消息模板中的 PlaceHolderName 相对应的类型。 由于值基于序号,因此该名称可以是任何内容。 对于 DateTimeOffset 类型来说,MMMM dd, yyyy 格式有效。

若要详细了解 DateTimeDateTimeOffset 格式设置,请参阅自定义日期和时间格式字符串

示例

以下示例演示如何使用 {} 占位符语法设置消息模板格式。 此外,还显示一个转义 {} 占位符语法的示例及其输出。 最后,还显示带有模板化占位符的字符串内插:

logger.LogInformation("Number: {Number}", 1);               // Number: 1
logger.LogInformation("{{Number}}: {Number}", 3);           // {Number}: 3
logger.LogInformation($"{{{{Number}}}}: {{Number}}", 5);    // {Number}: 5

提示

  • 在大多数情况下,进行日志记录时,你应该使用日志消息模板格式。 使用字符串内插可能会导致性能问题。
  • 代码分析规则 CA2254:模板应为静态表达式有助于提醒你日志消息未使用正确格式的位置。

记录异常

记录器方法的重载采用异常参数:

public void Test(string id)
{
    try
    {
        if (id is "none")
        {
            throw new Exception("Default Id detected.");
        }
    }
    catch (Exception ex)
    {
        _logger.LogWarning(
            AppLogEvents.Error, ex,
            "Failed to process iteration: {Id}", id);
    }
}

异常日志记录是特定于提供程序的。

默认日志级别

如果未设置默认日志级别,则默认的日志级别值为 Information

例如,请考虑以下辅助角色服务应用:

  • 使用 .NET 辅助角色模板创建的应用。
  • 已删除 appsettings.json 和 appsettings.Development.json 或对其进行重命名 。

使用上述设置,导航到隐私或主页会生成许多 TraceDebugInformation 消息,并在类别名称中包含 Microsoft

如果未在配置中设置默认日志级别,以下代码会设置默认日志级别:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Logging.SetMinimumLevel(LogLevel.Warning);

using IHost host = builder.Build();

await host.RunAsync();

筛选器函数

对配置或代码没有向其分配规则的所有提供程序和类别调用筛选器函数:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Logging.AddFilter((provider, category, logLevel) =>
{
    return provider.Contains("ConsoleLoggerProvider")
        && (category.Contains("Example") || category.Contains("Microsoft"))
        && logLevel >= LogLevel.Information;
});

using IHost host = builder.Build();

await host.RunAsync();

如果类别包含 ExampleMicrosoft,并且日志级别为 Information 或更高级别,以上代码会显示控制台日志。

日志作用域

作用域可对一组逻辑操作进行分组。 此分组可用于将相同的数据附加到作为集合的一部分而创建的每个日志。 例如,在处理事务期间创建的每个日志都可包括事务 ID。

范围:

以下提供程序支持范围:

要使用作用域,请在 using 块中包装记录器调用:

public async Task<T> GetAsync<T>(string id)
{
    T result;
    var transactionId = Guid.NewGuid().ToString();

    using (_logger.BeginScope(new List<KeyValuePair<string, object>>
        {
            new KeyValuePair<string, object>("TransactionId", transactionId),
        }))
    {
        _logger.LogInformation(
            AppLogEvents.Read, "Reading value for {Id}", id);

        var result = await _repository.GetAsync(id);
        if (result is null)
        {
            _logger.LogWarning(
                AppLogEvents.ReadNotFound, "GetAsync({Id}) not found", id);
        }
    }

    return result;
}

以下 JSON 为控制台提供程序启用范围:

{
    "Logging": {
        "Debug": {
            "LogLevel": {
                "Default": "Information"
            }
        },
        "Console": {
            "IncludeScopes": true,
            "LogLevel": {
                "Microsoft": "Warning",
                "Default": "Information"
            }
        },
        "LogLevel": {
            "Default": "Debug"
        }
    }
}

下列代码为控制台提供程序启用作用域:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Logging.ClearProviders();
builder.Logging.AddConsole(options => options.IncludeScopes = true);

using IHost host = builder.Build();

await host.RunAsync();

在 Main 中创建日志

以下代码通过在构建主机之后从 DI 获取 ILogger 实例来登录 Main

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

using IHost host = Host.CreateApplicationBuilder(args).Build();

var logger = host.Services.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Host created.");

await host.RunAsync();

前面的代码依赖于两个 NuGet 包:

其项目文件类似于以下内容:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
    <PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
  </ItemGroup>

</Project>

没有异步记录器方法

日志记录应该会很快,不值得牺牲性能来使用异步代码。 如果日志记录数据存储很慢,请不要直接写入它。 考虑先将日志消息写入快速存储,然后再将其移至慢速存储。 例如,登录到 SQL Server 时,请勿直接使用 Log 方法登录,因为 Log 方法是同步的。 相反,你会将日志消息同步添加到内存中的队列,并让后台辅助线程从队列中拉出消息,以完成将数据推送到 SQL Server 的异步工作。

更改正在运行的应用中的日志级别

不可使用日志记录 API 在应用运行时更改日志记录。 但是,一些配置提供程序可重新加载配置,这将对日志记录配置立即产生影响。 例如,文件配置提供程序默认情况下会重载日志记录配置。 如果在应用运行时在代码中更改了配置,该应用可调用 IConfigurationRoot.Reload 来更新应用的日志记录配置。

NuGet 包

ILogger<TCategoryName>ILoggerFactory 接口和实现将作为隐式包引用包含在大多数 .NET SDK 中。 如果未隐式引用,它们也会在以下 NuGet 包中显式提供:

如需详细了解哪些 .NET SDK 包含隐式包引用,请参阅 .NET SDK:有关隐式命名空间的表

另请参阅