Revisiting improved HTTP logging in ASP.NET Core 8
A few years ago, I had a play with HTTP logging added in ASP.NET Core 6. ASP.NET Core 8 introduced a set of additional configuration options that I believe are essential to make this feature usable. I will recap the details from the previous post below, but for more context, the first part of this series is here. In this post, I'll go through some of the changes introduced in HTTP logging since last.
Before I jump into the improvements, let's recap how to set up HTTP logging. In an ASP.NET Core 8 web app, include the following in the Program.cs
file:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpLogging(options => {}); // ⬅️
// ...
var app = builder.Build();
// ...
app.UseHttpLogging(); // ⬅️
// ...
app.Run();
The two important lines are marked with a commented arrow. Compared to ASP.NET Core 6, calling AddHttpLogging
now seems mandatory, but you can avoid setting any options like in the code above.
Unless you already output Information logging in your configured logging providers, HTTP logging can be enabled by adding the following to the LogLevel
section of the appsettings.json
file:
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
That's it. ASP.NET Core now outputs a log message per request and response through ILogger
. This can be inspected in Visual Studio in the Output window.
Structured logging
I just wanted to follow up on my complaints about missing structured logging from the previous post. The issue is that when logging a request or response, the log message becomes a concatenated string of key/value pairs. Including newlines but still now optimal when viewed in remote loggers like SQL Server, Elasticsearch, or elmah.io:
This is not a huge issue since most of the information embedded in the message still ends up in the right fields (like Method, URL, and Code in the screenshot above). It would still be nice if log messages logged from HTTP logging would include a template message like when logging structured properties through the ILogger
manually. I guess you can't win them all.
Combining request and response
One thing that bothered me when first looking at HTTP logging, was the separation between the request and response in two log messages (see the screenshot above). I like logging both the request and response in a single log message since it (IMO) represents the same action where a user requests the server and gets back a response.
Luckily, Microsoft included a new configuration option to do just that called CombineLogs
that can be set in options:
builder.Services.AddHttpLogging(options =>
{
options.CombineLogs = true; // ⬅️
});
When enabling combined logs, a single log message will be created per request (and response):
Notice how the message now contains the method, URL, and status code in one. Plus, the first part of the log message says "Request and Response". Having this option is great and causes log messages sent from HTTP logging to look more like other types of log messages like errors produced from exception logging middleware.
Interceptors
It's easy to demand everything in version 1 of a feature. But interceptors are one of those features that it's hard to live without. As already discussed in the previous post, the number of configuration options for HTTP logging is very limited. You basically only have the option to decide which fields and headers should be logged. This is where interceptors get really interesting.
As a default, HTTP logging will log all requests and responses. This includes requests for .css
, .js
, and similar files. Let's use this as an example of how to use interceptors. To ignore all requests for these two file types, add a new class named IgnoreLoggingInterceptor
with the following implementation:
public class IgnoreLoggingInterceptor : IHttpLoggingInterceptor
{
public ValueTask OnRequestAsync(HttpLoggingInterceptorContext logContext)
{
string? path = logContext.HttpContext.Request?.Path.Value;
if (path == null
|| path.EndsWith(".css", StringComparison.OrdinalIgnoreCase)
|| path.EndsWith(".js", StringComparison.OrdinalIgnoreCase))
{
logContext.LoggingFields = HttpLoggingFields.None;
}
return default;
}
public ValueTask OnResponseAsync(HttpLoggingInterceptorContext logContext)
{
return default;
}
}
The OnRequestAsync
and OnResponseAsync
methods will be called by HTTP logging before logging requests and responses. In these methods, we can do different things, like deciding which fields to include in the log message on runtime. In the example above, I set the LoggingFields
property to HttpLoggingFields.None
which will ignore the entire log message in case the request is for a css
or js
file. Since we already combined requests and responses into a single log message in the previous section, we only need to implement the OnRequestAsync
method.
To include this interceptor in the pipeline, set it up just after calling the AddHttpLogging
method in Program.cs
:
builder.Services.AddHttpLogging(options =>
{
options.CombineLogs = true;
});
builder.Services.AddHttpLoggingInterceptor<IgnoreLoggingInterceptor>(); // ⬅️
Having the option to add one or more interceptors is a very flexible feature for creating differentiated output messages from HTTP logging.