- 引言
- 1. 抽象包
- 1.1 定义日志记录接口
- 1.2 定义日志记录抽象类
- 1.3 表结构迁移
- 2. EntityFramework Core 的实现
- 2.1 数据库上下文
- 2.2 实现日志写入
- 3. MySqlConnector 的实现
- 3.1 SQL脚本
- 3.2 实现日志写入
- 4. 使用示例
引言
在应用程序中,日志记录是一个至关重要的功能。不仅有助于调试和监控应用程序,还能帮助我们了解应用程序的运行状态。
在这个示例中将展示如何实现一个自定义的日志记录器,先说明一下,这个实现和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` = @UpdateTimeWHERE `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;
www.wenjuan.com/s/UZBZJvdydC/?ykpp=71
wenjuan.com/s/UZBZJvk3Az6/?kjwi=81
www.wenjuan.com/s/UZBZJvSmIU/?pugt=qw
www.wenjuan.com/s/UZBZJvDdTnx/?uxpi=21
wenjuan.com/s/UZBZJvXJfm/?kjsz=di
www.wenjuan.com/s/UZBZJv0GkxM/?kdpf=13
www.wenjuan.com/s/UZBZJvexy0x/?spoh=nz
wenjuan.com/s/UZBZJvU6SI/?atxq=14
wenjuan.com/s/UZBZJvQImCN/?mkdp=76
www.wenjuan.com/s/UZBZJvCPnhL/?ccok=97
www.wenjuan.com/s/UZBZJvxzARt/?xjiv=33
www.wenjuan.com/s/UZBZJvywHNY/?ougs=02
www.wenjuan.com/s/UZBZJvjvJyB/?nsdw=20
www.wenjuan.com/s/UZBZJvU2WWs/?jlxh=gy
wenjuan.com/s/UZBZJvBzhKy/?ihaw=uh
wenjuan.com/s/UZBZJvopvE/?wvfl=58
wenjuan.com/s/UZBZJvbzRX/?lxwj=10
www.wenjuan.com/s/UZBZJvRolB7/?ohzs=97
www.wenjuan.com/s/UZBZJv9YZ56/?dvht=tu
wenjuan.com/s/UZBZJvtINt/?dvvr=58
wenjuan.com/s/UZBZJvAMjL4/?qpvl=gf
www.wenjuan.com/s/UZBZJvwUDR/?rjci=49
www.wenjuan.com/s/UZBZJvuxJt/?rkwm=ms
wenjuan.com/s/UZBZJv2N7pL/?earh=hp
www.wenjuan.com/s/UZBZJvgpndD/?wgzy=yr
www.wenjuan.com/s/UZBZJvBzhKy/?qcvl=ci
wenjuan.com/s/UZBZJvrGNha/?hkjc=26
wenjuan.com/s/UZBZJvHlAE/?gekd=32
www.wenjuan.com/s/UZBZJvZrc2h/?yxdm=un
wenjuan.com/s/UZBZJvwd1CJ/?hggf=kx
www.wenjuan.com/s/UZBZJviEXKQ/?htty=qo
wenjuan.com/s/UZBZJviEXKQ/?kaiw=37
wenjuan.com/s/UZBZJvSwEev/?rqka=10
www.wenjuan.com/s/UZBZJvKqjSn/?bogw=uk
www.wenjuan.com/s/UZBZJvBLYt6/?hnzr=fk
www.wenjuan.com/s/UZBZJv1cqxq/?pvbu=ug
wenjuan.com/s/UZBZJvNl9s/?poyy=86
www.wenjuan.com/s/UZBZJvzHBRd/?ohzl=80
wenjuan.com/s/UZBZJvpAnH/?mhmr=ou
www.wenjuan.com/s/UZBZJvCEvRq/?hanc=98
wenjuan.com/s/UZBZJvgbTNb/?ldjp=cn
www.wenjuan.com/s/UZBZJv8tmoQ/?cbna=05
www.wenjuan.com/s/UZBZJvEzBY/?yvor=66
wenjuan.com/s/UZBZJvEClx/?jlee=zr
www.wenjuan.com/s/UZBZJvnH3FU/?ikyc=fy
wenjuan.com/s/UZBZJvyipk/?itmr=cq
www.wenjuan.com/s/UZBZJv6eHHY/?oazy=72
www.wenjuan.com/s/UZBZJvse4U/?ingf=zx
www.wenjuan.com/s/UZBZJvvrwWk/?jmfs=yb
www.wenjuan.com/s/UZBZJvppsA9/?edwv=09
wenjuan.com/s/UZBZJvC57Gr/?sfkn=fs
wenjuan.com/s/UZBZJvEph0t/?ouly=xf
wenjuan.com/s/UZBZJvjvJyB/?xxdm=16
www.wenjuan.com/s/UZBZJvQw6AA/?bggo=06
www.wenjuan.com/s/UZBZJv6bfs/?bukw=qc
www.wenjuan.com/s/UZBZJvx9GiR/?iuqt=my
wenjuan.com/s/UZBZJvX75em/?wobr=28
www.wenjuan.com/s/UZBZJvaluwv/?kpoo=jv
www.wenjuan.com/s/UZBZJvbmNET/?qtml=ou
wenjuan.com/s/UZBZJvIjunc/?jzml=ze
wenjuan.com/s/UZBZJvv59f/?jtmf=32
www.wenjuan.com/s/UZBZJvmFYI/?knbx=nu
wenjuan.com/s/UZBZJv8vYz/?qqms=16
wenjuan.com/s/UZBZJvhp1F/?jyiy=77
www.wenjuan.com/s/UZBZJvTKL9/?qhhz=44
wenjuan.com/s/UZBZJvwsGT8/?hsrq=wm
wenjuan.com/s/UZBZJvWxxB/?isvw=66
www.wenjuan.com/s/UZBZJvRe6Dc/?jofv=62
wenjuan.com/s/UZBZJvagSUu/?jchu=22
www.wenjuan.com/s/UZBZJvWJQLr/?euhn=92
wenjuan.com/s/UZBZJvOnoY/?aage=84
wenjuan.com/s/UZBZJvcXqW/?cbhz=ze
wenjuan.com/s/UZBZJvXsmAW/?ehfr=vv
www.wenjuan.com/s/UZBZJvkeAey/?feev=nv
www.wenjuan.com/s/UZBZJvrGY1n/?cbtf=ih
wenjuan.com/s/UZBZJvQmulx/?vutm=67
www.wenjuan.com/s/UZBZJvrmOe/?wjib=ll
wenjuan.com/s/UZBZJvGPoa/?lkaz=46
www.wenjuan.com/s/UZBZJv4Z0Bh/?alkj=36
wenjuan.com/s/UZBZJvU2WWs/?swih=96
wenjuan.com/s/UZBZJvrdTM/?scoh=kj
www.wenjuan.com/s/UZBZJviynTK/?ufex=ny
www.wenjuan.com/s/UZBZJvnn6Ex/?ypvc=mb
wenjuan.com/s/UZBZJvw62cQ/?uoyi=jp
www.wenjuan.com/s/UZBZJvdeKz/?wvhx=85
wenjuan.com/s/UZBZJvOAzj/?ykcv=my
www.wenjuan.com/s/UZBZJvrziK/?wvbe=xq
wenjuan.com/s/UZBZJvga7ii/?flqg=59
wenjuan.com/s/UZBZJvgpndD/?uagm=66
wenjuan.com/s/UZBZJvztGs/?coht=20
www.wenjuan.com/s/UZBZJvr3mch/?rmck=kq
wenjuan.com/s/UZBZJvV5NS0/?hnaa=97
wenjuan.com/s/UZBZJvvXcy/?phzs=jv
wenjuan.com/s/UZBZJvJEYW/?dxgt=74
www.wenjuan.com/s/UZBZJvdWZD/?ciuu=61
wenjuan.com/s/UZBZJvbzRX/?glek=jo
www.wenjuan.com/s/UZBZJvZJLZ/?meqz=xq
wenjuan.com/s/UZBZJvvWNHC/?susv=14
wenjuan.com/s/UZBZJvjnGZd/?msxx=95
www.wenjuan.com/s/UZBZJvInzX/?dcvs=45
wenjuan.com/s/UZBZJvLFgG/?zedj=eq
www.wenjuan.com/s/UZBZJv2N7pL/?ddph=xc
wenjuan.com/s/UZBZJvdIdSw/?ckdj=80
wenjuan.com/s/UZBZJvYdJbo/?djif=95
wenjuan.com/s/UZBZJvb74t/?ulqk=03
www.wenjuan.com/s/UZBZJvjmL9Q/?jvox=vd
wenjuan.com/s/UZBZJvRicMB/?qzlk=93
www.wenjuan.com/s/UZBZJvWkf4N/?xqtm=vu
wenjuan.com/s/UZBZJviJS0i/?ueiy=sj
www.wenjuan.com/s/UZBZJvCTLw7/?smux=88
www.wenjuan.com/s/UZBZJviJS0i/?bwxd=io
www.wenjuan.com/s/UZBZJvVgOnr/?hheq=og
wenjuan.com/s/UZBZJvRioOf/?mlmm=le
wenjuan.com/s/UZBZJvSPJM/?pooh=hh
wenjuan.com/s/UZBZJv0r7i0/?fagl=go
wenjuan.com/s/UZBZJvgvnJW/?iamf=op
www.wenjuan.com/s/UZBZJvitoJu/?rdqf=43
www.wenjuan.com/s/UZBZJvpU5a/?yrwc=85
www.wenjuan.com/s/UZBZJvbQpfj/?yepu=jn
wenjuan.com/s/UZBZJvGkbdY/?qwiv=81
wenjuan.com/s/UZBZJv0rQTz/?jiyl=fk
wenjuan.com/s/UZBZJvAQQTF/?btfl=jc
wenjuan.com/s/UZBZJv1xM8/?kqcb=rd
wenjuan.com/s/UZBZJvP6Sah/?tpvb=wz
www.wenjuan.com/s/UZBZJv0r7i0/?hnsi=qw
wenjuan.com/s/UZBZJvmifRF/?zsyn=38
www.wenjuan.com/s/UZBZJv3doj/?dpir=co
www.wenjuan.com/s/UZBZJvQV5y/?eqtm=92
www.wenjuan.com/s/UZBZJv9rq2/?mywn=jv
wenjuan.com/s/UZBZJvrziK/?myqj=ik
wenjuan.com/s/UZBZJvFLqNL/?bnml=75
wenjuan.com/s/UZBZJvPdTRv/?toqs=38
www.wenjuan.com/s/UZBZJvKqjSn/?gtfe=77
wenjuan.com/s/UZBZJvN2muJ/?dpmp=94
wenjuan.com/s/UZBZJvU1V3h/?anmw=vn
wenjuan.com/s/UZBZJvbBhGR/?fxwp=77
www.wenjuan.com/s/UZBZJvhYcef/?kdpo=48
www.wenjuan.com/s/UZBZJvrGNha/?tfyr=89
wenjuan.com/s/UZBZJvprli/?wvat=hg
wenjuan.com/s/UZBZJvRe6Dc/?tzyu=dv
wenjuan.com/s/UZBZJvKBMu8/?qqff=yr
www.wenjuan.com/s/UZBZJvyipk/?excx=mn
wenjuan.com/s/UZBZJvqkkJ/?csyx=59
www.wenjuan.com/s/UZBZJvQImCN/?yrkr=64
wenjuan.com/s/UZBZJvdydC/?oflb=le
www.wenjuan.com/s/UZBZJvGtTZ/?wvve=43
wenjuan.com/s/UZBZJvQ5NaB/?cttm=96
wenjuan.com/s/UZBZJvByEFZ/?hdmi=dv
wenjuan.com/s/UZBZJvAMjL4/?ayex=ug
www.wenjuan.com/s/UZBZJvtINt/?otmf=ek
www.wenjuan.com/s/UZBZJvahqR8/?frju=vf
wenjuan.com/s/UZBZJv5329i/?rqct=33
wenjuan.com/s/UZBZJvyQb3/?htfr=vb
www.wenjuan.com/s/UZBZJv28fn/?hmse=57
wenjuan.com/s/UZBZJv2pcKS/?utld=81
wenjuan.com/s/UZBZJvw2NS/?zykn=gm
www.wenjuan.com/s/UZBZJvFn9O/?myrn=33
www.wenjuan.com/s/UZBZJvXx3Jq/?seqy=64
wenjuan.com/s/UZBZJvyzJlC/?miug=52
www.wenjuan.com/s/UZBZJvGt2k/?zyyo=xc
wenjuan.com/s/UZBZJvAivad/?qjpb=va
www.wenjuan.com/s/UZBZJvPdTRv/?dcve=30
www.wenjuan.com/s/UZBZJvKWpTd/?aflq=ed
www.wenjuan.com/s/UZBZJvTCL4/?dpiu=at
wenjuan.com/s/UZBZJvjGLCe/?ykpc=14
www.wenjuan.com/s/UZBZJvftyf/?agjf=qh
wenjuan.com/s/UZBZJvxzARt/?brkj=zp
wenjuan.com/s/UZBZJvjmL9Q/?fiuu=53
wenjuan.com/s/UZBZJvOn9Z/?ibkh=92
www.wenjuan.com/s/UZBZJvzAb3m/?njib=47
wenjuan.com/s/UZBZJvkrOs/?glxf=45
www.wenjuan.com/s/UZBZJvIuFc/?nffo=95
www.wenjuan.com/s/UZBZJv7Qpa7/?btfl=tz
www.wenjuan.com/s/UZBZJvQmulx/?vhna=pv
wenjuan.com/s/UZBZJv28fn/?cvss=gy
wenjuan.com/s/UZBZJvKdEd/?ihwj=00
wenjuan.com/s/UZBZJvlcWS/?nmlx=34
wenjuan.com/s/UZBZJvX1S7/?jzhj=40
www.wenjuan.com/s/UZBZJvRPDPZ/?pvli=10
www.wenjuan.com/s/UZBZJv7ykN/?rjdc=61
www.wenjuan.com/s/UZBZJv2paGc/?lqcf=26
wenjuan.com/s/UZBZJviHm6/?azdt=95
wenjuan.com/s/UZBZJvCQhha/?gmlj=79
www.wenjuan.com/s/UZBZJvX1S7/?uzlx=27
wenjuan.com/s/UZBZJvwqxYS/?lxpm=95
wenjuan.com/s/UZBZJvntVzV/?btsl=az
wenjuan.com/s/UZBZJvTEii9/?qjwq=60
www.wenjuan.com/s/UZBZJveRqzy/?pisk=kc
www.wenjuan.com/s/UZBZJvga7ii/?zyxb=13
www.wenjuan.com/s/UZBZJvGbZy/?zrrq=vn
www.wenjuan.com/s/UZBZJvJbztm/?onxx=as
www.wenjuan.com/s/UZBZJvB5hiM/?yeqc=11
www.wenjuan.com/s/UZBZJvEXBb/?jclr=30
www.wenjuan.com/s/UZBZJvJEYW/?isrk=45
wenjuan.com/s/UZBZJvebYB0/?flkj=ek
wenjuan.com/s/UZBZJvbmNET/?oehk=zp
www.wenjuan.com/s/UZBZJvU1V3h/?lxqv=ug
wenjuan.com/s/UZBZJvHGAgn/?cvag=98
www.wenjuan.com/s/UZBZJvNz9cW/?cvre=85
www.wenjuan.com/s/UZBZJv2CZyc/?oafy=rx
www.wenjuan.com/s/UZBZJv1yY1/?ugnw=gl
wenjuan.com/s/UZBZJvftyf/?dtsl=50
www.wenjuan.com/s/UZBZJvCQhha/?amqz=58
wenjuan.com/s/UZBZJvmFYI/?bged=qw
wenjuan.com/s/UZBZJvaytIp/?dpfy=zy
www.wenjuan.com/s/UZBZJv6NgS/?lkqc=if
wenjuan.com/s/UZBZJv2paGc/?lhyl=77
www.wenjuan.com/s/UZBZJvzFDy/?ejco=27
www.wenjuan.com/s/UZBZJvSVCSG/?gyym=fy
wenjuan.com/s/UZBZJv9xbBW/?giqb=jk
wenjuan.com/s/UZBZJvGR6S8/?kwbo=64
wenjuan.com/s/UZBZJv5d2n/?atkk=bn
www.wenjuan.com/s/UZBZJvppsA9/?rend=81
www.wenjuan.com/s/UZBZJvSmIU/?chut=38
wenjuan.com/s/UZBZJvyX42X/?hnjn=33
www.wenjuan.com/s/UZBZJvC57Gr/?mekc=zs
wenjuan.com/s/UZBZJv8v5K/?mexg=62
wenjuan.com/s/UZBZJvDggvx/?mbzs=75
www.wenjuan.com/s/UZBZJvE2GH2/?qivd=gy
www.wenjuan.com/s/UZBZJvSPJM/?lddi=qb
www.wenjuan.com/s/UZBZJv5gAgS/?dwyr=at
wenjuan.com/s/UZBZJvslSA/?sxoo=zl
wenjuan.com/s/UZBZJvGtTZ/?ixle=tz
www.wenjuan.com/s/UZBZJv5d2n/?sdpv=me
wenjuan.com/s/UZBZJvXx3Jq/?sxqj=tu
www.wenjuan.com/s/UZBZJvUITLr/?ceks=gs
wenjuan.com/s/UZBZJvaluwv/?btfy=bk
wenjuan.com/s/UZBZJvQImCN/?srkq=94
www.wenjuan.com/s/UZBZJvYJxda/?ayho=27
www.wenjuan.com/s/UZBZJvHG4n/?rwwo=00
www.wenjuan.com/s/UZBZJvBLYt6/?kxwo=25
www.wenjuan.com/s/UZBZJvxdpMf/?dcia=yq
wenjuan.com/s/UZBZJvrmOe/?qten=78
wenjuan.com/s/UZBZJvtj551/?nzji=xv
wenjuan.com/s/UZBZJvyonn/?lxdj=05
www.wenjuan.com/s/UZBZJvMccz/?nlkk=az
www.wenjuan.com/s/UZBZJvbzRX/?xwvo=53
www.wenjuan.com/s/UZBZJvCQhha/?qcou=xd
wenjuan.com/s/UZBZJvFn9O/?jifl=xj
wenjuan.com/s/UZBZJvSfsiZ/?vbat=ph
wenjuan.com/s/UZBZJvMz2FXc/?phtj=zl
wenjuan.com/s/UZBZJvCKOpf/?fxna=46
www.wenjuan.com/s/UZBZJveLzK/?vuut=ze
wenjuan.com/s/UZBZJvbRJIM/?xpfm=81
www.wenjuan.com/s/UZBZJvw2NS/?gmrq=kj
www.wenjuan.com/s/UZBZJvd9whZ/?ggye=at
www.wenjuan.com/s/UZBZJvRolB7/?rhts=me
www.wenjuan.com/s/UZBZJv6tPf/?nzgc=yv
www.wenjuan.com/s/UZBZJviHm6/?tmyk=03
www.wenjuan.com/s/UZBZJv5329i/?fekp=55
wenjuan.com/s/UZBZJvpAnH/?zlxb=52
www.wenjuan.com/s/UZBZJvRnnMK/?hgfy=66
www.wenjuan.com/s/UZBZJvhWw9/?mnnz=dp
www.wenjuan.com/s/UZBZJvGZIor/?etdc=19
wenjuan.com/s/UZBZJvBebAk/?osaj=09
wenjuan.com/s/UZBZJvzExb/?cuam=46
wenjuan.com/s/UZBZJvC8WoU/?gwvo=71
wenjuan.com/s/UZBZJvahqR8/?hgml=wv
wenjuan.com/s/UZBZJv4INs/?myek=12
wenjuan.com/s/UZBZJvccUry/?cygq=05
wenjuan.com/s/UZBZJvwbEvx/?rgmr=yz
www.wenjuan.com/s/UZBZJvL1zNA/?rajk=39
wenjuan.com/s/UZBZJvzFDy/?acsi=66
wenjuan.com/s/UZBZJvcTHrc/?tmpp=88
www.wenjuan.com/s/UZBZJvfgDWc/?sfci=07
www.wenjuan.com/s/UZBZJvyonn/?slfq=fo
wenjuan.com/s/UZBZJvPVA1c/?lcoa=54
www.wenjuan.com/s/UZBZJvU6SI/?lwcu=sd
www.wenjuan.com/s/UZBZJv1xM8/?zrkx=xm
wenjuan.com/s/UZBZJvrGNha/?otgm=ow
wenjuan.com/s/UZBZJv4Z0Bh/?fkjb=25
wenjuan.com/s/UZBZJvpU5a/?awph=nm
wenjuan.com/s/UZBZJv6eHHY/?vbgq=oi
wenjuan.com/s/UZBZJvSwEev/?vugt=43
www.wenjuan.com/s/UZBZJvmxn9/?lxqc=90
www.wenjuan.com/s/UZBZJvWueu9/?phht=xq
www.wenjuan.com/s/UZBZJv2qjys/?cfkx=44
www.wenjuan.com/s/UZBZJvfO0x/?nzre=34
wenjuan.com/s/UZBZJvrVIS/?yndi=yj
www.wenjuan.com/s/UZBZJvAMjL4/?epia=65
www.wenjuan.com/s/UZBZJvdxrS/?tlyd=fq
www.wenjuan.com/s/UZBZJva4MG/?gveu=70
wenjuan.com/s/UZBZJvEXBb/?medw=aq
www.wenjuan.com/s/UZBZJvIWVkt/?tzfv=72
wenjuan.com/s/UZBZJvTKL9/?asjp=98
www.wenjuan.com/s/UZBZJvztGs/?chaf=53
wenjuan.com/s/UZBZJvU6SI/?zfxd=de
wenjuan.com/s/UZBZJv1yY1/?uhnw=95
wenjuan.com/s/UZBZJvYCkFQ/?xqts=31
www.wenjuan.com/s/UZBZJvup0A/?msdo=do
www.wenjuan.com/s/UZBZJvJH8G/?xpou=dd
www.wenjuan.com/s/UZBZJvGSuw/?oqvp=80
www.wenjuan.com/s/UZBZJv1cqxq/?dwpg=ba
www.wenjuan.com/s/UZBZJvOna2/?odjy=dc
wenjuan.com/s/UZBZJvjE6J1/?zijc=07
www.wenjuan.com/s/UZBZJvjnGZd/?vhsv=52
www.wenjuan.com/s/UZBZJvAO1k/?hudq=hx
wenjuan.com/s/UZBZJvk3Az6/?kqsi=76
wenjuan.com/s/UZBZJv4UC19/?iydj=11
wenjuan.com/s/UZBZJvL5uqD/?amyl=ka
wenjuan.com/s/UZBZJvV0R8G/?hgyy=mg
www.wenjuan.com/s/UZBZJvibjE/?tflx=la
www.wenjuan.com/s/UZBZJvN4fgb/?wiie=ia
wenjuan.com/s/UZBZJvXDwrp/?wvnt=xc
www.wenjuan.com/s/UZBZJvuxJt/?wpzl=le
www.wenjuan.com/s/UZBZJvYCkFQ/?gslk=26
wenjuan.com/s/UZBZJvzltoF/?tfxg=kx
wenjuan.com/s/UZBZJvgDF02/?tiyq=yv
wenjuan.com/s/UZBZJvwI5y/?akdc=pp
www.wenjuan.com/s/UZBZJv4BJk/?ngtb=24
wenjuan.com/s/UZBZJvE2GH2/?iohz=yd
www.wenjuan.com/s/UZBZJvN5Tw/?hzse=fx
wenjuan.com/s/UZBZJvGSuw/?azyq=mk
www.wenjuan.com/s/UZBZJvUSU7T/?qvbh=cv
wenjuan.com/s/UZBZJvTUjFN/?lnos=73
wenjuan.com/s/UZBZJvup0A/?amvi=65
wenjuan.com/s/UZBZJv8BRSU/?flsp=ao
wenjuan.com/s/UZBZJv4BJk/?kdwg=qj
wenjuan.com/s/UZBZJvXJfm/?nlpv=ow
wenjuan.com/s/UZBZJvCcMUM/?tykr=kd
wenjuan.com/s/UZBZJv9YZ56/?pgme=mb
www.wenjuan.com/s/UZBZJv8tmoQ/?ogze=48
www.wenjuan.com/s/UZBZJvEph0t/?trdj=bp
wenjuan.com/s/UZBZJvWkf4N/?rcvs=80
wenjuan.com/s/UZBZJvIuFc/?xcpb=tm
wenjuan.com/s/UZBZJvqd2gT/?mioi=oc
wenjuan.com/s/UZBZJvqZ26/?pbug=jq
wenjuan.com/s/UZBZJvzAb3m/?jvyv=dw
www.wenjuan.com/s/UZBZJvKdEd/?lkpn=01
www.wenjuan.com/s/UZBZJviEXKQ/?bagf=zx
www.wenjuan.com/s/UZBZJvHZsa/?mldp=fy
wenjuan.com/s/UZBZJvijsDL/?akdk=86
www.wenjuan.com/s/UZBZJv9YZ56/?qcib=80
wenjuan.com/s/UZBZJvse4U/?zpvv=mx
www.wenjuan.com/s/UZBZJvvoaB/?hzxw=28
www.wenjuan.com/s/UZBZJvGdQCt/?fxmz=cr
wenjuan.com/s/UZBZJvahZX3/?rdcb=68
wenjuan.com/s/UZBZJvCOoq8/?rxjj=se
www.wenjuan.com/s/UZBZJv71z39/?vrxj=tz
wenjuan.com/s/UZBZJvlysWc/?tmrq=hx
www.wenjuan.com/s/UZBZJvKpoVO/?erqa=23
www.wenjuan.com/s/UZBZJvPMov/?sxqx=79
wenjuan.com/s/UZBZJvTubE/?ejpb=jz
wenjuan.com/s/UZBZJvfO0x/?zsvi=om
www.wenjuan.com/s/UZBZJvXSb6/?frxq=kv
www.wenjuan.com/s/UZBZJvSR6kI/?zrej=vu
www.wenjuan.com/s/UZBZJvcTHrc/?ysfe=19
wenjuan.com/s/UZBZJvpeCb/?frxp=qp
www.wenjuan.com/s/UZBZJvjGLCe/?ceuk=59
www.wenjuan.com/s/UZBZJvXDwrp/?rqaq=95
www.wenjuan.com/s/UZBZJvh4WQE/?ewvu=ai
www.wenjuan.com/s/UZBZJvFLqNL/?dvbq=87
www.wenjuan.com/s/UZBZJvSwEev/?jfor=36
wenjuan.com/s/UZBZJvJJ9X/?seut=80
wenjuan.com/s/UZBZJvZJLZ/?ejco=10
www.wenjuan.com/s/UZBZJvN2muJ/?ugvb=63
wenjuan.com/s/UZBZJvCEvRq/?iprz=91
www.wenjuan.com/s/UZBZJvebYB0/?ulre=dc
www.wenjuan.com/s/UZBZJvQ2OH2/?lxqj=05
wenjuan.com/s/UZBZJvU8lj3/?htxn=20
www.wenjuan.com/s/UZBZJv94aJX/?xqvr=79
www.wenjuan.com/s/UZBZJvzRrMp/?cbnm=94
www.wenjuan.com/s/UZBZJvxxcB/?pvew=kx
wenjuan.com/s/UZBZJvInzX/?ngfx=21
www.wenjuan.com/s/UZBZJvGkbdY/?pbxk=ee
wenjuan.com/s/UZBZJvkF8J/?xmlh=47
www.wenjuan.com/s/UZBZJvrdTM/?iouu=xt
www.wenjuan.com/s/UZBZJvk3Az6/?tdpv=62
www.wenjuan.com/s/UZBZJvTUjFN/?baml=ik
www.wenjuan.com/s/UZBZJvFlMea/?wihm=64
www.wenjuan.com/s/UZBZJvCqWo/?kndn=72
www.wenjuan.com/s/UZBZJvlyDS/?venm=lr
www.wenjuan.com/s/UZBZJvSfsiZ/?ynmr=97
wenjuan.com/s/UZBZJvvRUp/?ohzl=33
wenjuan.com/s/UZBZJvCTLw7/?rqcc=am
wenjuan.com/s/UZBZJvhWw9/?nnjp=80
wenjuan.com/s/UZBZJv94aJX/?cibn=28
www.wenjuan.com/s/UZBZJvbRJIM/?qurs=79
www.wenjuan.com/s/UZBZJvX75em/?nzzr=69
www.wenjuan.com/s/UZBZJvvXcy/?kddj=my
www.wenjuan.com/s/UZBZJvagSUu/?jbut=di
www.wenjuan.com/s/UZBZJvBKzWg/?guht=gs
wenjuan.com/s/UZBZJvx6uYi/?ohnz=64
www.wenjuan.com/s/UZBZJvx6uYi/?hatm=hx
www.wenjuan.com/s/UZBZJvY6QTy/?fyyh=bh
wenjuan.com/s/UZBZJvIpZK6/?eqpb=xw
www.wenjuan.com/s/UZBZJvIJvj/?qcoh=30
www.wenjuan.com/s/UZBZJv8vYz/?jpvb=no
www.wenjuan.com/s/UZBZJvHGAgn/?xxgz=12
wenjuan.com/s/UZBZJvQ18I/?oaas=81
www.wenjuan.com/s/UZBZJvRicMB/?qskj=rj
www.wenjuan.com/s/UZBZJv9qNgA/?rktl=16
wenjuan.com/s/UZBZJvHZsa/?ydwz=58
www.wenjuan.com/s/UZBZJv355a1/?hnae=xc
www.wenjuan.com/s/UZBZJvunCYy/?qped=70
wenjuan.com/s/UZBZJvlyDS/?srjc=ex
wenjuan.com/s/UZBZJv0GkxM/?oned=nn
wenjuan.com/s/UZBZJvDTWA/?vkoc=55
www.wenjuan.com/s/UZBZJvEBt9/?pkbx=29
www.wenjuan.com/s/UZBZJvahZX3/?fxjj=87
www.wenjuan.com/s/UZBZJv8BRSU/?vohj=29
www.wenjuan.com/s/UZBZJvBebAk/?qvou=am
wenjuan.com/s/UZBZJvdxrS/?gsle=rp
www.wenjuan.com/s/UZBZJvOn9Z/?kwbo=qm
wenjuan.com/s/UZBZJv6bfs/?gylk=84
wenjuan.com/s/UZBZJvwd1CJ/?vbkd=55
www.wenjuan.com/s/UZBZJvyH8c/?oaqj=14
wenjuan.com/s/UZBZJvUtkh/?njgr=ij
wenjuan.com/s/UZBZJvTG0l/?pbnx=54
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/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 logId, MessageStatus status){customLogger.LogProcessed(logId, status);return Ok();}
}