Request Logging and Performance Logging

In a production ASP.NET Core Web API, two categories of logs are especially important: HTTP request logs (which endpoint was called, how long it took, what status code was returned) and performance logs (slow database queries, slow service calls). Without these, troubleshooting performance regressions and error spikes requires guesswork. Serilog’s UseSerilogRequestLogging middleware provides concise, structured request logs, and standard Stopwatch-based logging patterns cover performance measurement.

Serilog Request Logging

// โ”€โ”€ Replace verbose ASP.NET Core request logging with Serilog's concise version โ”€โ”€
// Without UseSerilogRequestLogging, ASP.NET Core logs 4+ lines per request.
// With it: one structured line per request with all relevant properties.

// Program.cs โ€” add BEFORE MapControllers
app.UseSerilogRequestLogging(opts =>
{
    // Include response body size, endpoint name, user
    opts.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("UserId",        httpContext.User.Identity?.Name ?? "anonymous");
    };
    // Only log requests that take more than 200ms as Warning
    opts.GetLevel = (httpContext, elapsed, ex) =>
        ex != null ? LogEventLevel.Error
        : elapsed > 500 ? LogEventLevel.Warning
        : LogEventLevel.Information;
});

// Output: "HTTP GET /api/posts/42 responded 200 in 45.2 ms" + all enriched properties
// Each entry includes: Method, Path, StatusCode, Elapsed, RequestHost, UserId, etc.
Note: UseSerilogRequestLogging suppresses the verbose ASP.NET Core framework request logs by default (the multiple per-request Microsoft.AspNetCore.Hosting.HttpRequestIn and HttpRequestOut entries). This is usually desired โ€” Serilog’s single structured line is more useful than the framework’s multiple lines. To keep both, set opts.MessageTemplate and do not suppress the Microsoft log categories in your minimum level overrides.
Tip: Use the GetLevel delegate to log slow requests at a higher level. Requests completing in under 200ms are routine โ€” Information level is appropriate. Requests taking 500msโ€“2s may indicate a performance issue โ€” Warning level gets attention without alarm. Requests over 5 seconds indicate a definite problem โ€” Error level. This tiered approach means fast requests do not flood your logs, while slow requests are immediately visible in dashboards that monitor Warning and Error counts.
Warning: Do not log request bodies or response bodies by default โ€” they may contain passwords, payment details, personal data, or large payloads that flood your log storage. If you need to debug a specific failing request, implement a conditional body-logging middleware that activates only when a specific debug header or query parameter is present, and ensure it is disabled in production by default.

Performance Logging with Stopwatch

// โ”€โ”€ Measure and log operation duration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
public async Task<IReadOnlyList<Post>> GetPublishedAsync(int page, int size, CancellationToken ct)
{
    var sw = Stopwatch.StartNew();
    try
    {
        var posts = await _repo.GetPublishedAsync(page, size, ct);
        sw.Stop();

        if (sw.ElapsedMilliseconds > 500)
            _logger.LogWarning(
                "Slow query: GetPublishedAsync page={Page} size={Size} took {ElapsedMs}ms",
                page, size, sw.ElapsedMilliseconds);
        else
            _logger.LogDebug(
                "GetPublishedAsync page={Page} size={Size} completed in {ElapsedMs}ms",
                page, size, sw.ElapsedMilliseconds);

        return posts;
    }
    catch (Exception ex)
    {
        sw.Stop();
        _logger.LogError(ex,
            "GetPublishedAsync failed after {ElapsedMs}ms", sw.ElapsedMilliseconds);
        throw;
    }
}

// โ”€โ”€ High-performance LoggerMessage (eliminates allocation per call) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
public static partial class LogMessages
{
    [LoggerMessage(Level = LogLevel.Warning,
        Message = "Slow query: {Operation} took {ElapsedMs}ms")]
    public static partial void SlowQuery(ILogger logger, string operation, long elapsedMs);

    [LoggerMessage(Level = LogLevel.Information,
        Message = "User {UserId} authenticated from {IpAddress}")]
    public static partial void UserAuthenticated(ILogger logger, string userId, string ipAddress);
}

Common Mistakes

Mistake 1 โ€” Logging request bodies containing sensitive data

โŒ Wrong โ€” logging the full request body exposes passwords and personal data in logs.

โœ… Correct โ€” never log request/response bodies by default; add controlled debug logging if needed.

Mistake 2 โ€” Using string interpolation in LoggerMessage attributes

โŒ Wrong โ€” compile error: LoggerMessage messages must be compile-time constants.

โœ… Correct โ€” use named placeholders in the Message attribute; values passed as method parameters.

🧠 Test Yourself

What is the advantage of using source-generated [LoggerMessage] over calling logger.LogInformation() directly?