added IHttpContextAccessor

alphons <alphons@heijden.com> 11 Mar 2026, 11:48
25dd149931cff96abb20c83ed1fd95cdb47ebefb
20 files changed
  • README.md
  • src/VanDerHeijden.Logging.File/FileLoggingBuilderExtensions.cs
  • src/VanDerHeijden.Logging.File/README.md
  • src/VanDerHeijden.Logging.File/VanDerHeijden.Logging.File.csproj
  • src/VanDerHeijden.Logging.MongoDb/MongoDbLogWriter.cs
  • src/VanDerHeijden.Logging.MongoDb/MongoDbLoggingBuilderExtensions.cs
  • src/VanDerHeijden.Logging.MongoDb/README.md
  • src/VanDerHeijden.Logging.MongoDb/VanDerHeijden.Logging.MongoDb.csproj
  • src/VanDerHeijden.Logging.Redis/README.md
  • src/VanDerHeijden.Logging.Redis/RedisLogEntry.cs
  • src/VanDerHeijden.Logging.Redis/RedisLoggingBuilderExtensions.cs
  • src/VanDerHeijden.Logging.Redis/VanDerHeijden.Logging.Redis.csproj
  • src/VanDerHeijden.Logging.Sql/README.md
  • src/VanDerHeijden.Logging.Sql/SqlLogEntry.cs
  • src/VanDerHeijden.Logging.Sql/SqlLogWriter.cs
  • src/VanDerHeijden.Logging.Sql/SqlLoggingBuilderExtensions.cs
  • src/VanDerHeijden.Logging.Sql/VanDerHeijden.Logging.Sql.csproj
  • src/VanDerHeijden.Logging/BatchedLogger.cs
  • src/VanDerHeijden.Logging/README.md
  • src/VanDerHeijden.Logging/VanDerHeijden.Logging.csproj
