alphons / VanDerHeijden.Logging Public
ZIP
README

VanDerHeijden.Logging

High-performance, low-allocation batched logging for .NET 10, built on top of Microsoft.Extensions.Logging.

Log entries are written to an in-memory Channel<T> and flushed to the target in configurable batches, keeping the hot path (your application code) completely free of I/O.

Packages

PackageDescriptionNuGet
VanDerHeijden.LoggingCore abstractions![NuGet](https://www.nuget.org/packages/VanDerHeijden.Logging)
VanDerHeijden.Logging.FileDaily rotating file writer![NuGet](https://www.nuget.org/packages/VanDerHeijden.Logging.File)
VanDerHeijden.Logging.MongoDbMongoDB collection writer![NuGet](https://www.nuget.org/packages/VanDerHeijden.Logging.MongoDb)
VanDerHeijden.Logging.SqlSQL Server writer (SqlBulkCopy)![NuGet](https://www.nuget.org/packages/VanDerHeijden.Logging.Sql)
VanDerHeijden.Logging.RedisRedis list writer (RPUSH)![NuGet](https://www.nuget.org/packages/VanDerHeijden.Logging.Redis)

Architecture

Your application
      │
      ▼  logger.LogInformation(...)   [synchronous, no I/O]
 BatchedCategoryLogger<T>
      │
      ▼  channel.Writer.TryWrite(entry)
 Channel<T>  (bounded, in-memory)
      │
      ▼  background consumer task
 BatchedLogger<T>
      │  accumulates up to batchSize entries or maxIdleMs timeout
      ▼
 IBatchedLogWriter<T>.WriteBatchAsync(...)
      │
      ▼
 FileLogWriter / MongoDbLogWriter / SqlLogWriter / RedisLogWriter

Quick start

Install only the writer you need and register it in Program.cs. Each writer is independent — you can combine multiple writers simultaneously.

File

dotnet add package VanDerHeijden.Logging.File
builder.Logging.AddFileLogger(logDirectory: "Logs");

Writes daily rotating files to the Logs directory as log-yyyyMMdd.txt.

MongoDB

dotnet add package VanDerHeijden.Logging.MongoDb
var mongoClient = new MongoClient("mongodb://localhost:27017");
var collection = mongoClient
    .GetDatabase("myapp")
    .GetCollection<LogEntry>("logs");

builder.Logging.AddMongoDbLogger(collection);

SQL Server

dotnet add package VanDerHeijden.Logging.Sql
builder.Logging.AddSqlLogger(
    connectionString: "Server=.;Database=MyApp;Integrated Security=true;",
    tableName: "Logs");

Required table schema:

CREATE TABLE Logs (
    Id        BIGINT IDENTITY PRIMARY KEY,
    Timestamp DATETIME2       NOT NULL,
    Level     NVARCHAR(20)    NOT NULL,
    Category  NVARCHAR(256)   NOT NULL,
    Message   NVARCHAR(MAX)   NOT NULL,
    Exception NVARCHAR(MAX)   NULL,
    Path      NVARCHAR(1024)  NULL,
    Method    NVARCHAR(10)    NULL,
    ClientIp  NVARCHAR(45)    NULL,
    Referer   NVARCHAR(2048)  NULL,
    UserAgent NVARCHAR(512)   NULL
);

Redis

dotnet add package VanDerHeijden.Logging.Redis
var redis = await ConnectionMultiplexer.ConnectAsync("localhost:6379");

builder.Logging.AddRedisLogger(
    database: redis.GetDatabase(),
    listKey: "logs",
    ttl: TimeSpan.FromDays(7));   // optional: auto-expire the key

Entries are pushed to a Redis list as JSON via RPUSH and can be consumed by any Redis-compatible consumer (Logstash, a worker service, etc.) via BLPOP.

HTTP context enrichment

All writers automatically capture request metadata when IHttpContextAccessor is available:

builder.Services.AddHttpContextAccessor(); // enable once in Program.cs

The following fields are added to each log entry when an HTTP request is active:

FieldExample
Path/api/users/login
MethodPOST
ClientIp203.0.113.42 (respects X-Forwarded-For)
Refererhttps://example.com
UserAgentMozilla/5.0 ...

Outside an HTTP context (background services, hosted workers) all HTTP fields are null / omitted.

Configuration

BatchedLogger<T> accepts the following constructor parameters:

ParameterDefaultDescription
batchSize200Maximum entries per flush
maxIdleMs4000Maximum time (ms) between flushes when the batch is not full
fullModeWaitWhat to do when the channel is full (Wait or DropOldest)

Implementing a custom writer

Implement IBatchedLogWriter<T> and register it using BatchedLoggerProvider<T>:

public sealed class MyWriter : IBatchedLogWriter<string>
{
    public async Task WriteBatchAsync(List<string> entries, CancellationToken ct)
    {
        // write entries to your target
    }

    public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
builder.Logging.Services.AddSingleton<ILoggerProvider>(sp =>
{
    var httpContextAccessor = sp.GetService<IHttpContextAccessor>(); // optional
    var writer = new MyWriter();
    var logger = new BatchedLogger<string>(writer);
    return new BatchedLoggerProvider<string>(
        logger,
        entryFactory: (msg, level, ctx) => msg,
        httpContextAccessor);
});

Performance

Benchmarked with BenchmarkDotNet on .NET 10.0.3 (X64 RyuJIT AVX-512), Windows 11. Each figure is the mean time per WriteBatchAsync call, averaged over 2 000 consecutive calls.

BatchSizeMessageLengthMean/flushAllocated
180 B27.9 µs477 B
10256 B26.6 µs477 B
1001 024 B144.0 µs756 B
5001 024 B704.5 µs2 436 B

Allocation is flat (~477 B) for all batches up to 100 messages regardless of message length — zero GC pressure in typical use. The Write() call itself is non-blocking and allocates nothing beyond the log entry.

Hardware: Intel Core i5-1035G1 1.00 GHz · Full results in [VanDerHeijden.Logging.File](src/VanDerHeijden.Logging.File/README.md#performance).

License

MIT

Repository

https://github.com/alphons/VanDerHeijden.Logging