.NET?Core?實(shí)現(xiàn)一個自定義日志記錄器
引言
在應(yīng)用程序中,日志記錄是一個至關(guān)重要的功能。不僅有助于調(diào)試和監(jiān)控應(yīng)用程序,還能幫助我們了解應(yīng)用程序的運(yùn)行狀態(tài)。
在這個示例中將展示如何實(shí)現(xiàn)一個自定義的日志記錄器,先說明一下,這個實(shí)現(xiàn)和Microsoft.Extensions.Logging、Serilog、NLog什么的無關(guān),這里只是將自定義的日志數(shù)據(jù)存入數(shù)據(jù)庫中,或許你也可以理解為實(shí)現(xiàn)的是一個存數(shù)據(jù)的“Repository”,只不過用這個Repository來存的是日志。這個實(shí)現(xiàn)包含一個抽象包和兩個實(shí)現(xiàn)包,兩個實(shí)現(xiàn)分別是用 EntityFramework Core 和 MySqlConnector 。日志記錄操作將放在本地隊(duì)列中異步處理,以確保不影響業(yè)務(wù)處理。
1. 抽象包
1.1 定義日志記錄接口
首先,我們需要定義一個日志記錄接口 ICustomLogger,它包含兩個方法:LogReceived 和 LogProcessed。LogReceived 用于記錄接收到的日志,LogProcessed 用于更新日志的處理狀態(tài)。
namespace Logging.Abstractions;
public interface ICustomLogger
{
/// <summary>
/// 記錄一條日志
/// </summary>
void LogReceived(CustomLogEntry logEntry);
/// <summary>
/// 根據(jù)Id更新這條日志
/// </summary>
void LogProcessed(string logId, bool isSuccess);
}
定義一個日志結(jié)構(gòu)實(shí)體CustomLogEntry,用于存儲日志的詳細(xì)信息:
namespace Logging.Abstractions;
public class CustomLogEntry
{
/// <summary>
/// 日志唯一Id,數(shù)據(jù)庫主鍵
/// </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,它實(shí)現(xiàn)了ICustomLogger接口,并提供了日志記錄的基本功能,將日志寫入操作(插入or更新)放在本地隊(duì)列中異步處理。使用ConcurrentQueue來確保線程安全,并開啟一個后臺任務(wù)異步處理這些日志。這個抽象類只負(fù)責(zé)將日志寫入命令放到隊(duì)列中,實(shí)現(xiàn)類負(fù)責(zé)消費(fèi)隊(duì)列中的消息,確定日志應(yīng)該怎么寫?往哪里寫?這個示例中后邊會有兩個實(shí)現(xiàn),一個是基于EntityFramework Core的實(shí)現(xiàn),另一個是MySqlConnector的實(shí)現(xiàn)。
封裝一下日志寫入命令
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實(shí)現(xiàn)
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)
{
// 任務(wù)被取消,正常退出
}
catch (Exception e)
{
Logger.LogError(e, "處理待寫入日志隊(duì)列異常");
}
}
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 表結(jié)構(gòu)遷移
為了方便表結(jié)構(gòu)遷移,我們可以使用FluentMigrator.Runner.MySql,在項(xiàng)目中引入:
<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");
}
}
添加服務(wù)注冊
using FluentMigrator.Runner;
using Logging.Abstractions;
using Logging.Abstractions.Migrations;
namespace Microsoft.Extensions.DependencyInjection;
public static class CustomLoggerExtensions
{
/// <summary>
/// 添加自定義日志服務(wù)表結(jié)構(gòu)遷移
/// </summary>
/// <param name="services"></param>
/// <param name="connectionString">數(shù)據(jù)庫連接字符串</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 的實(shí)現(xiàn)
2.1 數(shù)據(jù)庫上下文
新建Logging.EntityFrameworkCore項(xiàng)目,添加對Logging.Abstractions項(xiàng)目的引用,并在項(xiàng)目中安裝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>
創(chuàng)建CustomLoggerDbContext類,用于管理日志實(shí)體
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 的創(chuàng)建和銷毀開銷。
創(chuàng)建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>
/// 創(chuàng)建 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 狀態(tài)
context.ChangeTracker.Clear();
return true;
}
}
2.2 實(shí)現(xiàn)日志寫入
創(chuàng)建一個EfCoreCustomLogger,繼承自CustomLogger,實(shí)現(xiàn)日志寫入的具體邏輯
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>
/// 根據(jù)Id查詢?nèi)罩?
/// </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);
}
}
}
添加服務(wù)注冊
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 的實(shí)現(xiàn)
MySqlConnector 的實(shí)現(xiàn)比較簡單,利用原生SQL操作數(shù)據(jù)庫完成日志的插入和更新。
新建Logging.MySqlConnector項(xiàng)目,添加對Logging.Abstractions項(xiàng)目的引用,并安裝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腳本
為了方便維護(hù),我們把需要用到的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>
/// 根據(jù)Id查詢?nèi)罩?
/// </summary>
public const string QueryByIdSql = """
SELECT `Id`, `TranceId`, `BizType`, `Body`, `Component`, `MsgType`, `Status`, `CreateTime`, `UpdateTime`, `Remark`
FROM `LogEntries`
WHERE `Id` = @Id;
""";
}
3.2 實(shí)現(xiàn)日志寫入
創(chuàng)建MySqlConnectorCustomLogger類,實(shí)現(xiàn)日志寫入的具體邏輯
using Logging.Abstractions;
using Microsoft.Extensions.Logging;
using MySqlConnector;
namespace Logging.MySqlConnector;
/// <summary>
/// 使用 MySqlConnector 實(shí)現(xiàn)記錄日志
/// </summary>
public class MySqlConnectorCustomLogger : CustomLogger
{
/// <summary>
/// 數(shù)據(jù)庫連接字符串
/// </summary>
private readonly string _connectionString;
/// <summary>
/// 構(gòu)造函數(shù)
/// </summary>
/// <param name="connectionString">MySQL連接字符串</param>
/// <param name="logger"></param>
public MySqlConnectorCustomLogger(
string connectionString,
ILogger<MySqlConnectorCustomLogger> logger)
: base(logger)
{
_connectionString = connectionString;
}
/// <summary>
/// 根據(jù)Id查詢?nèi)罩?
/// </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();
}
}
}
添加服務(wù)注冊
using Logging.Abstractions;
using Logging.MySqlConnector;
using Microsoft.Extensions.Logging;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// MySqlConnector 日志記錄器擴(kuò)展
/// </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的實(shí)現(xiàn)使用示例,MySqlConnector的使用方式相同。
新建WebApi項(xiàng)目,添加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 實(shí)現(xiàn)一個自定義日志記錄器的詳細(xì)內(nèi)容,更多關(guān)于.NET Core日志記錄的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
.NET?Core控制臺應(yīng)用ConsoleApp讀取appsettings.json配置文件
這篇文章介紹了.NET?Core控制臺應(yīng)用ConsoleApp讀取appsettings.json配置文件的方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-07-07
asp.net 頁面逐步呈現(xiàn)的方法總結(jié)
分塊編碼 ( chunked encoding )就是讓 response 分塊編碼進(jìn)行傳輸。response 分塊編碼,可以先傳輸一部分不需要處理的 html 代碼到客戶端,等其他耗時代碼執(zhí)行完畢后再傳輸另外的 html 代碼。2010-06-06
.Net通過TaskFactory.FromAsync簡化APM
這篇文章介紹了.Net通過TaskFactory.FromAsync簡化APM的方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-06-06
asp.net下出現(xiàn)其中的組件“訪問被拒絕”的解決方法
asp.net下出現(xiàn)其中的組件“訪問被拒絕”的解決方法...2007-04-04
asp.net下獲取遠(yuǎn)程網(wǎng)頁的內(nèi)容之二(downmoon原創(chuàng))
asp.net下獲取遠(yuǎn)程網(wǎng)頁的內(nèi)容之二(downmoon原創(chuàng))...2007-04-04
asp.net(c#)ref,out ,params的區(qū)別
C#中有三個關(guān)鍵字-ref,out ,params,雖然本人不喜歡這三個關(guān)鍵字,因?yàn)樗鼈円伤破茐拿嫦驅(qū)ο筇匦?。但是既然m$把融入在c#體系中,那么我們就來認(rèn)識一下參數(shù)修飾符ref,out ,params吧,還有它們的區(qū)別。2009-12-12
DataList 中動態(tài)綁定服務(wù)器子控件的代碼
DataList 中動態(tài)綁定服務(wù)器子控件的代碼...2007-09-09

