引言
在应用程序中,日志记录是一个至关重要的功能。不仅有助于调试和监控应用程序,还能帮助我们了解应用程序的运行状态。
在这个示例中将展示如何实现一个自定义的日志记录器,先说明一下,这个实现和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 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.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="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.ntityframeworkcore
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(); } }
以上就是.net core 实现一个自定义日志记录器的详细内容,更多关于.net core日志记录的资料请关注代码网其它相关文章!
发表评论