简单的日志记录

提示

可以从 GitHub 中下载本文的示例

Entity Framework Core (EF Core) 简单日志记录可用于在开发和调试应用程序时轻松获取日志。 这种形式的日志记录只需进行极少的配置,不需要额外的 NuGet 包。

提示

EF Core 还与 Microsoft.Extensions.Logging 集成,后者需要进行更多配置,但通常更适合在生产应用程序中进行日志记录。

配置

可以在配置 DbContext 实例时使用 LogTo 从任意类型的应用程序访问 EF Core 日志。 此配置通常通过替代 DbContext.OnConfiguring 来完成。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.LogTo(Console.WriteLine);

或者,可将 LogTo 作为 AddDbContext 的一部分来调用,或者在创建 DbContextOptions 实例以传递给 DbContext 构造函数时进行调用。

提示

当使用 AddDbContext 或将 DbContextOptions 实例传递给 DbContext 构造函数时,仍会调用 OnConfiguring。 这使得它成为应用上下文配置的理想位置,而无需考虑如何构造 DbContext。

引导日志

记录到控制台

LogTo 需要一个接受字符串的 Action<T> 委托。 EF Core 将使用字符串为生成的每条日志消息调用此委托。 然后由该委托对给定消息执行某些操作。

此委托通常使用 Console.WriteLine 方法,如上所示。 这导致每条日志消息都被写入控制台。

记录到调试窗口

Debug.WriteLine 可用于将输出发送到 Visual Studio 或其他 IDE 中的调试窗口。 在这种情况下必须使用 Lambda 语法,因为 Debug 类是从发布版本编译的。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.LogTo(message => Debug.WriteLine(message));

记录到文件

写入文件时,需要为文件创建 StreamWriter 或相似的类。 然后可以像上面的其他示例一样使用 WriteLine 方法。 请记住,通过在释放上下文时释放编写器来确保文件完全关闭。 例如:

private readonly StreamWriter _logStream = new StreamWriter("mylog.txt", append: true);

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.LogTo(_logStream.WriteLine);

public override void Dispose()
{
    base.Dispose();
    _logStream.Dispose();
}

public override async ValueTask DisposeAsync()
{
    await base.DisposeAsync();
    await _logStream.DisposeAsync();
}

提示

请考虑使用 Microsoft.Extensions.Logging 记录到生产应用程序中的文件。

获取详细消息

敏感数据

默认情况下,EF Core 不会在异常消息中包含任何数据的值。 这是因为这些数据可能是机密数据,如果不处理异常,可能会在生产使用中泄露这些数据。

但是,了解数据值(尤其是键值)在调试时非常有用。 可以通过调用 EnableSensitiveDataLogging() 在 EF Core 中启用此功能。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .LogTo(Console.WriteLine)
        .EnableSensitiveDataLogging();

详细查询异常

出于性能原因,EF Core 不会在 try-catch 块中包装每个调用以从数据库提供程序读取值。 但是,这有时会导致难以诊断的异常,尤其是当数据库在模型不允许的情况下返回 NULL 时。

启用 EnableDetailedErrors 将导致 EF 引入这些 try-catch 块,从而提供更详细的错误。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .LogTo(Console.WriteLine)
        .EnableDetailedErrors();

筛选

日志级别

系统会为每条 EF Core 日志消息分配由 LogLevel 枚举定义的级别。 默认情况下,EF Core 简单日志记录包括 Debug 级别或更高级别的每条消息。 LogTo 可以传递一个更高的最低级别来筛选掉某些消息。 例如,传递 Information 会生成一组数量最少的日志(仅限于数据库访问和一些内务处理消息)。

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);

特定消息

每条日志消息都将分配有一个 EventId。 对于特定于关系的消息,可以从 CoreEventId 类或 RelationalEventId 类访问这些 ID。 数据库提供程序也可能在相似的类中具有特定于提供程序的 ID。 例如,SQL Server 提供程序的 SqlServerEventId