diff --git a/README.md b/README.md
index 4b588ed..77384ec 100644
--- a/README.md
+++ b/README.md
@@ -87,7 +87,12 @@ CREATE TABLE Logs (
Level NVARCHAR(20) NOT NULL,
Category NVARCHAR(256) NOT NULL,
Message NVARCHAR(MAX) NOT NULL,
- Exception NVARCHAR(MAX) 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
);
```
@@ -108,6 +113,26 @@ builder.Logging.AddRedisLogger(
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:
@@ -135,11 +160,15 @@ public sealed class MyWriter : IBatchedLogWriter<string>
```
```csharp
-builder.Logging.Services.AddSingleton<ILoggerProvider>(_ =>
+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, (msg, level) => msg);
+ return new BatchedLoggerProvider<string>(
+ logger,
+ entryFactory: (msg, level, ctx) => msg,
+ httpContextAccessor);
});
```
diff --git a/src/VanDerHeijden.Logging.File/FileLoggingBuilderExtensions.cs b/src/VanDerHeijden.Logging.File/FileLoggingBuilderExtensions.cs
index 26bf378..67110a7 100644
--- a/src/VanDerHeijden.Logging.File/FileLoggingBuilderExtensions.cs
+++ b/src/VanDerHeijden.Logging.File/FileLoggingBuilderExtensions.cs
@@ -1,3 +1,4 @@
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Threading.Channels;
@@ -22,14 +23,19 @@ public static class FileLoggingBuilderExtensions
/// <returns>The <paramref name="builder"/> so that additional calls can be chained.</returns>
public static ILoggingBuilder AddFileLogger(this ILoggingBuilder builder, string logDirectory = "Logs")
{
- builder.Services.AddSingleton<ILoggerProvider>(_ =>
+ builder.Services.AddSingleton<ILoggerProvider>(sp =>
{
+ var httpContextAccessor = sp.GetService<IHttpContextAccessor>();
var logWriter = new FileLogWriter(logDirectory);
var batchedLogger = new BatchedLogger<string>(logWriter, fullMode: BoundedChannelFullMode.Wait);
return new BatchedLoggerProvider<string>(
batchedLogger,
- entryFactory: (message, _) =>
- $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {message}{Environment.NewLine}"
+ entryFactory: (message, _, ctx) =>
+ {
+ var http = ctx is null ? "" : $" [{ctx.Method} {ctx.Path} {ctx.ClientIp}]";
+ return $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}{http} {message}{Environment.NewLine}";
+ },
+ httpContextAccessor
);
});
return builder;
diff --git a/src/VanDerHeijden.Logging.File/README.md b/src/VanDerHeijden.Logging.File/README.md
index 51cd5ad..f276972 100644
--- a/src/VanDerHeijden.Logging.File/README.md
+++ b/src/VanDerHeijden.Logging.File/README.md
@@ -29,8 +29,20 @@ Log files are written to the `Logs` directory (relative to the working directory
## Log format
+Without HTTP context:
```
-2026-02-22 14:03:12.456 [Information] MyApp.Service: User logged in
+2026-02-22 14:03:12.456 MyApp.Service: User logged in
+```
+
+With HTTP context (when `IHttpContextAccessor` is registered):
+```
+2026-02-22 14:03:12.456 [POST /api/users/login 203.0.113.42] MyApp.Service: User logged in
+```
+
+Register `IHttpContextAccessor` in `Program.cs` to enable HTTP enrichment:
+
+```csharp
+builder.Services.AddHttpContextAccessor();
```
## Performance
diff --git a/src/VanDerHeijden.Logging.File/VanDerHeijden.Logging.File.csproj b/src/VanDerHeijden.Logging.File/VanDerHeijden.Logging.File.csproj
index bd5e2c2..ee67d8f 100644
--- a/src/VanDerHeijden.Logging.File/VanDerHeijden.Logging.File.csproj
+++ b/src/VanDerHeijden.Logging.File/VanDerHeijden.Logging.File.csproj
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<PackageId>VanDerHeijden.Logging.File</PackageId>
- <Version>10.0.4</Version>
+ <Version>10.0.6</Version>
<Authors>VanDerHeijden</Authors>
<Description>File log writer for VanDerHeijden.Logging: writes batched log entries to daily rotating text files.</Description>
<PackageTags>logging;file;batched</PackageTags>
@@ -20,8 +20,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
- <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.4" />
</ItemGroup>
<ItemGroup>
diff --git a/src/VanDerHeijden.Logging.MongoDb/MongoDbLogWriter.cs b/src/VanDerHeijden.Logging.MongoDb/MongoDbLogWriter.cs
index 8305486..b347174 100644
--- a/src/VanDerHeijden.Logging.MongoDb/MongoDbLogWriter.cs
+++ b/src/VanDerHeijden.Logging.MongoDb/MongoDbLogWriter.cs
@@ -28,6 +28,21 @@ public class LogEntry
/// <summary>Gets or sets the string representation of an associated exception, or <see langword="null"/> if none.</summary>
public string? Exception { get; set; }
+
+ /// <summary>Gets or sets the request path (e.g. <c>"/api/users"</c>), or <see langword="null"/> outside an HTTP context.</summary>
+ public string? Path { get; set; }
+
+ /// <summary>Gets or sets the HTTP method (e.g. <c>"GET"</c>), or <see langword="null"/> outside an HTTP context.</summary>
+ public string? Method { get; set; }
+
+ /// <summary>Gets or sets the client IP address, or <see langword="null"/> outside an HTTP context.</summary>
+ public string? ClientIp { get; set; }
+
+ /// <summary>Gets or sets the Referer header value, or <see langword="null"/> outside an HTTP context.</summary>
+ public string? Referer { get; set; }
+
+ /// <summary>Gets or sets the User-Agent header value, or <see langword="null"/> outside an HTTP context.</summary>
+ public string? UserAgent { get; set; }
}
/// <summary>
diff --git a/src/VanDerHeijden.Logging.MongoDb/MongoDbLoggingBuilderExtensions.cs b/src/VanDerHeijden.Logging.MongoDb/MongoDbLoggingBuilderExtensions.cs
index bccb7f0..a03758c 100644
--- a/src/VanDerHeijden.Logging.MongoDb/MongoDbLoggingBuilderExtensions.cs
+++ b/src/VanDerHeijden.Logging.MongoDb/MongoDbLoggingBuilderExtensions.cs
@@ -1,3 +1,4 @@
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
@@ -18,19 +19,26 @@ public static class MongoDbLoggingBuilderExtensions
/// <returns>The <paramref name="builder"/> so that additional calls can be chained.</returns>
public static ILoggingBuilder AddMongoDbLogger(this ILoggingBuilder builder, IMongoCollection<LogEntry> collection)
{
- builder.Services.AddSingleton<ILoggerProvider>(_ =>
+ builder.Services.AddSingleton<ILoggerProvider>(sp =>
{
+ var httpContextAccessor = sp.GetService<IHttpContextAccessor>();
var logWriter = new MongoDbLogWriter(collection);
var batchedLogger = new BatchedLogger<LogEntry>(logWriter, batchSize: 100, maxIdleMs: 3000, fullMode: BoundedChannelFullMode.DropOldest);
return new BatchedLoggerProvider<LogEntry>(
batchedLogger,
- entryFactory: (message, logLevel) => new LogEntry
+ entryFactory: (message, logLevel, ctx) => new LogEntry
{
Timestamp = DateTime.UtcNow,
- Level = logLevel.ToString(),
- Category = message.Split(':')[0],
- Message = message
- }
+ Level = logLevel.ToString(),
+ Category = message.Split(':')[0],
+ Message = message,
+ Path = ctx?.Path,
+ Method = ctx?.Method,
+ ClientIp = ctx?.ClientIp,
+ Referer = ctx?.Referer,
+ UserAgent = ctx?.UserAgent
+ },
+ httpContextAccessor
);
});
return builder;
diff --git a/src/VanDerHeijden.Logging.MongoDb/README.md b/src/VanDerHeijden.Logging.MongoDb/README.md
index 6664ddb..041ad92 100644
--- a/src/VanDerHeijden.Logging.MongoDb/README.md
+++ b/src/VanDerHeijden.Logging.MongoDb/README.md
@@ -26,15 +26,28 @@ builder.Logging.AddMongoDbLogger(collection);
```csharp
public class LogEntry
{
- public string Id { get; set; } // ObjectId
+ public string Id { get; set; } // ObjectId
public DateTime Timestamp { get; set; }
- public string Level { get; set; }
- public string Category { get; set; }
- public string Message { get; set; }
- public string? Exception { get; set; }
+ public string Level { get; set; }
+ public string Category { get; set; }
+ public string Message { get; set; }
+ public string? Exception { get; set; }
+ public string? Path { get; set; }
+ public string? Method { get; set; }
+ public string? ClientIp { get; set; }
+ public string? Referer { get; set; }
+ public string? UserAgent { get; set; }
}
```
+The HTTP fields are populated automatically when `IHttpContextAccessor` is registered:
+
+```csharp
+builder.Services.AddHttpContextAccessor();
+```
+
+Outside an HTTP context they are `null` and not stored in the document.
+
## Repository
[https://github.com/alphons/VanDerHeijden.Logging](https://github.com/alphons/VanDerHeijden.Logging)
diff --git a/src/VanDerHeijden.Logging.MongoDb/VanDerHeijden.Logging.MongoDb.csproj b/src/VanDerHeijden.Logging.MongoDb/VanDerHeijden.Logging.MongoDb.csproj
index 6aec6c7..f6e2f12 100644
--- a/src/VanDerHeijden.Logging.MongoDb/VanDerHeijden.Logging.MongoDb.csproj
+++ b/src/VanDerHeijden.Logging.MongoDb/VanDerHeijden.Logging.MongoDb.csproj
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<PackageId>VanDerHeijden.Logging.MongoDb</PackageId>
- <Version>10.0.4</Version>
+ <Version>10.0.6</Version>
<Authors>VanDerHeijden</Authors>
<Description>MongoDB log writer for VanDerHeijden.Logging: writes batched log entries to a MongoDB collection.</Description>
<PackageTags>logging;mongodb;batched</PackageTags>
@@ -20,9 +20,9 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
- <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
- <PackageReference Include="MongoDB.Driver" Version="3.6.0" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.4" />
+ <PackageReference Include="MongoDB.Driver" Version="3.7.0" />
</ItemGroup>
<ItemGroup>
diff --git a/src/VanDerHeijden.Logging.Redis/README.md b/src/VanDerHeijden.Logging.Redis/README.md
index 54fb302..d530237 100644
--- a/src/VanDerHeijden.Logging.Redis/README.md
+++ b/src/VanDerHeijden.Logging.Redis/README.md
@@ -30,10 +30,23 @@ builder.Logging.AddRedisLogger(
"level": "Information",
"category": "MyApp.Service",
"message": "MyApp.Service: User logged in",
- "exception": null
+ "exception": null,
+ "path": "/api/users/login",
+ "method": "POST",
+ "clientIp": "203.0.113.42",
+ "referer": "https://example.com/login",
+ "userAgent": "Mozilla/5.0 ..."
}
```
+The HTTP fields are populated automatically when `IHttpContextAccessor` is registered:
+
+```csharp
+builder.Services.AddHttpContextAccessor();
+```
+
+Outside an HTTP context they are `null` and omitted from the JSON output.
+
## Notes
- The Redis list grows until consumed. Make sure a consumer drains it via `BLPOP`/`LPOP`.
diff --git a/src/VanDerHeijden.Logging.Redis/RedisLogEntry.cs b/src/VanDerHeijden.Logging.Redis/RedisLogEntry.cs
index 63729b3..8aa7ace 100644
--- a/src/VanDerHeijden.Logging.Redis/RedisLogEntry.cs
+++ b/src/VanDerHeijden.Logging.Redis/RedisLogEntry.cs
@@ -19,4 +19,19 @@ public class RedisLogEntry
/// <summary>Gets or sets the string representation of an associated exception, or <see langword="null"/> if none.</summary>
public string? Exception { get; set; }
+
+ /// <summary>Gets or sets the request path (e.g. <c>"/api/users"</c>), or <see langword="null"/> outside an HTTP context.</summary>
+ public string? Path { get; set; }
+
+ /// <summary>Gets or sets the HTTP method (e.g. <c>"GET"</c>), or <see langword="null"/> outside an HTTP context.</summary>
+ public string? Method { get; set; }
+
+ /// <summary>Gets or sets the client IP address, or <see langword="null"/> outside an HTTP context.</summary>
+ public string? ClientIp { get; set; }
+
+ /// <summary>Gets or sets the Referer header value, or <see langword="null"/> outside an HTTP context.</summary>
+ public string? Referer { get; set; }
+
+ /// <summary>Gets or sets the User-Agent header value, or <see langword="null"/> outside an HTTP context.</summary>
+ public string? UserAgent { get; set; }
}
diff --git a/src/VanDerHeijden.Logging.Redis/RedisLoggingBuilderExtensions.cs b/src/VanDerHeijden.Logging.Redis/RedisLoggingBuilderExtensions.cs
index d01744f..8574e80 100644
--- a/src/VanDerHeijden.Logging.Redis/RedisLoggingBuilderExtensions.cs
+++ b/src/VanDerHeijden.Logging.Redis/RedisLoggingBuilderExtensions.cs
@@ -1,3 +1,4 @@
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
@@ -27,19 +28,26 @@ public static class RedisLoggingBuilderExtensions
string listKey = "logs",
TimeSpan? ttl = null)
{
- builder.Services.AddSingleton<ILoggerProvider>(_ =>
+ builder.Services.AddSingleton<ILoggerProvider>(sp =>
{
+ var httpContextAccessor = sp.GetService<IHttpContextAccessor>();
var logWriter = new RedisLogWriter(database, listKey, ttl);
var batchedLogger = new BatchedLogger<RedisLogEntry>(logWriter, batchSize: 200, maxIdleMs: 2000, fullMode: BoundedChannelFullMode.DropOldest);
return new BatchedLoggerProvider<RedisLogEntry>(
batchedLogger,
- entryFactory: (message, logLevel) => new RedisLogEntry
+ entryFactory: (message, logLevel, ctx) => new RedisLogEntry
{
Timestamp = DateTime.UtcNow,
Level = logLevel.ToString(),
Category = message.Split(':')[0],
- Message = message
- }
+ Message = message,
+ Path = ctx?.Path,
+ Method = ctx?.Method,
+ ClientIp = ctx?.ClientIp,
+ Referer = ctx?.Referer,
+ UserAgent = ctx?.UserAgent
+ },
+ httpContextAccessor
);
});
return builder;
diff --git a/src/VanDerHeijden.Logging.Redis/VanDerHeijden.Logging.Redis.csproj b/src/VanDerHeijden.Logging.Redis/VanDerHeijden.Logging.Redis.csproj
index 43d7c31..0af851a 100644
--- a/src/VanDerHeijden.Logging.Redis/VanDerHeijden.Logging.Redis.csproj
+++ b/src/VanDerHeijden.Logging.Redis/VanDerHeijden.Logging.Redis.csproj
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<PackageId>VanDerHeijden.Logging.Redis</PackageId>
- <Version>10.0.4</Version>
+ <Version>10.0.6</Version>
<Authors>VanDerHeijden</Authors>
<Description>Redis log writer for VanDerHeijden.Logging: writes batched log entries to a Redis list using RPUSH.</Description>
<PackageTags>logging;redis;batched</PackageTags>
@@ -20,9 +20,9 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
- <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
- <PackageReference Include="StackExchange.Redis" Version="2.11.3" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.4" />
+ <PackageReference Include="StackExchange.Redis" Version="2.11.8" />
</ItemGroup>
<ItemGroup>
diff --git a/src/VanDerHeijden.Logging.Sql/README.md b/src/VanDerHeijden.Logging.Sql/README.md
index fce7b12..df5322c 100644
--- a/src/VanDerHeijden.Logging.Sql/README.md
+++ b/src/VanDerHeijden.Logging.Sql/README.md
@@ -27,10 +27,35 @@ CREATE TABLE Logs (
Level NVARCHAR(20) NOT NULL,
Category NVARCHAR(256) NOT NULL,
Message NVARCHAR(MAX) NOT NULL,
- Exception NVARCHAR(MAX) 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
);
```
+The HTTP columns (`Path`, `Method`, `ClientIp`, `Referer`, `UserAgent`) are populated automatically when
+`IHttpContextAccessor` is registered in the DI container:
+
+```csharp
+builder.Services.AddHttpContextAccessor();
+```
+
+Outside an HTTP context (background services, console apps) these columns are `NULL`.
+
+### Migrating an existing table
+
+```sql
+ALTER TABLE Logs
+ ADD Path NVARCHAR(1024) NULL,
+ Method NVARCHAR(10) NULL,
+ ClientIp NVARCHAR(45) NULL,
+ Referer NVARCHAR(2048) NULL,
+ UserAgent NVARCHAR(512) NULL;
+```
+
## Repository
[https://github.com/alphons/VanDerHeijden.Logging](https://github.com/alphons/VanDerHeijden.Logging)
diff --git a/src/VanDerHeijden.Logging.Sql/SqlLogEntry.cs b/src/VanDerHeijden.Logging.Sql/SqlLogEntry.cs
index b9bb126..502f394 100644
--- a/src/VanDerHeijden.Logging.Sql/SqlLogEntry.cs
+++ b/src/VanDerHeijden.Logging.Sql/SqlLogEntry.cs
@@ -19,4 +19,19 @@ public class SqlLogEntry
/// <summary>Gets or sets the string representation of an associated exception, or <see langword="null"/> if none.</summary>
public string? Exception { get; set; }
+
+ /// <summary>Gets or sets the request path (e.g. <c>"/api/users"</c>), or <see langword="null"/> outside an HTTP context.</summary>
+ public string? Path { get; set; }
+
+ /// <summary>Gets or sets the HTTP method (e.g. <c>"GET"</c>), or <see langword="null"/> outside an HTTP context.</summary>
+ public string? Method { get; set; }
+
+ /// <summary>Gets or sets the client IP address, or <see langword="null"/> outside an HTTP context.</summary>
+ public string? ClientIp { get; set; }
+
+ /// <summary>Gets or sets the Referer header value, or <see langword="null"/> outside an HTTP context.</summary>
+ public string? Referer { get; set; }
+
+ /// <summary>Gets or sets the User-Agent header value, or <see langword="null"/> outside an HTTP context.</summary>
+ public string? UserAgent { get; set; }
}
diff --git a/src/VanDerHeijden.Logging.Sql/SqlLogWriter.cs b/src/VanDerHeijden.Logging.Sql/SqlLogWriter.cs
index 0144143..4ea777e 100644
--- a/src/VanDerHeijden.Logging.Sql/SqlLogWriter.cs
+++ b/src/VanDerHeijden.Logging.Sql/SqlLogWriter.cs
@@ -11,7 +11,12 @@ namespace VanDerHeijden.Logging.Sql;
/// Level NVARCHAR(20) NOT NULL,
/// Category NVARCHAR(256) NOT NULL,
/// Message NVARCHAR(MAX) NOT NULL,
-/// Exception NVARCHAR(MAX) 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
/// );
/// </summary>
public sealed class SqlLogWriter(string connectionString, string tableName = "Logs") : IBatchedLogWriter<SqlLogEntry>
@@ -37,6 +42,11 @@ public sealed class SqlLogWriter(string connectionString, string tableName = "Lo
bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.Category), "Category");
bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.Message), "Message");
bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.Exception), "Exception");
+ bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.Path), "Path");
+ bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.Method), "Method");
+ bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.ClientIp), "ClientIp");
+ bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.Referer), "Referer");
+ bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.UserAgent), "UserAgent");
var table = ToDataTable(entries);
await bulkCopy.WriteToServerAsync(table, ct);
@@ -53,9 +63,21 @@ public sealed class SqlLogWriter(string connectionString, string tableName = "Lo
table.Columns.Add("Category", typeof(string));
table.Columns.Add("Message", typeof(string));
table.Columns.Add("Exception", typeof(string));
+ table.Columns.Add("Path", typeof(string));
+ table.Columns.Add("Method", typeof(string));
+ table.Columns.Add("ClientIp", typeof(string));
+ table.Columns.Add("Referer", typeof(string));
+ table.Columns.Add("UserAgent", typeof(string));
foreach (var e in entries)
- table.Rows.Add(e.Timestamp, e.Level, e.Category, e.Message, (object?)e.Exception ?? DBNull.Value);
+ table.Rows.Add(
+ e.Timestamp, e.Level, e.Category, e.Message,
+ (object?)e.Exception ?? DBNull.Value,
+ (object?)e.Path ?? DBNull.Value,
+ (object?)e.Method ?? DBNull.Value,
+ (object?)e.ClientIp ?? DBNull.Value,
+ (object?)e.Referer ?? DBNull.Value,
+ (object?)e.UserAgent ?? DBNull.Value);
return table;
}
diff --git a/src/VanDerHeijden.Logging.Sql/SqlLoggingBuilderExtensions.cs b/src/VanDerHeijden.Logging.Sql/SqlLoggingBuilderExtensions.cs
index d3e6889..29b52a2 100644
--- a/src/VanDerHeijden.Logging.Sql/SqlLoggingBuilderExtensions.cs
+++ b/src/VanDerHeijden.Logging.Sql/SqlLoggingBuilderExtensions.cs
@@ -1,3 +1,4 @@
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Threading.Channels;
@@ -24,19 +25,26 @@ public static class SqlLoggingBuilderExtensions
string connectionString,
string tableName = "Logs")
{
- builder.Services.AddSingleton<ILoggerProvider>(_ =>
+ builder.Services.AddSingleton<ILoggerProvider>(sp =>
{
+ var httpContextAccessor = sp.GetService<IHttpContextAccessor>();
var logWriter = new SqlLogWriter(connectionString, tableName);
var batchedLogger = new BatchedLogger<SqlLogEntry>(logWriter, batchSize: 200, maxIdleMs: 4000, fullMode: BoundedChannelFullMode.Wait);
return new BatchedLoggerProvider<SqlLogEntry>(
batchedLogger,
- entryFactory: (message, logLevel) => new SqlLogEntry
+ entryFactory: (message, logLevel, ctx) => new SqlLogEntry
{
Timestamp = DateTime.UtcNow,
Level = logLevel.ToString(),
Category = message.Split(':')[0],
- Message = message
- }
+ Message = message,
+ Path = ctx?.Path,
+ Method = ctx?.Method,
+ ClientIp = ctx?.ClientIp,
+ Referer = ctx?.Referer,
+ UserAgent = ctx?.UserAgent
+ },
+ httpContextAccessor
);
});
return builder;
diff --git a/src/VanDerHeijden.Logging.Sql/VanDerHeijden.Logging.Sql.csproj b/src/VanDerHeijden.Logging.Sql/VanDerHeijden.Logging.Sql.csproj
index 1762cca..1c07e52 100644
--- a/src/VanDerHeijden.Logging.Sql/VanDerHeijden.Logging.Sql.csproj
+++ b/src/VanDerHeijden.Logging.Sql/VanDerHeijden.Logging.Sql.csproj
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<PackageId>VanDerHeijden.Logging.Sql</PackageId>
- <Version>10.0.4</Version>
+ <Version>10.0.6</Version>
<Authors>VanDerHeijden</Authors>
<Description>SQL Server log writer for VanDerHeijden.Logging: writes batched log entries to a SQL Server table using SqlBulkCopy.</Description>
<PackageTags>logging;sql;sqlserver;batched</PackageTags>
@@ -21,8 +21,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
- <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.4" />
</ItemGroup>
<ItemGroup>
diff --git a/src/VanDerHeijden.Logging/BatchedLogger.cs b/src/VanDerHeijden.Logging/BatchedLogger.cs
index c4362d3..70d67dc 100644
--- a/src/VanDerHeijden.Logging/BatchedLogger.cs
+++ b/src/VanDerHeijden.Logging/BatchedLogger.cs
@@ -1,8 +1,21 @@
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Threading.Channels;
namespace VanDerHeijden.Logging;
+/// <summary>
+/// HTTP context properties captured at the moment a log entry is created.
+/// All fields are <see langword="null"/> when no HTTP context is active.
+/// </summary>
+public sealed record HttpLogContext(
+ string? Path,
+ string? Method,
+ string? ClientIp,
+ string? Referer,
+ string? UserAgent,
+ string? SessionId);
+
/// <summary>
/// Defines a writer that receives a batch of log entries and persists them to a backing store.
/// </summary>
@@ -148,9 +161,17 @@ public sealed class BatchedLogger<T> : IDisposable
/// <typeparam name="T">The type of log entry produced by <paramref name="entryFactory"/>.</typeparam>
/// <param name="batchedLogger">The shared batched logger used by all created loggers.</param>
/// <param name="entryFactory">
-/// A factory that converts a formatted message string and <see cref="LogLevel"/> into a <typeparamref name="T"/> entry.
+/// A factory that converts a formatted message, <see cref="LogLevel"/>, and optional <see cref="HttpLogContext"/>
+/// into a <typeparamref name="T"/> entry.
+/// </param>
+/// <param name="httpContextAccessor">
+/// Optional <see cref="IHttpContextAccessor"/> used to enrich log entries with request metadata.
+/// When <see langword="null"/>, HTTP properties are omitted.
/// </param>
-public sealed class BatchedLoggerProvider<T>(BatchedLogger<T> batchedLogger, Func<string, LogLevel, T> entryFactory) : ILoggerProvider
+public sealed class BatchedLoggerProvider<T>(
+ BatchedLogger<T> batchedLogger,
+ Func<string, LogLevel, HttpLogContext?, T> entryFactory,
+ IHttpContextAccessor? httpContextAccessor = null) : ILoggerProvider
{
/// <summary>
/// Creates an <see cref="ILogger"/> for the given category name.
@@ -158,7 +179,7 @@ public sealed class BatchedLoggerProvider<T>(BatchedLogger<T> batchedLogger, Fun
/// <param name="categoryName">The category name for messages produced by the logger.</param>
/// <returns>An <see cref="ILogger"/> instance.</returns>
public ILogger CreateLogger(string categoryName) =>
- new BatchedCategoryLogger<T>(batchedLogger, categoryName, entryFactory);
+ new BatchedCategoryLogger<T>(batchedLogger, categoryName, entryFactory, httpContextAccessor);
/// <summary>
/// Disposes the underlying <see cref="BatchedLogger{T}"/>, flushing any remaining entries.
@@ -166,7 +187,11 @@ public sealed class BatchedLoggerProvider<T>(BatchedLogger<T> batchedLogger, Fun
public void Dispose() => batchedLogger.Dispose();
}
-internal sealed class BatchedCategoryLogger<T>(BatchedLogger<T> batchedLogger, string categoryName, Func<string, LogLevel, T> entryFactory) : ILogger
+internal sealed class BatchedCategoryLogger<T>(
+ BatchedLogger<T> batchedLogger,
+ string categoryName,
+ Func<string, LogLevel, HttpLogContext?, T> entryFactory,
+ IHttpContextAccessor? httpContextAccessor) : ILogger
{
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
@@ -174,6 +199,29 @@ internal sealed class BatchedCategoryLogger<T>(BatchedLogger<T> batchedLogger, s
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel)) return;
- batchedLogger.Write(entryFactory($"{categoryName}: {formatter(state, exception)}{(exception != null ? $"{Environment.NewLine}{exception}" : "")}", logLevel));
+ var message = $"{categoryName}: {formatter(state, exception)}{(exception != null ? $"{Environment.NewLine}{exception}" : "")}";
+ batchedLogger.Write(entryFactory(message, logLevel, BuildHttpContext()));
+ }
+
+ private HttpLogContext? BuildHttpContext()
+ {
+ if (httpContextAccessor?.HttpContext is not { } ctx) return null;
+
+ string? ip = ctx.Connection.RemoteIpAddress?.ToString();
+ string? forwarded = ctx.Request.Headers["X-Forwarded-For"].FirstOrDefault();
+ if (!string.IsNullOrEmpty(forwarded))
+ ip = forwarded.Split(',')[0].Trim();
+
+ string? sessionId = null;
+ try { sessionId = ctx.Session?.Id; } catch (InvalidOperationException) { }
+
+ return new HttpLogContext(
+ ctx.Request.Path.ToString(),
+ ctx.Request.Method,
+ ip ?? "Unknown",
+ ctx.Request.Headers["Referer"].ToString(),
+ ctx.Request.Headers["UserAgent"].ToString(),
+ sessionId ?? string.Empty
+ );
}
}
diff --git a/src/VanDerHeijden.Logging/README.md b/src/VanDerHeijden.Logging/README.md
index 9877479..b1580b1 100644
--- a/src/VanDerHeijden.Logging/README.md
+++ b/src/VanDerHeijden.Logging/README.md
@@ -34,14 +34,22 @@ public sealed class MyWriter : IBatchedLogWriter<string>
Register it:
```csharp
-builder.Logging.Services.AddSingleton<ILoggerProvider>(_ =>
+builder.Logging.Services.AddSingleton<ILoggerProvider>(sp =>
{
+ var httpContextAccessor = sp.GetService<IHttpContextAccessor>(); // optional
var writer = new MyWriter();
var logger = new BatchedLogger<string>(writer, batchSize: 200, maxIdleMs: 4000);
- return new BatchedLoggerProvider<string>(logger, entryFactory: (msg, level) => msg);
+ return new BatchedLoggerProvider<string>(
+ logger,
+ entryFactory: (msg, level, ctx) => msg,
+ httpContextAccessor);
});
```
+The `entryFactory` receives an optional `HttpLogContext` with request metadata (`Path`, `Method`, `ClientIp`,
+`Referer`, `UserAgent`, `SessionId`). It is `null` when no HTTP context is active or when
+`IHttpContextAccessor` is not registered.
+
## Repository
[https://github.com/alphons/VanDerHeijden.Logging](https://github.com/alphons/VanDerHeijden.Logging)
diff --git a/src/VanDerHeijden.Logging/VanDerHeijden.Logging.csproj b/src/VanDerHeijden.Logging/VanDerHeijden.Logging.csproj
index fda551c..e0e0f1f 100644
--- a/src/VanDerHeijden.Logging/VanDerHeijden.Logging.csproj
+++ b/src/VanDerHeijden.Logging/VanDerHeijden.Logging.csproj
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<PackageId>VanDerHeijden.Logging</PackageId>
- <Version>10.0.5</Version>
+ <Version>10.0.6</Version>
<Authors>VanDerHeijden</Authors>
<Description>Core batched logging abstractions for .NET: IBatchedLogWriter&lt;T&gt;, BatchedLogger&lt;T&gt; and BatchedLoggerProvider&lt;T&gt;.</Description>
<PackageTags>logging;batched;core</PackageTags>
@@ -20,14 +20,14 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.3" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3" />
- <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
- <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.3" />
+ <PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.4" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.4" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.4" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.9" />
- <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
- <PackageReference Include="Microsoft.Net.Http.Headers" Version="10.0.3" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.4" />
+ <PackageReference Include="Microsoft.Net.Http.Headers" Version="10.0.4" />
</ItemGroup>