Request Logging — HTTP Traffic Observability in ASP.NET Core
Log HTTP requests and responses in ASP.NET Core: Serilog's UseSerilogRequestLogging, HttpLogging middleware, custom request logging middleware, performance logging, and what to include versus exclude.
Why Request Logging
Per-request logging gives you:
✓ Method, path, status code, duration for every request
✓ Performance baselines: which endpoints are slow?
✓ Error rates per endpoint: which paths return 5xx?
✓ Traffic patterns: which endpoints are called most?
✓ Baseline for SLA monitoring
Without it: you know the application is slow but not where.
With it: "GET /api/prescriptions/{id} p99 = 850ms" — actionable.Serilog Request Logging
// NuGet: Serilog.AspNetCore
// Add before routing and auth, after exception handler
app.UseSerilogRequestLogging(options =>
{
options.MessageTemplate =
"HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000}ms";
// Enrich the log entry with additional properties
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme);
diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString());
diagnosticContext.Set("ClientIP", httpContext.Connection.RemoteIpAddress?.ToString());
// Include user ID if authenticated
var userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId is not null)
diagnosticContext.Set("UserId", userId);
};
// Filter: skip health check endpoints (reduce noise)
options.GetLevel = (httpContext, elapsed, ex) =>
{
if (httpContext.Request.Path.StartsWithSegments("/health"))
return LogEventLevel.Verbose; // effectively filtered out
if (ex is not null || httpContext.Response.StatusCode >= 500)
return LogEventLevel.Error;
if (elapsed > 1000 || httpContext.Response.StatusCode >= 400)
return LogEventLevel.Warning;
return LogEventLevel.Information;
};
});ASP.NET Core HTTP Logging Middleware
// Built-in .NET 6+ middleware for HTTP logging
// NuGet: none needed — in Microsoft.AspNetCore.HttpLogging
builder.Services.AddHttpLogging(logging =>
{
logging.LoggingFields = HttpLoggingFields.RequestMethod
| HttpLoggingFields.RequestPath
| HttpLoggingFields.ResponseStatusCode
| HttpLoggingFields.Duration;
// Do NOT include RequestBody or ResponseBody in production — they contain PII
// Do NOT include RequestHeaders unless you filter out Authorization headers
logging.RequestHeaders.Add("X-Correlation-Id");
logging.ResponseHeaders.Add("X-Correlation-Id");
logging.MediaTypeOptions.AddText("application/json");
logging.RequestBodyLogLimit = 0; // do not log request body
logging.ResponseBodyLogLimit = 0; // do not log response body
});
app.UseHttpLogging();Custom Request Timing Middleware
// For detailed timing with clinical context
public sealed class RequestTimingMiddleware : IMiddleware
{
private readonly ILogger<RequestTimingMiddleware> _logger;
private const int SlowRequestThresholdMs = 500;
public RequestTimingMiddleware(ILogger<RequestTimingMiddleware> logger)
=> _logger = logger;
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var sw = Stopwatch.StartNew();
try
{
await next(context);
}
finally
{
sw.Stop();
var elapsed = sw.ElapsedMilliseconds;
var method = context.Request.Method;
var path = context.Request.Path.Value;
var statusCode = context.Response.StatusCode;
if (elapsed > SlowRequestThresholdMs)
{
_logger.LogWarning(
"Slow request: {Method} {Path} → {StatusCode} in {ElapsedMs}ms",
method, path, statusCode, elapsed);
}
else
{
_logger.LogInformation(
"Request: {Method} {Path} → {StatusCode} in {ElapsedMs}ms",
method, path, statusCode, elapsed);
}
}
}
}What to Log and What to Exclude
INCLUDE in request logs:
✓ HTTP method (GET, POST, PUT, DELETE)
✓ Request path (sanitized — replace IDs with placeholders if needed)
✓ Status code (200, 400, 500)
✓ Duration (milliseconds)
✓ Correlation ID / Request ID
✓ User ID (authenticated only — not username or email)
✓ Client IP (for audit and abuse detection)
EXCLUDE from request logs:
✗ Request body (may contain PII: names, DOB, clinical values)
✗ Response body (same PII risk)
✗ Authorization header (contains tokens — never log tokens)
✗ Cookie header (session tokens)
✗ Patient name, MRN, diagnosis in URL path where possible
✗ Query string if it contains tokens or PII
HIPAA/GDPR: log who accessed what, not the data they accessed.Route Template Logging
// Problem: /api/patients/3f4a... appears in logs with the actual GUID.
// At scale: 1,000,000 unique paths instead of one.
// Metrics aggregation is impossible when paths include IDs.
// Fix: log the route template, not the actual path
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
// Use the matched route template — groups all variants of /patients/{id}
var endpoint = httpContext.GetEndpoint();
var routePattern = (endpoint as RouteEndpoint)?.RoutePattern.RawText;
if (routePattern is not null)
diagnosticContext.Set("RouteTemplate", routePattern);
// Logs: "RouteTemplate: api/patients/{id}" instead of "api/patients/3f4..."
};Health Check Filtering
// Kubernetes/Azure: health probes hit /health every 10 seconds
// Logging every probe adds thousands of meaningless entries per day
// Option 1: filter via Serilog GetLevel (already shown above)
options.GetLevel = (ctx, elapsed, ex) =>
ctx.Request.Path.StartsWithSegments("/health")
? LogEventLevel.Verbose
: LogEventLevel.Information;
// Option 2: exclude health check paths from HttpLogging
app.UseWhen(
ctx => !ctx.Request.Path.StartsWithSegments("/health"),
branch => branch.UseHttpLogging());Production issue I've seen: A team's request logs included the full request body for POST endpoints. A POST to
/api/prescriptionslogged the entire JSON body includingwarfarinDoseMg,patientAllergyList, andprescriberNotes. When a developer shared a log snippet for debugging, it included real clinical data for 3 patients. Under HIPAA, logging PHI in application logs requires the same protections as the PHI itself. The fix was removing body logging entirely and logging only structural metadata (IDs, operation type, status codes).
Key Takeaway
Use
app.UseSerilogRequestLogging()with a customEnrichDiagnosticContextfor method, path, status code, duration, and user ID. Log route templates, not actual paths with embedded IDs — enables metric aggregation. Never log request/response bodies in production — they contain PII. Filter health check paths to reduce log noise. Flag slow requests (over 500ms) at Warning level so they appear in alerts without manual querying.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.