I use dependency injection and the built in logging to make this configurable. I use this pattern when OpenAPI or SOAP definitions are unreliable, or there are bugs in the auto client generation.
When you use an HttpClient it automatically logs headers and precise timings at the trace level for that configured name.
For instance, of you injected services.AddHttpClient("MyHttpClient" ...) you can get headers with this in your appsettings.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
...
"System.Net.Http.HttpClient.MyHttpClient": "Trace"
},
I extend this to request/response content logging with this:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Net.Http;
/// <summary>HTTP request wrapper that logs request/response body.
/// Use this for debugging issues with third party services.</summary>
class TraceContentLoggingHandler(HttpMessageHandler innerHandler, ILogger logger) : DelegatingHandler(innerHandler)
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (logger is null || !logger.IsEnabled(LogLevel.Trace))
return await base.SendAsync(request, cancellationToken); // If not tracing, skip logging
if (request.Content is not null)
logger.LogTrace("""
Request body {URI}
{Content}
""",
request.RequestUri,
await request.Content.ReadAsStringAsync(cancellationToken));
var response = await base.SendAsync(request, cancellationToken); // This is disposable, but if we dispose it the ultimate caller can't read the content
if (response.Content is not null)
logger.LogInformation("""
Response {Code} {Status}
{Content}
""",
(int)response.StatusCode,
response.ReasonPhrase,
await ReadableResponse(response, cancellationToken));
return response;
}
static async Task<string> ReadableResponse(HttpResponseMessage response, CancellationToken cancellationToken)
{
string? contentType = response.Content.Headers.GetValues("Content-Type")?.FirstOrDefault();
if (contentType == "application/zip")
return $"ZIP file {response.Content.Headers.GetValues("Content-Length").FirstOrDefault()} bytes";
return await response.Content.ReadAsStringAsync(cancellationToken);
}
}
public static class TraceContentLoggingExtension {
/// <summary>When trace logging is enabled, log request/response bodies.
/// However, this means that trace logging will be slower.</summary>
public static IHttpClientBuilder AddTraceContentLogging(this IHttpClientBuilder httpClientBuilder)
{
// Get the logger for the named HttpClient
var sp = httpClientBuilder.Services.BuildServiceProvider();
var logger = sp.GetService<ILoggerFactory>()?.CreateLogger($"System.Net.Http.HttpClient.{httpClientBuilder.Name}");
// If trace logging is enabled, add the logging handler
if (logger?.IsEnabled(LogLevel.Trace) ?? false)
httpClientBuilder.Services.Configure(
httpClientBuilder.Name,
(HttpClientFactoryOptions options) =>
options.HttpMessageHandlerBuilderActions.Add(b =>
b.PrimaryHandler = new TraceContentLoggingHandler(b.PrimaryHandler, logger)
));
return httpClientBuilder;
}
}
Then, when registering the HttpClient I can set the trace logging in the DI setup:
serviceCollection.AddHttpClient("MyHttpClient", ...). // Named HTTP client
AddTraceContentLogging(); // Add content logging when trace
...
// Any time I get this client from DI it can log content
var httpClient = serviceProvider.GetRequiredService<IHttpClientFactory>().CreateClient("MyHttpClient");
This then makes the request/response logging something I can turn on or off with the "Trace".
This improves over the accepted answer in a few ways:
- Turn on and off from a config file
- Uses the built in logging (can go to EventLog or whatever) instead of
Console
- Avoids
new HttpClient and the pain that causes (always use an HttpClientFactory)
- Easy to add to any existing dependency injected
HttpClient
- Works in .NET8
However, you should still be careful with this...
- It slows down trace logging significantly - not a problem for debugging, but for any performance testing this will slow the existing trace timings down.
- Streaming APIs don't work with it - the logging will stall the code you want to actually handle the request
- Both headers and content will get logged, this can very easily expose things like security keys you don't want shared. For that reason I often make this DEBUG only:
serviceCollection.AddHttpClient("MyHttpClient", client =>
{
client.DefaultRequestHeaders.Add("Authorization", $"Basic {encodedSecret}");
})
#if DEBUG
.AddTraceContentLogging();
#endif