VanDerHeijden.Logging / README.md
Code · 197 lines · 6470 bytes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197# 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

| Package | Description | NuGet |
|---|---|---|
| `VanDerHeijden.Logging` | Core abstractions | [![NuGet](https://img.shields.io/nuget/v/VanDerHeijden.Logging)](https://www.nuget.org/packages/VanDerHeijden.Logging) |
| `VanDerHeijden.Logging.File` | Daily rotating file writer | [![NuGet](https://img.shields.io/nuget/v/VanDerHeijden.Logging.File)](https://www.nuget.org/packages/VanDerHeijden.Logging.File) |
| `VanDerHeijden.Logging.MongoDb` | MongoDB collection writer | [![NuGet](https://img.shields.io/nuget/v/VanDerHeijden.Logging.MongoDb)](https://www.nuget.org/packages/VanDerHeijden.Logging.MongoDb) |
| `VanDerHeijden.Logging.Sql` | SQL Server writer (SqlBulkCopy) | [![NuGet](https://img.shields.io/nuget/v/VanDerHeijden.Logging.Sql)](https://www.nuget.org/packages/VanDerHeijden.Logging.Sql) |
| `VanDerHeijden.Logging.Redis` | Redis list writer (RPUSH) | [![NuGet](https://img.shields.io/nuget/v/VanDerHeijden.Logging.Redis)](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

```bash
dotnet add package VanDerHeijden.Logging.File
```

```csharp
builder.Logging.AddFileLogger(logDirectory: "Logs");
```

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

### MongoDB

```bash
dotnet add package VanDerHeijden.Logging.MongoDb
```

```csharp
var mongoClient = new MongoClient("mongodb://localhost:27017");
var collection = mongoClient
    .GetDatabase("myapp")
    .GetCollection<LogEntry>("logs");

builder.Logging.AddMongoDbLogger(collection);
```

### SQL Server

```bash
dotnet add package VanDerHeijden.Logging.Sql
```

```csharp
builder.Logging.AddSqlLogger(
    connectionString: "Server=.;Database=MyApp;Integrated Security=true;",
    tableName: "Logs");
```

Required table schema:

```sql
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

```bash
dotnet add package VanDerHeijden.Logging.Redis
```

```csharp
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:

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

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

| Field | Example |
|---|---|
| `Path` | `/api/users/login` |
| `Method` | `POST` |
| `ClientIp` | `203.0.113.42` (respects `X-Forwarded-For`) |
| `Referer` | `https://example.com` |
| `UserAgent` | `Mozilla/5.0 ...` |

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

## Configuration

`BatchedLogger<T>` accepts the following constructor parameters:

| Parameter | Default | Description |
|---|---|---|
| `batchSize` | 200 | Maximum entries per flush |
| `maxIdleMs` | 4000 | Maximum time (ms) between flushes when the batch is not full |
| `fullMode` | `Wait` | What to do when the channel is full (`Wait` or `DropOldest`) |

## Implementing a custom writer

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

```csharp
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;
}
```

```csharp
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](https://benchmarkdotnet.org/) 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.

| BatchSize | MessageLength | Mean/flush | Allocated |
|----------:|:-------------:|-----------:|----------:|
| 1         | 80 B          |    27.9 µs |     477 B |
| 10        | 256 B         |    26.6 µs |     477 B |
| 100       | 1 024 B       |   144.0 µs |     756 B |
| 500       | 1 024 B       |   704.5 µs |   2 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](https://github.com/alphons/VanDerHeijden.Logging)