使用 .NET Core 实现一个自定义日志记录器

引言

在应用程序中,日志记录是一个至关重要的功能。不仅有助于调试和监控应用程序,还能帮助我们了解应用程序的运行状态。
在这个示例中将展示如何实现一个自定义的日志记录器,先说明一下,这个实现和Microsoft.Extensions.LoggingSerilogNLog什么的无关,这里只是将自定义的日志数据存入数据库中,或许你也可以理解为实现的是一个存数据的“Repository”,只不过用这个Repository来存的是日志🙈。这个实现包含一个抽象包和两个实现包,两个实现分别是用 EntityFramework Core 和 MySqlConnector 。日志记录操作将放在本地队列中异步处理,以确保不影响业务处理。

1. 抽象包

1.1 定义日志记录接口

首先,我们需要定义一个日志记录接口 ICustomLogger,它包含两个方法:LogReceived 和 LogProcessed。LogReceived 用于记录接收到的日志,LogProcessed 用于更新日志的处理状态。

namespace Logging.Abstractions;  public interface ICustomLogger {     /// <summary>     /// 记录一条日志     /// </summary>     void LogReceived(CustomLogEntry logEntry);      /// <summary>     /// 根据Id更新这条日志     /// </summary>     void LogProcessed(string logId, bool isSuccess); }  

定义一个日志结构实体CustomLogEntry,用于存储日志的详细信息:

namespace Logging.Abstractions;  public class CustomLogEntry {     /// <summary>     /// 日志唯一Id,数据库主键     /// </summary>     public string Id { get; set; } = Guid.NewGuid().ToString();     public string Message { get; set; } = default!;     public bool IsSuccess { get; set; }     public DateTime CreateTime { get; set; } = DateTime.UtcNow;     public DateTime? UpdateTime { get; set; } = DateTime.UtcNow; } 

1.2 定义日志记录抽象类

接下来,定义一个抽象类CustomLogger,它实现了ICustomLogger接口,并提供了日志记录的基本功能,将日志写入操作(插入or更新)放在本地队列中异步处理。使用ConcurrentQueue来确保线程安全,并开启一个后台任务异步处理这些日志。这个抽象类只负责将日志写入命令放到队列中,实现类负责消费队列中的消息,确定日志应该怎么写?往哪里写?这个示例中后边会有两个实现,一个是基于EntityFramework Core的实现,另一个是MySqlConnector的实现。

封装一下日志写入命令

namespace Logging.Abstractions;  public class WriteCommand(WriteCommandType commandType, CustomLogEntry logEntry) {     public WriteCommandType CommandType { get; } = commandType;     public CustomLogEntry LogEntry { get; } = logEntry; }  public enum WriteCommandType {     /// <summary>     /// 插入     /// </summary>     Insert,      /// <summary>     /// 更新     /// </summary>     Update } 

CustomLogger实现

using System.Collections.Concurrent; using Microsoft.Extensions.Logging;  namespace Logging.Abstractions;  public abstract class CustomLogger : ICustomLogger, IDisposable, IAsyncDisposable {     protected ILogger<CustomLogger> Logger { get; }      protected ConcurrentQueue<WriteCommand> WriteQueue { get; }      protected Task WriteTask { get; }     private readonly CancellationTokenSource _cancellationTokenSource;     private readonly CancellationToken _cancellationToken;      protected CustomLogger(ILogger<CustomLogger> logger)     {         Logger = logger;          WriteQueue = new ConcurrentQueue<WriteCommand>();         _cancellationTokenSource = new CancellationTokenSource();         _cancellationToken = _cancellationTokenSource.Token;         WriteTask = Task.Factory.StartNew(TryWriteAsync, _cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default);     }      public void LogReceived(CustomLogEntry logEntry)     {         WriteQueue.Enqueue(new WriteCommand(WriteCommandType.Insert, logEntry));     }      public void LogProcessed(string messageId, bool isSuccess)     {         var logEntry = GetById(messageId);         if (logEntry == null)         {             return;         }          logEntry.IsSuccess = isSuccess;         logEntry.UpdateTime = DateTime.UtcNow;         WriteQueue.Enqueue(new WriteCommand(WriteCommandType.Update, logEntry));     }      private async Task TryWriteAsync()     {         try         {             while (!_cancellationToken.IsCancellationRequested)             {                 if (WriteQueue.IsEmpty)                 {                     await Task.Delay(1000, _cancellationToken);                     continue;                 }                  if (WriteQueue.TryDequeue(out var writeCommand))                 {                     await WriteAsync(writeCommand);                 }             }              while (WriteQueue.TryDequeue(out var remainingCommand))             {                 await WriteAsync(remainingCommand);             }         }         catch (OperationCanceledException)         {             // 任务被取消,正常退出         }         catch (Exception e)         {             Logger.LogError(e, "处理待写入日志队列异常");         }     }      protected abstract CustomLogEntry? GetById(string messageId);      protected abstract Task WriteAsync(WriteCommand writeCommand);      public void Dispose()     {         Dispose(true);         GC.SuppressFinalize(this);     }      public async ValueTask DisposeAsync()     {         await DisposeAsyncCore();         Dispose(false);         GC.SuppressFinalize(this);     }      protected virtual void Dispose(bool disposing)     {         if (disposing)         {             _cancellationTokenSource.Cancel();             try             {                 WriteTask.Wait();             }             catch (AggregateException ex)             {                 foreach (var innerException in ex.InnerExceptions)                 {                     Logger.LogError(innerException, "释放资源异常");                 }             }             finally             {                 _cancellationTokenSource.Dispose();             }         }     }      protected virtual async Task DisposeAsyncCore()     {         _cancellationTokenSource.Cancel();         try         {             await WriteTask;         }         catch (Exception e)         {             Logger.LogError(e, "释放资源异常");         }         finally         {             _cancellationTokenSource.Dispose();         }     } } 

1.3 表结构迁移

为了方便表结构迁移,我们可以使用FluentMigrator.Runner.MySql,在项目中引入:

<Project Sdk="Microsoft.NET.Sdk"> 	<PropertyGroup> 		<TargetFramework>net8.0</TargetFramework> 		<ImplicitUsings>enable</ImplicitUsings> 		<Nullable>enable</Nullable> 	</PropertyGroup>  	<ItemGroup> 		<PackageReference Include="FluentMigrator.Runner.MySql" Version="6.2.0" /> 	</ItemGroup> </Project> 

新建一个CreateLogEntriesTable,放在Migrations目录下

[Migration(20241216)] public class CreateLogEntriesTable : Migration {     public override void Up()     {         Create.Table("LogEntries")             .WithColumn("Id").AsString(36).PrimaryKey()             .WithColumn("Message").AsCustom(text)             .WithColumn("IsSuccess").AsBoolean().NotNullable()             .WithColumn("CreateTime").AsDateTime().NotNullable()             .WithColumn("UpdateTime").AsDateTime();     }      public override void Down()     {         Delete.Table("LogEntries");     } } 

添加服务注册

using FluentMigrator.Runner; using Logging.Abstractions; using Logging.Abstractions.Migrations;  namespace Microsoft.Extensions.DependencyInjection;  public static class CustomLoggerExtensions {     /// <summary>     /// 添加自定义日志服务表结构迁移     /// </summary>     /// <param name="services"></param>     /// <param name="connectionString">数据库连接字符串</param>     /// <returns></returns>     public static IServiceCollection AddCustomLoggerMigration(this IServiceCollection services, string connectionString)     {         services.AddFluentMigratorCore()             .ConfigureRunner(                 rb => rb.AddMySql5()                     .WithGlobalConnectionString(connectionString)                     .ScanIn(typeof(CreateLogEntriesTable).Assembly)                     .For.Migrations()             )             .AddLogging(lb =>             {                 lb.AddFluentMigratorConsole();             });          using var serviceProvider = services.BuildServiceProvider();         using var scope = serviceProvider.CreateScope();         var runner = scope.ServiceProvider.GetRequiredService<IMigrationRunner>();         runner.MigrateUp();          return services;     } } 

2. EntityFramework Core 的实现

2.1 数据库上下文

新建Logging.EntityFrameworkCore项目,添加对Logging.Abstractions项目的引用,并在项目中安装Pomelo.EntityFrameworkCore.MySqlMicrosoft.Extensions.ObjectPool

<Project Sdk="Microsoft.NET.Sdk">   <PropertyGroup>     <TargetFramework>net8.0</TargetFramework>     <ImplicitUsings>enable</ImplicitUsings>     <Nullable>enable</Nullable>   </PropertyGroup>    <ItemGroup>     <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="8.0.11" />     <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />   </ItemGroup>    <ItemGroup>     <ProjectReference Include="..Logging.AbstractionsLogging.Abstractions.csproj" />   </ItemGroup>  </Project> 

创建CustomLoggerDbContext类,用于管理日志实体

using Logging.Abstractions; using Microsoft.EntityFrameworkCore;  namespace Logging.EntityFrameworkCore;  public class CustomLoggerDbContext(DbContextOptions<CustomLoggerDbContext> options) : DbContext(options) {     public virtual DbSet<CustomLogEntry> LogEntries { get; set; } } 

使用 ObjectPool 管理 DbContext:提高性能,减少 DbContext 的创建和销毁开销。
创建CustomLoggerDbContextPoolPolicy

using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.ObjectPool;  namespace Logging.EntityFrameworkCore;  /// <summary> /// DbContext 池策略 /// </summary> /// <param name="options"></param> public class CustomLoggerDbContextPoolPolicy(DbContextOptions<CustomLoggerDbContext> options) : IPooledObjectPolicy<CustomLoggerDbContext> {     /// <summary>     /// 创建 DbContext     /// </summary>     /// <returns></returns>     public CustomLoggerDbContext Create()     {         return new CustomLoggerDbContext(options);     }      /// <summary>     /// 回收 DbContext     /// </summary>     /// <param name="context"></param>     /// <returns></returns>     public bool Return(CustomLoggerDbContext context)     {         // 重置 DbContext 状态         context.ChangeTracker.Clear();         return true;      } }  

2.2 实现日志写入

创建一个EfCoreCustomLogger,继承自CustomLogger,实现日志写入的具体逻辑

using Logging.Abstractions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool;  namespace Logging.EntityFrameworkCore;  /// <summary> /// EfCore自定义日志记录器 /// </summary> public class EfCoreCustomLogger(ObjectPool<CustomLoggerDbContext> contextPool, ILogger<EfCoreCustomLogger> logger) : CustomLogger(logger) {     /// <summary>     /// 根据Id查询日志     /// </summary>     /// <param name="logId"></param>     /// <returns></returns>     protected override CustomLogEntry? GetById(string logId)     {         var dbContext = contextPool.Get();         try         {             return dbContext.LogEntries.Find(logId);         }         finally         {             contextPool.Return(dbContext);         }     }      /// <summary>     /// 写入日志     /// </summary>     /// <param name="writeCommand"></param>     /// <returns></returns>     /// <exception cref="ArgumentOutOfRangeException"></exception>     protected override async Task WriteAsync(WriteCommand writeCommand)     {         var dbContext = contextPool.Get();          try         {             switch (writeCommand.CommandType)             {                 case WriteCommandType.Insert:                     if (writeCommand.LogEntry != null)                     {                         await dbContext.LogEntries.AddAsync(writeCommand.LogEntry);                     }                      break;                 case WriteCommandType.Update:                 {                     if (writeCommand.LogEntry != null)                     {                         dbContext.LogEntries.Update(writeCommand.LogEntry);                     }                      break;                 }                 default:                     throw new ArgumentOutOfRangeException();             }              await dbContext.SaveChangesAsync();         }         finally         {             contextPool.Return(dbContext);         }     } } 

添加服务注册

using Logging.Abstractions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.ObjectPool;  namespace Logging.EntityFrameworkCore;  public static class EfCoreCustomLoggerExtensions {     public static IServiceCollection AddEfCoreCustomLogger(this IServiceCollection services, string connectionString)     {         if (string.IsNullOrEmpty(connectionString))         {             throw new ArgumentNullException(nameof(connectionString));         }          services.AddCustomLoggerMigration(connectionString);          services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();         services.AddSingleton(serviceProvider =>         {             var options = new DbContextOptionsBuilder<CustomLoggerDbContext>()                 .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))                 .Options;             var poolProvider = serviceProvider.GetRequiredService<ObjectPoolProvider>();             return poolProvider.Create(new CustomLoggerDbContextPoolPolicy(options));         });          services.AddSingleton<ICustomLogger, EfCoreCustomLogger>();          return services;     } } 

3. MySqlConnector 的实现

MySqlConnector 的实现比较简单,利用原生SQL操作数据库完成日志的插入和更新。
新建Logging.MySqlConnector项目,添加对Logging.Abstractions项目的引用,并安装MySqlConnector

<Project Sdk="Microsoft.NET.Sdk"> 	<PropertyGroup> 		<TargetFramework>net8.0</TargetFramework> 		<ImplicitUsings>enable</ImplicitUsings> 		<Nullable>enable</Nullable> 	</PropertyGroup>  	<ItemGroup> 		<PackageReference Include="MySqlConnector" Version="2.4.0" /> 	</ItemGroup>  	<ItemGroup> 		<ProjectReference Include="..Logging.AbstractionsLogging.Abstractions.csproj" /> 	</ItemGroup>  </Project> 

3.1 SQL脚本

为了方便维护,我们把需要用到的SQL脚本放在一个Consts类中

namespace Logging.MySqlConnector;  public class Consts {     /// <summary>     /// 插入日志     /// </summary>     public const string InsertSql = """                                     INSERT INTO `LogEntries` (`Id`, `TranceId`, `BizType`, `Body`, `Component`, `MsgType`, `Status`, `CreateTime`, `UpdateTime`, `Remark`)                                     VALUES (@Id, @TranceId, @BizType, @Body, @Component, @MsgType, @Status, @CreateTime, @UpdateTime, @Remark);                                     """;      /// <summary>     /// 更新日志     /// </summary>     public const string UpdateSql = """                                     UPDATE `LogEntries` SET `Status` = @Status, `UpdateTime` = @UpdateTime                                     WHERE `Id` = @Id;                                     """;      /// <summary>     /// 根据Id查询日志     /// </summary>     public const string QueryByIdSql = """                                         SELECT `Id`, `TranceId`, `BizType`, `Body`, `Component`, `MsgType`, `Status`, `CreateTime`, `UpdateTime`, `Remark`                                         FROM `LogEntries`                                         WHERE `Id` = @Id;                                         """; } 

3.2 实现日志写入

创建MySqlConnectorCustomLogger类,实现日志写入的具体逻辑

using Logging.Abstractions; using Microsoft.Extensions.Logging; using MySqlConnector;  namespace Logging.MySqlConnector;  /// <summary> /// 使用 MySqlConnector 实现记录日志 /// </summary> public class MySqlConnectorCustomLogger : CustomLogger {      /// <summary>     /// 数据库连接字符串     /// </summary>     private readonly string _connectionString;      /// <summary>     /// 构造函数     /// </summary>     /// <param name="connectionString">MySQL连接字符串</param>     /// <param name="logger"></param>     public MySqlConnectorCustomLogger(         string connectionString,          ILogger<MySqlConnectorCustomLogger> logger)         : base(logger)     {         _connectionString = connectionString;     }      /// <summary>      /// 根据Id查询日志     /// </summary>     /// <param name="messageId"></param>     /// <returns></returns>     protected override CustomLogEntry? GetById(string messageId)     {         using var connection = new MySqlConnection(_connectionString);         connection.Open();          using var command = new MySqlCommand(Consts.QueryByIdSql, connection);         command.Parameters.AddWithValue("@Id", messageId);          using var reader = command.ExecuteReader();         if (!reader.Read())         {             return null;         }          return new CustomLogEntry         {             Id = reader.GetString(0),             Message = reader.GetString(1),             IsSuccess = reader.GetBoolean(2),             CreateTime = reader.GetDateTime(3),             UpdateTime = reader.GetDateTime(4)         };     }      /// <summary>     /// 处理日志     /// </summary>     /// <param name="writeCommand"></param>     /// <returns></returns>     /// <exception cref="ArgumentOutOfRangeException"></exception>     protected override async Task WriteAsync(WriteCommand writeCommand)     {         await using var connection = new MySqlConnection(_connectionString);         await connection.OpenAsync();          switch (writeCommand.CommandType)         {             case WriteCommandType.Insert:                 {                     if (writeCommand.LogEntry != null)                     {                         await using var command = new MySqlCommand(Consts.InsertSql, connection);                         command.Parameters.AddWithValue("@Id", writeCommand.LogEntry.Id);                         command.Parameters.AddWithValue("@Message", writeCommand.LogEntry.Message);                         command.Parameters.AddWithValue("@IsSuccess", writeCommand.LogEntry.IsSuccess);                         command.Parameters.AddWithValue("@CreateTime", writeCommand.LogEntry.CreateTime);                         command.Parameters.AddWithValue("@UpdateTime", writeCommand.LogEntry.UpdateTime);                         await command.ExecuteNonQueryAsync();                     }                      break;                 }             case WriteCommandType.Update:                 {                     if (writeCommand.LogEntry != null)                     {                         await using var command = new MySqlCommand(Consts.UpdateSql, connection);                         command.Parameters.AddWithValue("@Id", writeCommand.LogEntry.Id);                         command.Parameters.AddWithValue("@IsSuccess", writeCommand.LogEntry.IsSuccess);                         command.Parameters.AddWithValue("@UpdateTime", writeCommand.LogEntry.UpdateTime);                         await command.ExecuteNonQueryAsync();                     }                      break;                 }             default:                 throw new ArgumentOutOfRangeException();         }     } } 

添加服务注册

using Logging.Abstractions; using Logging.MySqlConnector; using Microsoft.Extensions.Logging;  namespace Microsoft.Extensions.DependencyInjection;  /// <summary> /// MySqlConnector 日志记录器扩展 /// </summary> public static class MySqlConnectorCustomLoggerExtensions {     /// <summary>     /// 添加 MySqlConnector 日志记录器     /// </summary>     /// <param name="services"></param>     /// <param name="connectionString"></param>     /// <returns></returns>     public static IServiceCollection AddMySqlConnectorCustomLogger(this IServiceCollection services, string connectionString)     {         if (string.IsNullOrEmpty(connectionString))         {             throw new ArgumentNullException(nameof(connectionString));         }          services.AddSingleton<ICustomLogger>(s =>         {             var logger = s.GetRequiredService<ILogger<MySqlConnectorCustomLogger>>();             return new MySqlConnectorCustomLogger(connectionString, logger);         });         services.AddCustomLoggerMigration(connectionString);          return services;     } } 

4. 使用示例

下边是一个EntityFramework Core的实现使用示例,MySqlConnector的使用方式相同。
新建WebApi项目,添加Logging.EntityFrameworkCore

var builder = WebApplication.CreateBuilder(args);  // Add services to the container.  builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen();  // 添加EntityFrameworkCore日志记录器 var connectionString = builder.Configuration.GetConnectionString("MySql"); builder.Services.AddEfCoreCustomLogger(connectionString!);  var app = builder.Build();  // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) {     app.UseSwagger();     app.UseSwaggerUI(); }  app.UseAuthorization();  app.MapControllers();  app.Run(); 

在控制器中使用

namespace EntityFrameworkCoreTest.Controllers;  [ApiController] [Route("[controller]")] public class TestController(ICustomLogger customLogger) : ControllerBase {     [HttpPost("InsertLog")]     public IActionResult Post(CustomLogEntry model)     {         customLogger.LogReceived(model);          return Ok();      }      [HttpPut("UpdateLog")]     public IActionResult Put(string messageId, MessageStatus status)     {         customLogger.LogProcessed(messageId, status);          return Ok();     } }  

发表评论

相关文章

当前内容话题