简单日志记录

小窍门

可以从 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的一部分,或在创建一个要传给DbContext构造函数的DbContextOptions实例时调用。

小窍门

在使用 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。 例如,SqlServerEventId 是 SQL Server 提供程序的示例。

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.Connection的所有消息, Database.Command以及 Database.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));