SoFunction
Updated on 2025-03-04

.NET Core implements a custom logger

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 andSerilogNLogWhat'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 implementsICustomLoggerInterface and provides basic functions of logging, log writing operations (insert or updates) are processed asynchronously in a local queue. useConcurrentQueueto 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
}

CustomLoggeraccomplish

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
{
    /// &lt;summary&gt;
    /// Add custom log service table structure migration    /// &lt;/summary&gt;
    /// &lt;param name="services"&gt;&lt;/param&gt;
    /// <param name="connectionString">Database connection string</param>    /// &lt;returns&gt;&lt;/returns&gt;
    public static IServiceCollection AddCustomLoggerMigration(this IServiceCollection services, string connectionString)
    {
        ()
            .ConfigureRunner(
                rb =&gt; rb.AddMySql5()
                    .WithGlobalConnectionString(connectionString)
                    .ScanIn(typeof(CreateLogEntriesTable).Assembly)
                    .()
            )
            .AddLogging(lb =&gt;
            {
                ();
            });

        using var serviceProvider = ();
        using var scope = ();
        var runner = &lt;IMigrationRunner&gt;();
        ();

        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>

createCustomLoggerDbContextClass, 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 ;

/// &lt;summary&gt;
/// DbContext Pool Policy/// &lt;/summary&gt;
/// &lt;param name="options"&gt;&lt;/param&gt;
public class CustomLoggerDbContextPoolPolicy(DbContextOptions&lt;CustomLoggerDbContext&gt; options) : IPooledObjectPolicy&lt;CustomLoggerDbContext&gt;
{
    /// &lt;summary&gt;
    /// Create DbContext    /// &lt;/summary&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    public CustomLoggerDbContext Create()
    {
        return new CustomLoggerDbContext(options);
    }

    /// &lt;summary&gt;
    /// Recycle DbContext    /// &lt;/summary&gt;
    /// &lt;param name="context"&gt;&lt;/param&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    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 ;

/// &lt;summary&gt;
/// EfCore custom logger/// &lt;/summary&gt;
public class EfCoreCustomLogger(ObjectPool&lt;CustomLoggerDbContext&gt; contextPool, ILogger&lt;EfCoreCustomLogger&gt; logger) : CustomLogger(logger)
{
    /// &lt;summary&gt;
    /// Query the log according to ID    /// &lt;/summary&gt;
    /// &lt;param name="logId"&gt;&lt;/param&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    protected override CustomLogEntry? GetById(string logId)
    {
        var dbContext = ();
        try
        {
            return (logId);
        }
        finally
        {
            (dbContext);
        }
    }

    /// &lt;summary&gt;
    /// Write to log    /// &lt;/summary&gt;
    /// &lt;param name="writeCommand"&gt;&lt;/param&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    /// &lt;exception cref="ArgumentOutOfRangeException"&gt;&lt;/exception&gt;
    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 itMySqlConnectorBag

<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 oneConstsIn class

namespace ;

public class Consts
{
    /// &lt;summary&gt;
    /// Insert log    /// &lt;/summary&gt;
    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);
                                    """;

    /// &lt;summary&gt;
    /// Update log    /// &lt;/summary&gt;
    public const string UpdateSql = """
                                    UPDATE `LogEntries` SET `Status` = @Status, `UpdateTime` = @UpdateTime
                                    WHERE `Id` = @Id;
                                    """;

    /// &lt;summary&gt;
    /// Query the log according to ID    /// &lt;/summary&gt;
    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

createMySqlConnectorCustomLoggerClass, implement the specific logic of log writing

using ;
using ;
using MySqlConnector;

namespace ;

/// &lt;summary&gt;
/// Use MySqlConnector to log/// &lt;/summary&gt;
public class MySqlConnectorCustomLogger : CustomLogger
{

    /// &lt;summary&gt;
    /// Database connection string    /// &lt;/summary&gt;
    private readonly string _connectionString;

    /// &lt;summary&gt;
    /// Constructor    /// &lt;/summary&gt;
    /// <param name="connectionString">MySQL connection string</param>    /// &lt;param name="logger"&gt;&lt;/param&gt;
    public MySqlConnectorCustomLogger(
        string connectionString, 
        ILogger&lt;MySqlConnectorCustomLogger&gt; logger)
        : base(logger)
    {
        _connectionString = connectionString;
    }

    /// &lt;summary&gt; 
    /// Query the log according to ID    /// &lt;/summary&gt;
    /// &lt;param name="messageId"&gt;&lt;/param&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    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)
        };
    }

    /// &lt;summary&gt;
    /// Process logs    /// &lt;/summary&gt;
    /// &lt;param name="writeCommand"&gt;&lt;/param&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    /// &lt;exception cref="ArgumentOutOfRangeException"&gt;&lt;/exception&gt;
    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 ;

/// &lt;summary&gt;
/// MySqlConnector Logger Extension/// &lt;/summary&gt;
public static class MySqlConnectorCustomLoggerExtensions
{
    /// &lt;summary&gt;
    /// Add MySqlConnector Logger    /// &lt;/summary&gt;
    /// &lt;param name="services"&gt;&lt;/param&gt;
    /// &lt;param name="connectionString"&gt;&lt;/param&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    public static IServiceCollection AddMySqlConnectorCustomLogger(this IServiceCollection services, string connectionString)
    {
        if ((connectionString))
        {
            throw new ArgumentNullException(nameof(connectionString));
        }

        &lt;ICustomLogger&gt;(s =&gt;
        {
            var logger = &lt;ILogger&lt;MySqlConnectorCustomLogger&gt;&gt;();
            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!