introduction
Logging is a crucial feature in applications. Not only helps debug and monitor applications, it also helps us understand the operation status of the application.
In this example, we will show how to implement a custom logger. Let's first explain this implementation and、
Serilog
、NLog
What's wrong, here is just to store custom log data into the database, maybe you can understand it asWhat is implemented is a "Repository" that stores data, but the logs are stored using this Repository.. This implementation contains an abstract package and two implementation packages, two implementations are respectively EntityFramework Core and MySqlConnector. Logging operations will be processed asynchronously in the local queue to ensure that they do not affect business processing.
1. Abstract package
1.1 Defining a logging interface
First, we need to define a logging interfaceICustomLogger
, it contains two methods: LogReceived and LogProcessed. LogReceived is used to record the received log, and LogProcessed is used to update the processing status of the log.
namespace ; public interface ICustomLogger { /// <summary> /// Record a log /// </summary> void LogReceived(CustomLogEntry logEntry); /// <summary> /// Update this log according to ID /// </summary> void LogProcessed(string logId, bool isSuccess); }
Define a log structure entityCustomLogEntry
, used to store log details:
namespace ; public class CustomLogEntry { /// <summary> /// The log unique ID, the database primary key /// </summary> public string Id { get; set; } = ().ToString(); public string Message { get; set; } = default!; public bool IsSuccess { get; set; } public DateTime CreateTime { get; set; } = ; public DateTime? UpdateTime { get; set; } = ; }
1.2 Defining logging abstract class
Next, define an abstract classCustomLogger
, it implementsICustomLogger
Interface and provides basic functions of logging, log writing operations (insert or updates) are processed asynchronously in a local queue. useConcurrentQueue
to ensure thread safety and enable a background task to process these logs asynchronously. This abstract class is only responsible for putting the logs into the queue. The implementation class is responsible for consuming the messages in the queue. To determine how to write the logs? Where to write? There will be two implementations in this example, one is based on EntityFramework Core, and the other is the MySqlConnector implementation.
Encapsulate the log writing command
namespace ; public class WriteCommand(WriteCommandType commandType, CustomLogEntry logEntry) { public WriteCommandType CommandType { get; } = commandType; public CustomLogEntry LogEntry { get; } = logEntry; } public enum WriteCommandType { /// <summary> /// Insert /// </summary> Insert, /// <summary> /// renew /// </summary> Update }
CustomLogger
accomplish
using ; using ; namespace ; 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 = (TryWriteAsync, _cancellationToken, , ); } public void LogReceived(CustomLogEntry logEntry) { (new WriteCommand(, logEntry)); } public void LogProcessed(string messageId, bool isSuccess) { var logEntry = GetById(messageId); if (logEntry == null) { return; } = isSuccess; = ; (new WriteCommand(, logEntry)); } private async Task TryWriteAsync() { try { while (!_cancellationToken.IsCancellationRequested) { if () { await (1000, _cancellationToken); continue; } if ((out var writeCommand)) { await WriteAsync(writeCommand); } } while ((out var remainingCommand)) { await WriteAsync(remainingCommand); } } catch (OperationCanceledException) { // The task is cancelled and exits normally } catch (Exception e) { (e, "Troubleshoot log queue exceptions to be written"); } } protected abstract CustomLogEntry? GetById(string messageId); protected abstract Task WriteAsync(WriteCommand writeCommand); public void Dispose() { Dispose(true); (this); } public async ValueTask DisposeAsync() { await DisposeAsyncCore(); Dispose(false); (this); } protected virtual void Dispose(bool disposing) { if (disposing) { _cancellationTokenSource.Cancel(); try { (); } catch (AggregateException ex) { foreach (var innerException in ) { (innerException, "Release resource exception"); } } finally { _cancellationTokenSource.Dispose(); } } } protected virtual async Task DisposeAsyncCore() { _cancellationTokenSource.Cancel(); try { await WriteTask; } catch (Exception e) { (e, "Release resource exception"); } finally { _cancellationTokenSource.Dispose(); } } }
1.3 Table structure migration
To facilitate table structure migration, we can use, introduced in the project:
<Project Sdk=""> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="" Version="6.2.0" /> </ItemGroup> </Project>
Create a new oneCreateLogEntriesTable
, placed in the Migrations directory
[Migration(20241216)] public class CreateLogEntriesTable : Migration { public override void Up() { ("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() { ("LogEntries"); } }
Add service registration
using ; using ; using ; namespace ; public static class CustomLoggerExtensions { /// <summary> /// Add custom log service table structure migration /// </summary> /// <param name="services"></param> /// <param name="connectionString">Database connection string</param> /// <returns></returns> public static IServiceCollection AddCustomLoggerMigration(this IServiceCollection services, string connectionString) { () .ConfigureRunner( rb => rb.AddMySql5() .WithGlobalConnectionString(connectionString) .ScanIn(typeof(CreateLogEntriesTable).Assembly) .() ) .AddLogging(lb => { (); }); using var serviceProvider = (); using var scope = (); var runner = <IMigrationRunner>(); (); return services; } }
2. Implementation of EntityFramework Core
2.1 Database context
Create a new project, add a reference to the project, and install it in the projectand
。
<Project Sdk=""> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="" Version="8.0.11" /> <PackageReference Include="" Version="8.0.2" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\\" /> </ItemGroup> </Project>
createCustomLoggerDbContext
Class, used to manage log entities
using ; using ; namespace ; public class CustomLoggerDbContext(DbContextOptions<CustomLoggerDbContext> options) : DbContext(options) { public virtual DbSet<CustomLogEntry> LogEntries { get; set; } }
useObjectPoolmanageDbContext: Improve performance and reduce the overhead of creating and destroying DbContext.
createCustomLoggerDbContextPoolPolicy
using ; using ; namespace ; /// <summary> /// DbContext Pool Policy/// </summary> /// <param name="options"></param> public class CustomLoggerDbContextPoolPolicy(DbContextOptions<CustomLoggerDbContext> options) : IPooledObjectPolicy<CustomLoggerDbContext> { /// <summary> /// Create DbContext /// </summary> /// <returns></returns> public CustomLoggerDbContext Create() { return new CustomLoggerDbContext(options); } /// <summary> /// Recycle DbContext /// </summary> /// <param name="context"></param> /// <returns></returns> public bool Return(CustomLoggerDbContext context) { // Reset DbContext status (); return true; } }
2.2 Implement log writing
Create aEfCoreCustomLogger
, inherited fromCustomLogger
, implement the specific logic of log writing
using ; using ; using ; namespace ; /// <summary> /// EfCore custom logger/// </summary> public class EfCoreCustomLogger(ObjectPool<CustomLoggerDbContext> contextPool, ILogger<EfCoreCustomLogger> logger) : CustomLogger(logger) { /// <summary> /// Query the log according to ID /// </summary> /// <param name="logId"></param> /// <returns></returns> protected override CustomLogEntry? GetById(string logId) { var dbContext = (); try { return (logId); } finally { (dbContext); } } /// <summary> /// Write to log /// </summary> /// <param name="writeCommand"></param> /// <returns></returns> /// <exception cref="ArgumentOutOfRangeException"></exception> protected override async Task WriteAsync(WriteCommand writeCommand) { var dbContext = (); try { switch () { case : if ( != null) { await (); } break; case : { if ( != null) { (); } break; } default: throw new ArgumentOutOfRangeException(); } await (); } finally { (dbContext); } } }
Add service registration
using ; using ; using ; using ; namespace ; public static class EfCoreCustomLoggerExtensions { public static IServiceCollection AddEfCoreCustomLogger(this IServiceCollection services, string connectionString) { if ((connectionString)) { throw new ArgumentNullException(nameof(connectionString)); } (connectionString); <ObjectPoolProvider, DefaultObjectPoolProvider>(); (serviceProvider => { var options = new DbContextOptionsBuilder<CustomLoggerDbContext>() .UseMySql(connectionString, (connectionString)) .Options; var poolProvider = <ObjectPoolProvider>(); return (new CustomLoggerDbContextPoolPolicy(options)); }); <ICustomLogger, EfCoreCustomLogger>(); return services; } }
3. Implementation of MySqlConnector
The implementation of MySqlConnector is relatively simple, using native SQL operation database to complete log insertion and update.
Create a new project, add a reference to the project, and install itMySqlConnector
Bag
<Project 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="..\\" /> </ItemGroup> </Project>
3.1 SQL scripts
For easy maintenance, we put the SQL scripts we need to use in oneConsts
In class
namespace ; public class Consts { /// <summary> /// Insert log /// </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> /// Update log /// </summary> public const string UpdateSql = """ UPDATE `LogEntries` SET `Status` = @Status, `UpdateTime` = @UpdateTime WHERE `Id` = @Id; """; /// <summary> /// Query the log according to ID /// </summary> public const string QueryByIdSql = """ SELECT `Id`, `TranceId`, `BizType`, `Body`, `Component`, `MsgType`, `Status`, `CreateTime`, `UpdateTime`, `Remark` FROM `LogEntries` WHERE `Id` = @Id; """; }
3.2 Implement log writing
createMySqlConnectorCustomLogger
Class, implement the specific logic of log writing
using ; using ; using MySqlConnector; namespace ; /// <summary> /// Use MySqlConnector to log/// </summary> public class MySqlConnectorCustomLogger : CustomLogger { /// <summary> /// Database connection string /// </summary> private readonly string _connectionString; /// <summary> /// Constructor /// </summary> /// <param name="connectionString">MySQL connection string</param> /// <param name="logger"></param> public MySqlConnectorCustomLogger( string connectionString, ILogger<MySqlConnectorCustomLogger> logger) : base(logger) { _connectionString = connectionString; } /// <summary> /// Query the log according to ID /// </summary> /// <param name="messageId"></param> /// <returns></returns> protected override CustomLogEntry? GetById(string messageId) { using var connection = new MySqlConnection(_connectionString); (); using var command = new MySqlCommand(, connection); ("@Id", messageId); using var reader = (); if (!()) { return null; } return new CustomLogEntry { Id = (0), Message = (1), IsSuccess = (2), CreateTime = (3), UpdateTime = (4) }; } /// <summary> /// Process logs /// </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 (); switch () { case : { if ( != null) { await using var command = new MySqlCommand(, connection); ("@Id", ); ("@Message", ); ("@IsSuccess", ); ("@CreateTime", ); ("@UpdateTime", ); await (); } break; } case : { if ( != null) { await using var command = new MySqlCommand(, connection); ("@Id", ); ("@IsSuccess", ); ("@UpdateTime", ); await (); } break; } default: throw new ArgumentOutOfRangeException(); } } }
Add service registration
using ; using ; using ; namespace ; /// <summary> /// MySqlConnector Logger Extension/// </summary> public static class MySqlConnectorCustomLoggerExtensions { /// <summary> /// Add MySqlConnector Logger /// </summary> /// <param name="services"></param> /// <param name="connectionString"></param> /// <returns></returns> public static IServiceCollection AddMySqlConnectorCustomLogger(this IServiceCollection services, string connectionString) { if ((connectionString)) { throw new ArgumentNullException(nameof(connectionString)); } <ICustomLogger>(s => { var logger = <ILogger<MySqlConnectorCustomLogger>>(); return new MySqlConnectorCustomLogger(connectionString, logger); }); (connectionString); return services; } }
4. Use examples
Below is an example of the implementation of EntityFramework Core. The MySqlConnector is used the same way.
Create a new WebApi project and add
var builder = (args); // Add services to the container. (); // Learn more about configuring Swagger/OpenAPI at /aspnetcore/swashbuckle (); (); // Add EntityFrameworkCore loggervar connectionString = ("MySql"); (connectionString!); var app = (); // Configure the HTTP request pipeline. if (()) { (); (); } (); (); ();
Use in the controller
namespace ; [ApiController] [Route("[controller]")] public class TestController(ICustomLogger customLogger) : ControllerBase { [HttpPost("InsertLog")] public IActionResult Post(CustomLogEntry model) { (model); return Ok(); } [HttpPut("UpdateLog")] public IActionResult Put(string messageId, MessageStatus status) { (messageId, status); return Ok(); } }
The above is the detailed content of .NET Core implementing a custom logger. For more information about .NET Core logging, please follow my other related articles!