LogTo 可以配置为仅记录与一个或多个事件 ID 关联的消息。 例如,仅记录正在初始化或释放的上下文的消息:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .LogTo(Console.WriteLine, new[] { CoreEventId.ContextDisposed, CoreEventId.ContextInitialized });

消息类别

系统会将每条日志消息分配到一个已命名的分层记录器类别。 这些类别包括:

类别 消息
Microsoft.EntityFrameworkCore 所有 EF Core 消息
Microsoft.EntityFrameworkCore.Database 所有数据库交互
Microsoft.EntityFrameworkCore.Database.Connection 使用数据库连接
Microsoft.EntityFrameworkCore.Database.Command 使用数据库命令
Microsoft.EntityFrameworkCore.Database.Transaction 使用数据库事务
Microsoft.EntityFrameworkCore.Update 正在保存实体,不包括数据库交互
Microsoft.EntityFrameworkCore.Model 所有模型和元数据交互
Microsoft.EntityFrameworkCore.Model.Validation 模型验证
Microsoft.EntityFrameworkCore.Query 查询,不包括数据库交互
Microsoft.EntityFrameworkCore.Infrastructure 常规事件,例如上下文创建
Microsoft.EntityFrameworkCore.Scaffolding 数据库反向工程
Microsoft.EntityFrameworkCore.Migrations 迁移
Microsoft.EntityFrameworkCore.ChangeTracking 更改跟踪交互

LogTo 可以配置为仅记录一个或多个类别的消息。 例如,仅记录数据库交互:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .LogTo(Console.WriteLine, new[] { DbLoggerCategory.Database.Name });

请注意,DbLoggerCategory 类将提供一个用于查找某个类别的分层 API,并且无需对字符串进行硬编码。

由于类别是分层的,因此,这个使用 Database 类别的示例将包含子类别 Database.ConnectionDatabase.CommandDatabase.Transaction 的所有消息。

自定义筛选器

LogTo 允许在上述筛选选项不够用的情况下使用自定义筛选器。 例如,若要记录 Information 级别或更高级别的任何消息,以及有关打开和关闭连接的消息:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .LogTo(
            Console.WriteLine,
            (eventId, logLevel) => logLevel >= LogLevel.Information
                                   || eventId == RelationalEventId.ConnectionOpened
                                   || eventId == RelationalEventId.ConnectionClosed);

提示

使用自定义筛选器或使用此处所示的任何其他选项进行筛选比在 LogTo 委托中筛选更有效。 这是因为,如果筛选器确定不应记录消息,那么就连日志消息也不会创建。

特定消息的配置

通过 EF Core ConfigureWarnings API,应用程序可以更改遇到特定事件时发生的情况。 这可以用于:

  • 更改记录事件的日志级别
  • 完全跳过事件记录
  • 发生事件时引发异常

更改事件的日志级别

上一个示例使用自定义筛选器记录 LogLevel.Information 级别的每条消息以及为 LogLevel.Debug 定义的两个事件。 通过将两个 Debug 事件的日志级别更改为 Information,可以实现相同的目的:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .ConfigureWarnings(
            b => b.Log(
                (RelationalEventId.ConnectionOpened, LogLevel.Information),
                (RelationalEventId.ConnectionClosed, LogLevel.Information)))
        .LogTo(Console.WriteLine, LogLevel.Information);

禁止记录事件

可以通过类似的方式取消记录单个事件。 这对于忽略已查看和了解的警告特别有用。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .ConfigureWarnings(b => b.Ignore(CoreEventId.DetachedLazyLoadingWarning))
        .LogTo(Console.WriteLine);

针对事件引发异常

最后,EF Core 可以配置为针对给定事件引发异常。 这对于将警告更改为错误特别有用。 (这正是 ConfigureWarnings 方法的最初目的,并因此而得名。)例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .ConfigureWarnings(b => b.Throw(RelationalEventId.MultipleCollectionIncludeWarning))
        .LogTo(Console.WriteLine);

消息内容和格式设置

