English 简体中文 繁體中文 한국 사람 日本語 Deutsch русский بالعربية TÜRKÇE português คนไทย french
查看: 5|回复: 0

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

[复制链接]
查看: 5|回复: 0

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

[复制链接]
查看: 5|回复: 0

363

主题

0

回帖

1099

积分

金牌会员

积分
1099
智能配送设备

363

主题

0

回帖

1099

积分

金牌会员

积分
1099
2025-2-7 01:10:45 | 显示全部楼层 |阅读模式
目录


引言

在应用程序中,日志记录是一个至关重要的功能。不仅有助于调试和监控应用程序,还能帮助我们了解应用程序的运行状态。
在这个示例中将展示如何实现一个自定义的日志记录器,先说明一下,这个实现和Microsoft.Extensions.Logging、Serilog、NLog什么的无关,这里只是将自定义的日志数据存入数据库中,或许你也可以理解为实现的是一个存数据的“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 logId, bool isSuccess)    {        var logEntry = GetById(logId);        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 logId);    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.MySql和Microsoft.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.Abstractions\Logging.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.Abstractions\Logging.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="logId"></param>    /// <returns></returns>    protected override CustomLogEntry? GetById(string logId)    {        using var connection = new MySqlConnection(_connectionString);        connection.Open();        using var command = new MySqlCommand(Consts.QueryByIdSql, connection);        command.Parameters.AddWithValue("@Id", logId);        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/swashbucklebuilder.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 logId, MessageStatus status)    {        customLogger.LogProcessed(logId, status);        return Ok();    }}
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

363

主题

0

回帖

1099

积分

金牌会员

积分
1099

QQ|智能设备 | 粤ICP备2024353841号-1

GMT+8, 2025-3-10 15:17 , Processed in 2.022263 second(s), 27 queries .

Powered by 智能设备

©2025

|网站地图