Logging HttpClient
requests and responses in C# is essential for debugging API calls, monitoring performance, and troubleshooting network issues. This guide covers multiple approaches from basic built-in logging to advanced custom implementations.
Method 1: Built-in HttpClient Logging
Configuration via appsettings.json
The simplest approach is to enable built-in logging through configuration:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"System.Net.Http.HttpClient": "Debug",
"System.Net.Http.HttpClient.Default.LogicalHandler": "Debug",
"System.Net.Http.HttpClient.Default.ClientHandler": "Debug"
}
}
}
.NET 6+ Program.cs Setup
var builder = WebApplication.CreateBuilder(args);
// Add HttpClient with logging
builder.Services.AddHttpClient();
// Optional: Configure specific log levels
builder.Logging.AddFilter("System.Net.Http.HttpClient", LogLevel.Debug);
var app = builder.Build();
Service Implementation
public class ApiService
{
private readonly HttpClient _httpClient;
private readonly ILogger<ApiService> _logger;
public ApiService(IHttpClientFactory httpClientFactory, ILogger<ApiService> logger)
{
_httpClient = httpClientFactory.CreateClient();
_logger = logger;
}
public async Task<string> GetDataAsync(string url)
{
_logger.LogInformation("Making request to {Url}", url);
var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
_logger.LogInformation("Received {ContentLength} characters", content.Length);
return content;
}
}
Method 2: Custom Logging Handler
For more control over what gets logged, create a custom DelegatingHandler
:
public class LoggingHandler : DelegatingHandler
{
private readonly ILogger<LoggingHandler> _logger;
public LoggingHandler(ILogger<LoggingHandler> logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
// Log request
_logger.LogInformation("HTTP {Method} {Uri}", request.Method, request.RequestUri);
if (request.Content != null)
{
var requestBody = await request.Content.ReadAsStringAsync();
_logger.LogDebug("Request Body: {RequestBody}", requestBody);
}
var stopwatch = Stopwatch.StartNew();
// Send request
var response = await base.SendAsync(request, cancellationToken);
stopwatch.Stop();
// Log response
_logger.LogInformation("HTTP {Method} {Uri} responded {StatusCode} in {ElapsedMilliseconds}ms",
request.Method, request.RequestUri, (int)response.StatusCode, stopwatch.ElapsedMilliseconds);
if (response.Content != null)
{
var responseBody = await response.Content.ReadAsStringAsync();
_logger.LogDebug("Response Body: {ResponseBody}", responseBody);
}
return response;
}
}
Register the Custom Handler
// In Program.cs or Startup.cs
builder.Services.AddTransient<LoggingHandler>();
builder.Services.AddHttpClient<ApiService>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
})
.AddHttpMessageHandler<LoggingHandler>();
Method 3: Named HttpClient with Logging
For different logging configurations per API:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Named client with specific logging
services.AddHttpClient("ApiClient", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
})
.AddHttpMessageHandler<LoggingHandler>();
// Another client with different configuration
services.AddHttpClient("InternalApi", client =>
{
client.BaseAddress = new Uri("https://internal.api.com/");
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()
{
UseProxy = false // Example: disable proxy for internal calls
});
}
}
Using Named Clients
public class MultiApiService
{
private readonly HttpClient _externalClient;
private readonly HttpClient _internalClient;
public MultiApiService(IHttpClientFactory httpClientFactory)
{
_externalClient = httpClientFactory.CreateClient("ApiClient");
_internalClient = httpClientFactory.CreateClient("InternalApi");
}
public async Task<string> GetExternalDataAsync()
{
var response = await _externalClient.GetAsync("data");
return await response.Content.ReadAsStringAsync();
}
}
Method 4: Logging with Serilog
For structured logging with Serilog:
public class SerilogLoggingHandler : DelegatingHandler
{
private readonly ILogger _logger = Log.ForContext<SerilogLoggingHandler>();
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var requestId = Guid.NewGuid().ToString("N")[..8];
_logger.Information("HTTP Request {RequestId}: {Method} {Uri}",
requestId, request.Method, request.RequestUri);
var response = await base.SendAsync(request, cancellationToken);
_logger.Information("HTTP Response {RequestId}: {StatusCode} {ReasonPhrase}",
requestId, (int)response.StatusCode, response.ReasonPhrase);
return response;
}
}
What Gets Logged
With Debug
level logging enabled, you'll see:
info: System.Net.Http.HttpClient.Default.LogicalHandler[100]
Start processing HTTP request GET https://api.example.com/data
info: System.Net.Http.HttpClient.Default.ClientHandler[100]
Sending HTTP request GET https://api.example.com/data
info: System.Net.Http.HttpClient.Default.ClientHandler[101]
Received HTTP response after 156ms - OK
info: System.Net.Http.HttpClient.Default.LogicalHandler[101]
End processing HTTP request after 159ms - OK
Security Considerations
⚠️ Important Security Notes:
- Never log sensitive data like API keys, passwords, or personal information
- Use
LogLevel.Debug
only in development environments - Consider implementing log filtering for production:
public class SafeLoggingHandler : DelegatingHandler
{
private readonly ILogger<SafeLoggingHandler> _logger;
private readonly IWebHostEnvironment _env;
public SafeLoggingHandler(ILogger<SafeLoggingHandler> logger, IWebHostEnvironment env)
{
_logger = logger;
_env = env;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
// Only log request/response bodies in development
var shouldLogBodies = _env.IsDevelopment();
if (shouldLogBodies && request.Content != null)
{
var body = await request.Content.ReadAsStringAsync();
// Filter sensitive data
var safeBody = FilterSensitiveData(body);
_logger.LogDebug("Request: {Body}", safeBody);
}
var response = await base.SendAsync(request, cancellationToken);
return response;
}
private string FilterSensitiveData(string content)
{
// Implement your filtering logic here
return content.Replace("\"password\":\"[^\"]*\"", "\"password\":\"***\"");
}
}
Performance Considerations
- Built-in logging has minimal performance impact
- Custom handlers add slight overhead for each request
- Avoid logging large response bodies in production
- Use asynchronous logging when possible
- Consider using
IMemoryCache
for frequently accessed endpoints
This comprehensive approach gives you full visibility into your HttpClient operations while maintaining security and performance best practices.