LogTo 中的默认内容跨多行设置格式。 第一行包含消息元数据:

  • LogLevel,作为四字符前缀
  • 本地时间戳,针对当前区域性设置格式
  • EventId,采用可以复制/粘贴以从 CoreEventId 或其他 EventId 类之一获取成员的格式,外加原始 ID 值
  • 事件类别,如上所述。

例如:

info: 10/6/2020 10:52:45.581 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Blogs" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Blogs" PRIMARY KEY AUTOINCREMENT,
          "Name" INTEGER NOT NULL
      );
dbug: 10/6/2020 10:52:45.582 RelationalEventId.TransactionCommitting[20210] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committing transaction.
dbug: 10/6/2020 10:52:45.585 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

可以通过从 DbContextLoggerOptions 传递值来自定义此内容,如以下部分所示。

提示

请考虑使用 Microsoft.Extensions.Logging 进一步控制日志格式设置。

使用 UTC 时间

默认情况下,时间戳设计为进行调试时在本地使用。 通过 DbContextLoggerOptions.DefaultWithUtcTime 改用与区域性无关的 UTC 时间戳,其他内容保持不变。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.LogTo(
        Console.WriteLine,
        LogLevel.Debug,
        DbContextLoggerOptions.DefaultWithUtcTime);

此示例将导致以下日志格式设置:

info: 2020-10-06T17:55:39.0333701Z RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Blogs" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Blogs" PRIMARY KEY AUTOINCREMENT,
          "Name" INTEGER NOT NULL
      );
dbug: 2020-10-06T17:55:39.0333892Z RelationalEventId.TransactionCommitting[20210] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committing transaction.
dbug: 2020-10-06T17:55:39.0351684Z RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

单行日志记录

有时,每条日志消息只占一行会很有用。 这可以通过 DbContextLoggerOptions.SingleLine 启用。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.LogTo(
        Console.WriteLine,
        LogLevel.Debug,
        DbContextLoggerOptions.DefaultWithLocalTime | DbContextLoggerOptions.SingleLine);

此示例将导致以下日志格式设置:

info: 10/6/2020 10:52:45.723 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) -> Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']CREATE TABLE "Blogs" (    "Id" INTEGER NOT NULL CONSTRAINT "PK_Blogs" PRIMARY KEY AUTOINCREMENT,    "Name" INTEGER NOT NULL);
dbug: 10/6/2020 10:52:45.723 RelationalEventId.TransactionCommitting[20210] (Microsoft.EntityFrameworkCore.Database.Transaction) -> Committing transaction.
dbug: 10/6/2020 10:52:45.725 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction) -> Committed transaction.

其他内容选项

DbContextLoggerOptions 中的其他标志可用于减少日志中包含的元数据量。 与单行日志记录结合使用时,可能会很有用。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.LogTo(
        Console.WriteLine,
        LogLevel.Debug,
        DbContextLoggerOptions.UtcTime | DbContextLoggerOptions.SingleLine);

此示例将导致以下日志格式设置:

2020-10-06T17:52:45.7320362Z -> Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']CREATE TABLE "Blogs" (    "Id" INTEGER NOT NULL CONSTRAINT "PK_Blogs" PRIMARY KEY AUTOINCREMENT,    "Name" INTEGER NOT NULL);
2020-10-06T17:52:45.7320531Z -> Committing transaction.
2020-10-06T17:52:45.7339441Z -> Committed transaction.

从 EF6 迁移

EF Core 简单日志记录与 EF6 中的 Database.Log 有两个重要的区别:

  • 日志消息不仅限于数据库交互
  • 必须在上下文初始化时配置日志记录

对于第一个区别,可以使用上述筛选选项限制记录哪些消息。

第二个区别是一项有意更改,旨在通过在不需要时不生成日志消息来提高性能。 但是,通过在 DbContext 上创建 Log 属性,然后仅在设置后才使用它,仍然可以获得与 EF6 类似的行为。 例如:

public Action<string> Log { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.LogTo(s => Log?.Invoke(s));