Practical request logging and monitoring with .NET middleware

ASP.NET Core handles every web request through an ordered pipeline of middleware components. That pipeline is a convenient place to record what arrived, how long the work took and how the response finished. A well-designed logging layer gives support staff and developers a shared trail without forcing controllers and background jobs to repeat the same diagnostic code.

The aim is not to capture everything. It is to produce useful, low-overhead evidence that reveals slow endpoints, rising error rates and overloaded dependencies while keeping credentials and personal information out of the telemetry. The middleware below provides the request summary; OpenTelemetry, metrics and application traces add the wider operational picture.

Why middleware belongs in the request pipeline

Middleware runs once around the rest of the request pipeline, so it can observe requests that never reach a controller, including unmatched routes and rejected requests. It also sees the final status code and elapsed time. That makes it a better home for consistent request summaries than scattering logging calls across action methods, Razor pages and minimal API endpoints.

The response can change while the request travels through the pipeline, which is why middleware should measure the completed operation rather than infer the outcome from the route alone. ASP.NET Core also creates diagnostic information for each request. Reusing that information keeps middleware aligned with the framework’s own hosting and tracing behaviour.

This personal site carries technical tutorials alongside shorter news posts, including a Dutch-language news item. Keeping those categories distinct in the URL and navigation makes it easier for readers to choose the material they need. The same principle applies inside an application: request telemetry should have a predictable identity and location instead of being mixed into every feature.

Building a safe request logging middleware

Start with a small class that records one request event. The implementation below uses constructor injection, a disposable logging scope and TimeProvider, avoiding a direct dependency on the system clock so tests can control elapsed time.

using System.Diagnostics;
using Microsoft.AspNetCore.Routing;

public sealed class RequestLoggingMiddleware(
    RequestDelegate next,
    ILogger<RequestLoggingMiddleware> logger,
    TimeProvider clock)
{
    public async Task InvokeAsync(HttpContext context)
    {
        var started = clock.GetTimestamp();

        using var scope = logger.BeginScope(
            new Dictionary<string, object?>
            {
                ["TraceId"] = Activity.Current?.TraceId.ToString()
                    ?? context.TraceIdentifier,
                ["RequestId"] = context.TraceIdentifier
            });

        try
        {
            await next(context);

            var elapsed = clock.GetElapsedTime(started);
            var status = context.Response.StatusCode;
            var level = status >= 500 ? LogLevel.Error
                : status >= 400 ? LogLevel.Warning
                : LogLevel.Information;
            var route = (context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText
                ?? "unmatched";

            logger.Log(
                level,
                "HTTP {Method} {Route} returned {StatusCode} in {ElapsedMs} ms",
                context.Request.Method,
                route,
                status,
                elapsed.TotalMilliseconds);
        }
        catch (Exception ex)
        {
            var elapsed = clock.GetElapsedTime(started);

            logger.LogError(
                ex,
                "HTTP {Method} failed after {ElapsedMs} ms",
                context.Request.Method,
                elapsed.TotalMilliseconds);
            throw;
        }
    }
}

Register the clock and middleware in the application setup. If you use exception-handling middleware, place it before request logging so the final error response receives a normal completion summary; the catch block then mainly covers failures that escape that handler.

builder.Services.AddSingleton(TimeProvider.System);

app.UseMiddleware<RequestLoggingMiddleware>();

The default shape is deliberately conservative. It does not read request or response bodies, capture query strings or record cookies, because bodies may contain passwords, health information or uploaded files, and reading a streaming response can break it. Raw paths can also contain identifiers. Prefer the matched route template, as shown here, or sanitise unmatched paths according to an explicit policy.

For Australian organisations, telemetry retention can also fall within privacy obligations. The Australian Privacy Principles in the Privacy Act 1988 may apply to personal information held in logs, depending on the organisation and its activities. Set retention periods deliberately, restrict access and document why an address or account identifier is being recorded; “we might need it later” is not a useful policy.

Adding correlation, tracing and useful context

A request line is much more useful when it can be joined to other events. HttpContext.TraceIdentifier links entries made during one request, while Activity.Current.TraceId can connect that work to an outbound HTTP call or a trace exported through OpenTelemetry. The scope in the middleware places both identifiers on subsequent log events, so support staff can search for one request without reconstructing its path from timestamps.

ASP.NET Core already establishes an Activity for incoming requests and propagates the W3C traceparent header when a downstream service participates. Creating another activity inside the logging middleware would produce duplicate or misleading spans. Read the current activity instead, and make sure the logging provider includes scopes in its output. For JSON console output, that means enabling the IncludeScopes option.

Keep timestamps in UTC and convert them for human reports when necessary. Australian Eastern Time alternates between AEST and AEDT, and daylight-saving changes can make incident timelines confusing if local offsets are mixed with UTC. A small dashboard label such as “AEST” or “AEDT”, plus an explicit UTC option, prevents an hour of confusion during a busy support call.

Turning logs into operational signals

Logs explain individual events; metrics reveal patterns across many requests. A practical service view needs request rate, latency percentiles, error rate and saturation, rather than thousands of nearly identical success messages. Modern ASP.NET Core supplies hosting metrics such as request duration and active requests, and the OpenTelemetry .NET SDK can collect them alongside custom application metrics.

Use counters for request totals, histograms for latency and gauges for values that rise and fall. Keep route labels bounded: /orders/{id} is a useful metric dimension, while /orders/123 creates a new time series for every order and can overwhelm the monitoring backend. Dependency calls also deserve their own duration and failure metrics, since a fast API can still be slow because its SQL Server queries or payment provider are struggling.

The Australian Signals Directorate’s cybersecurity guidance is a useful reminder to protect diagnostic data and make monitoring part of routine operations. It should be useful to authorised responders without becoming an open map of customer activity. On dashboards, show the signals that require action first and keep detailed traces one click away. A guide to affiliate widget advice makes a related design point: reducing clutter helps people notice what matters.

Practical recommendations for production use

A short, enforceable checklist works better than a large logging policy that nobody follows.

Sydney and Melbourne employers in banking, logistics, government and retail often maintain .NET services alongside cloud platforms and operational support teams. That makes a shared middleware especially valuable: application developers get a standard instrumentation point, while platform engineers can maintain consistent exporters and dashboards without asking every team to invent its own format. Teams should agree on naming, ownership and retention before the first production deployment, then revisit those decisions as the service grows.

Put the middleware to work

Start with one staging deployment, send a mixture of successful, failed and slow requests through it, and inspect the resulting logs, metrics and traces together. Confirm that an on-call engineer can find a failing request, identify its route and see the dependency involved without searching several unrelated tools.

Choose one telemetry destination, define a small set of service objectives, and make the middleware the consistent source of request metadata. Add it to a staging environment today, measure its overhead against a baseline, and turn the findings into a maintained standard for the team.