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