Architecture Note: Minimal Request‑Logging Middleware for ASP.NET Core 6+
Define a lightweight logging middleware that captures timestamp, method, path and status without blocking the pipeline, respects trust boundaries, and can be toggled via configuration.
20 Aug 2026, 16:20 UTC

Requirements
The goal is to add request‑logging to an ASP.NET Core Web API that:
- Writes a structured log entry for every HTTP request containing timestamp, HTTP method, request path, and response status code.
- Does not block the request pipeline or add noticeable latency.
- Respects trust boundaries by never logging potentially sensitive data such as the Authorization header, cookies, or request body.
- Can be enabled or disabled, and its log level adjusted, through standard configuration (
appsettings.json). - Continues to serve the request even if the underlying logger fails (e.g., disk full).
Minimal Suitable Design
The smallest design that satisfies the requirements is a single middleware class that:
- Receives
HttpContextand aRequestDelegaterepresenting the next component. - Records the start timestamp.
- Invokes the delegate to let the rest of the pipeline run.
- After the delegate completes, reads the response status code.
- Calls
ILogger<RequestLoggingMiddleware>.LogInformation(or another level) with a message template that includes the captured fields. - Wraps the logger call in a
try/catchso that any logging exception is swallowed and the request proceeds.
Placing the middleware after routing (app.UseRouting()) but before endpoint execution (app.UseEndpoints or app.MapControllers) ensures that:
- All requests that reach the application (including those that will later return 404) are logged.
- Static‑file requests handled by
app.UseStaticFilesare not double‑logged because they terminate before the logging middleware.
Trust / Data Boundaries
To avoid leaking secrets, the middleware logs only:
context.Request.Methodcontext.Request.Pathcontext.Response.StatusCode- A timestamp from
DateTime.UtcNow
Headers, query strings, and the request body are deliberately omitted. If a future requirement mandates logging of non‑sensitive headers, they can be added after an explicit allow‑list review.
Operational Checks
After deployment, verify the middleware works as expected:
- Health‑endpoint validation – Ensure the standard health endpoint (
/healthz) returns 200 and that a log entry appears for the request. - Log‑level configuration – Set the logger’s minimum level in
appsettings.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning"
}
}
}
Changing the level to "Warning" should suppress the request‑logging entries (which are logged at "Information").
- Latency monitoring – Measure the time between the middleware’s start and end timestamps (can be added as a custom metric). A sudden increase indicates the logging step or the underlying logger is becoming a bottleneck.
Failure Modes and Mitigations
- Logger throws an exception (e.g., disk full, mis‑configured sink). The
try/catcharound the logger call prevents the exception from propagating, so the request continues and returns its normal status code. Operators should still monitor the application’s error logs for internal logger failures. - Mis‑configuration leading to missing logs – If the logger is inadvertently set to a level higher than "Information", request logs disappear. The operational check described above (adjusting level and confirming suppression) detects this.
- Incorrect middleware ordering – Placing the logger before
app.UseRoutingcan cause duplicate logs for static files; placing it after authentication skips unauthenticated requests. Verify order by reviewingProgram.csand confirming the sequence:UseRouting→ logging middleware →UseEndpoints(orMapControllers).
When the Design Would Change
The current minimal design is appropriate for most line‑of‑business APIs. It would be revisited if:
- The application requires high‑throughput, asynchronous logging (e.g., >10k RPS) where even a synchronous logger call could add latency. In that case, introduce a background
Channel<LogEntry>that the middleware writes to, and a dedicated consumer service persists the entries. - Compliance or audit rules demand redaction of personally identifiable information (PII) that might appear in query strings or headers. Then a separate enrichment step would inspect and scrub those fields before they are passed to the logger.
- Business logic needs to correlate logs across services using a trace‑id propagated via HTTP headers. The middleware would then extract and log that header (after confirming it is non‑sensitive) and propagate it via
Activity.Current.
Example Implementation
Create the middleware class:
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var start = DateTime.UtcNow;
try
{
await _next(context);
}
finally
{
var end = DateTime.UtcNow;
var elapsed = end - start;
try
{
_logger.LogInformation(
"{Timestamp:O} {Method} {Path} responded {StatusCode} in {ElapsedMs}ms",
start,
context.Request.Method,
context.Request.Path,
context.Response.StatusCode,
elapsed.TotalMilliseconds);
}
catch (Exception loggerEx)
{
// Swallow logger errors to avoid breaking the request pipeline.
// In production, consider emitting a diagnostic event or metric here.
_logger.LogWarning(loggerEx, "Failed to write request log.");
}
}
}
}
// Extension method for easier registration
public static class RequestLoggingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}
Register it in Program.cs (ASP.NET Core 6 minimal hosting model):
var builder = WebApplication.CreateBuilder(args);
// Add services (controllers, etc.)
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Middleware pipeline
app.UseRouting();
// Place logging after routing, before endpoint execution
app.UseRequestLogging();
app.UseAuthorization();
app.MapControllers();
app.Run();
To test locally:
- Create a new project:
dotnet new webapi -n LoggingDemo - Add the middleware class and extension to the project.
- Insert the registration lines as shown above.
- Run:
dotnet run --project LoggingDemo - In another terminal, send a request:
curl -i http://localhost:5000/weatherforecast - Observe the console output; you should see a line similar to:
2026-10-11T14:23:51.123Z GET /weatherforecast responded 200 in 3.2ms
Adjust appsettings.json to set "Logging:LogLevel:Default": "Warning" and repeat the request; the request‑logging line should no longer appear, confirming that the middleware respects the configured log level.
Limitations and Practical Verification
This middleware does not:
- Log the request body or headers (by design). If that becomes necessary, implement a separate enrichment middleware that redacts known sensitive fields before logging.
- Provide structured properties for advanced querying (e.g., via Seq or Elasticsearch) beyond the message template. To add properties, replace the
LogInformationcall with a scoped begin scope or useILogger.BeginScope. - Guarantee ordering of logs when the underlying logger uses asynchronous sinks; the try/catch ensures the request continues, but log persistence timing is sink‑dependent.
To verify that the middleware does not add noticeable latency, run a simple load test (e.g., hey -n 1000 -c 10 http://localhost:5000/weatherforecast) and compare average response times with and without the middleware registered. Any increase should be within the low‑single‑digit millisecond range for typical I/O‑bound workloads.
Rollback
Adding the middleware does not alter persistent state; it only inserts a delegate into the request pipeline. If you need to remove it, delete the app.UseRequestLogging(); line and redeploy. No database or configuration migration is required.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.