Learnixo
Back to blog
AI Systemsintermediate

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.

Asma Hafeez KhanMay 16, 20265 min read
LoggingRequest LoggingSerilogASP.NET Core.NETObservability
Share:𝕏

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

C#
// 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

C#
// 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

C#
// 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

C#
// 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

C#
// 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/prescriptions logged the entire JSON body including warfarinDoseMg, patientAllergyList, and prescriberNotes. 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 custom EnrichDiagnosticContext for 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.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.