.NET?Core?實(shí)現(xiàn)一個(gè)自定義日志記錄器
引言
在應(yīng)用程序中,日志記錄是一個(gè)至關(guān)重要的功能。不僅有助于調(diào)試和監(jiān)控應(yīng)用程序,還能幫助我們了解應(yīng)用程序的運(yùn)行狀態(tài)。
在這個(gè)示例中將展示如何實(shí)現(xiàn)一個(gè)自定義的日志記錄器,先說(shuō)明一下,這個(gè)實(shí)現(xiàn)和Microsoft.Extensions.Logging
、Serilog
、NLog
什么的無(wú)關(guān),這里只是將自定義的日志數(shù)據(jù)存入數(shù)據(jù)庫(kù)中,或許你也可以理解為實(shí)現(xiàn)的是一個(gè)存數(shù)據(jù)的“Repository”,只不過(guò)用這個(gè)Repository來(lái)存的是日志。這個(gè)實(shí)現(xiàn)包含一個(gè)抽象包和兩個(gè)實(shí)現(xiàn)包,兩個(gè)實(shí)現(xiàn)分別是用 EntityFramework Core 和 MySqlConnector 。日志記錄操作將放在本地隊(duì)列中異步處理,以確保不影響業(yè)務(wù)處理。
1. 抽象包
1.1 定義日志記錄接口
首先,我們需要定義一個(gè)日志記錄接口 ICustomLogger
,它包含兩個(gè)方法: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); }
定義一個(gè)日志結(jié)構(gòu)實(shí)體CustomLogEntry
,用于存儲(chǔ)日志的詳細(xì)信息:
namespace Logging.Abstractions; public class CustomLogEntry { /// <summary> /// 日志唯一Id,數(shù)據(jù)庫(kù)主鍵 /// </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 定義日志記錄抽象類
接下來(lái),定義一個(gè)抽象類CustomLogger
,它實(shí)現(xiàn)了ICustomLogger
接口,并提供了日志記錄的基本功能,將日志寫(xiě)入操作(插入or更新)放在本地隊(duì)列中異步處理。使用ConcurrentQueue
來(lái)確保線程安全,并開(kāi)啟一個(gè)后臺(tái)任務(wù)異步處理這些日志。這個(gè)抽象類只負(fù)責(zé)將日志寫(xiě)入命令放到隊(duì)列中,實(shí)現(xiàn)類負(fù)責(zé)消費(fèi)隊(duì)列中的消息,確定日志應(yīng)該怎么寫(xiě)?往哪里寫(xiě)?這個(gè)示例中后邊會(huì)有兩個(gè)實(shí)現(xiàn),一個(gè)是基于EntityFramework Core的實(shí)現(xiàn),另一個(gè)是MySqlConnector的實(shí)現(xiàn)。
封裝一下日志寫(xiě)入命令
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, "處理待寫(xiě)入日志隊(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>
新建一個(gè)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ù)注冊(cè)
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ù)庫(kù)連接字符串</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ù)庫(kù)上下文
新建Logging.EntityFrameworkCore項(xiàng)目,添加對(duì)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)建和銷毀開(kāi)銷。
創(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)日志寫(xiě)入
創(chuàng)建一個(gè)EfCoreCustomLogger
,繼承自CustomLogger
,實(shí)現(xiàn)日志寫(xiě)入的具體邏輯
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> /// 寫(xiě)入日志 /// </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ù)注冊(cè)
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)比較簡(jiǎn)單,利用原生SQL操作數(shù)據(jù)庫(kù)完成日志的插入和更新。
新建Logging.MySqlConnector項(xiàng)目,添加對(duì)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腳本放在一個(gè)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)日志寫(xiě)入
創(chuàng)建MySqlConnectorCustomLogger
類,實(shí)現(xiàn)日志寫(xiě)入的具體邏輯
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ù)庫(kù)連接字符串 /// </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ù)注冊(cè)
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. 使用示例
下邊是一個(gè)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)一個(gè)自定義日志記錄器的詳細(xì)內(nèi)容,更多關(guān)于.NET Core日志記錄的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
在ASP.NET Core中顯示自定義的錯(cuò)誤頁(yè)面
大家在用瀏覽器訪問(wèn)服務(wù)器時(shí),不同情況下會(huì)返回不同的信息。服務(wù)器發(fā)生錯(cuò)誤就會(huì)返回錯(cuò)誤信息,我們最熟悉的就是404錯(cuò)誤頁(yè)面,但是這里我想和大家分享下在ASP.NET Core中如何顯示自定義的500或404錯(cuò)誤頁(yè)面,有需要的朋友們可以參考借鑒,下面來(lái)一起看看吧。2016-12-12TreeView無(wú)刷新獲取text及value實(shí)現(xiàn)代碼
這篇文章介紹了TreeView無(wú)刷新獲取text及value實(shí)現(xiàn)代碼,有需要的朋友可以參考一下2013-10-10.net實(shí)現(xiàn)ping的實(shí)例代碼
這篇文章主要介紹了.net實(shí)現(xiàn)ping的實(shí)例代碼,需要的朋友可以參考下2014-02-02為HttpClient添加默認(rèn)請(qǐng)求報(bào)頭的四種解決方案
這篇文章主要給大家介紹了關(guān)于為HttpClient添加默認(rèn)請(qǐng)求報(bào)頭的四種解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用HttpClient具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09asp.net基于HashTable實(shí)現(xiàn)購(gòu)物車的方法
這篇文章主要介紹了asp.net基于HashTable實(shí)現(xiàn)購(gòu)物車的方法,涉及asp.net中HashTable結(jié)合session實(shí)現(xiàn)購(gòu)物車功能的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-12-12在Code First模式中自動(dòng)創(chuàng)建Entity模型
這篇文章介紹了在Code First模式中自動(dòng)創(chuàng)建Entity模型的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06ASP.NET?Core?Web?API中實(shí)現(xiàn)監(jiān)控的方法
本文介紹了在ASP.NETCoreWebAPI中實(shí)現(xiàn)監(jiān)控的幾種流行開(kāi)源工具,可以監(jiān)控API的性能、請(qǐng)求、響應(yīng)時(shí)間、錯(cuò)誤率等,具有一定的參考價(jià)值,感興趣的可以了解一下2025-01-01IdnentiyServer使用客戶端憑據(jù)訪問(wèn)API的實(shí)例代碼
這篇文章主要介紹了IdnentiyServer-使用客戶端憑據(jù)訪問(wèn)API的相關(guān)知識(shí),非常不錯(cuò),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2018-10-10