Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DilanLivera/aead87e60303892a8766e20eb3f8adcf to your computer and use it in GitHub Desktop.
Save DilanLivera/aead87e60303892a8766e20eb3f8adcf to your computer and use it in GitHub Desktop.
Incoming HTTP request and response logging

Custom incoming HTTP request and response logging middleware

using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.IO;

namespace WeatherApi.Middleware;

public class RequestResponseLoggingMiddleware
{
    private readonly ILogger<RequestResponseLoggingMiddleware> _logger;
    private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
    private readonly RequestDelegate _next;

    public RequestResponseLoggingMiddleware(
        RequestDelegate next, 
        ILogger<RequestResponseLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
        _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager();
    }

    public async Task Invoke(HttpContext context)
    {
        if (!_logger.IsEnabled(LogLevel.Debug))
        {
            await _next.Invoke(context);
            return;
        }

        await LogRequest(context.Request);

        var originalResponseBodyStream = context.Response.Body;

        using var responseBody = _recyclableMemoryStreamManager.GetStream();

        context.Response.Body = responseBody;

        await _next(context);

        await LogResponse(context.Response);

        await responseBody.CopyToAsync(originalResponseBodyStream);
    }

    private async Task LogRequest(HttpRequest httpRequest)
    {
        var requestDictionary = await GetRequestContent(httpRequest);

        _logger.LogDebug("Request content: {RequestContent}", requestDictionary);
    }

    private async Task LogResponse(HttpResponse httpResponse)
    {
        var responseDictionary = await GetResponseContent(httpResponse);

        _logger.LogDebug("Response content: {ResponseContent}", responseDictionary);
    }

    public async Task<Dictionary<string, string>> GetRequestContent(HttpRequest request)
    {
        if (request is null)
        {
            return new Dictionary<string, string>();
        }

        var requestDictionary = new Dictionary<string, string>
        {
            { nameof(request.Method), request.Method },
            { nameof(request.Path), request.Path },
            { nameof(request.Scheme), request.Scheme }
        };

        if (request.Headers != null && request.Headers.Any())
        {
            var headers = JsonSerializer.Serialize(request.Headers);
            requestDictionary.Add(nameof(request.Headers), headers);
        }

        if (request.QueryString.HasValue && request.QueryString.Value != null)
        {
            requestDictionary.Add(nameof(request.QueryString), request.QueryString.Value);
        }

        if (request.Body is null ||
            request.ContentType is null ||
            !request.ContentType.Contains("application/json"))
        {
            return requestDictionary;
        }

        request.EnableBuffering();

        await using var requestBodyStream = _recyclableMemoryStreamManager.GetStream();
        await request.Body.CopyToAsync(requestBodyStream);

        request.Body.Seek(0, SeekOrigin.Begin);

        var requestBodyContent = GetContent(requestBodyStream);
        requestDictionary.Add(nameof(request.Body), requestBodyContent);

        return requestDictionary;
    }

    public async Task<Dictionary<string, string>> GetResponseContent(HttpResponse response)
    {
        if (response is null)
        {
            return new Dictionary<string, string>();
        }

        var responseDictionary = new Dictionary<string, string>
            {
                { nameof(response.StatusCode), response.StatusCode.ToString() }
            };

        if (response.Headers != null && response.Headers.Any())
        {
            var headers = JsonSerializer.Serialize(response.Headers);
            responseDictionary.Add(nameof(response.Headers), headers);
        }

        response.Body.Seek(0, SeekOrigin.Begin);

        if (response.ContentType is null || !response.ContentType.Contains("application/json"))
        {
            return responseDictionary;
        }

        await using var responseBodyStream = _recyclableMemoryStreamManager.GetStream();
        await response.Body.CopyToAsync(responseBodyStream);

        var responseBodyContent = GetContent(responseBodyStream);
        response.Body.Seek(0, SeekOrigin.Begin);

        if (!string.IsNullOrEmpty(responseBodyContent))
        {
            responseDictionary.Add(nameof(response.Body), responseBodyContent);
        }

        return responseDictionary;
    }

    private static string GetContent(Stream stream)
    {
        const int readChunkBufferLength = 4096;

        stream.Seek(0, SeekOrigin.Begin);

        using var textWriter = new StringWriter();
        using var reader = new StreamReader(stream);

        var readChunk = new char[readChunkBufferLength];

        int readChunkLength;

        do
        {
            readChunkLength = reader.ReadBlock(readChunk, 0, readChunkBufferLength);
            textWriter.Write(readChunk, 0, readChunkLength);
        }
        while (readChunkLength > 0);

        return textWriter.ToString();
    }
}

.NET HTTP Logging middleware

Microsoft Docs: HTTP Logging in ASP.NET Core

Configure the HTTP Logging middleware using the appsettings.json file

HttpLogging.cs

public class HttpLogging
{
    private readonly List<string> _requestHeaders = new();
    private readonly List<string> _responseHeaders = new();
    private List<string> _additionalMediaTypes = new();
    public List<HttpLoggingFields> HttpLoggingFields { get; init; } = new();

    public List<string> AdditionalMediaTypes
    {
        get => _additionalMediaTypes.Where(mediaType => !string.IsNullOrWhiteSpace(mediaType)).ToList();
        set => _additionalMediaTypes = value;
    }

    public List<string> RequestHeaders
    {
        get => _requestHeaders.Where(header => !string.IsNullOrWhiteSpace(header)).ToList();
        init => _requestHeaders = value;
    }

    public List<string> ResponseHeaders
    {
        get => _responseHeaders.Where(header => !string.IsNullOrWhiteSpace(header)).ToList();
        init => _responseHeaders = value;
    }

    public int RequestBodyLogLimitInBytes { get; init; } = 32000; // Default is 32KB
    public int ResponseBodyLogLimitInBytes { get; init; } = 32000; // Default is 32KB
}

Program.cs

// removed for brevity

builder.Services.AddCustomHttpLoggingWithHttpLoggingOptions(builder.Configuration);

WebApplication app = builder.Build();

app.UseHttpLogging();

// removed for brevity

ServiceCollectionExtensions.cs

public static IServiceCollection AddCustomHttpLoggingWithHttpLoggingOptions(
    this IServiceCollection services, IConfiguration configuration)
{
    HttpLogging loggingConfiguration = services
        .Configure<HttpLogging>(configuration.GetSection(nameof(HttpLogging)))
        .BuildServiceProvider()
        .GetRequiredService<IOptions<HttpLogging>>().Value;

    services.AddHttpLogging(options =>
    {
        options.LoggingFields = loggingConfiguration.HttpLoggingFields.Aggregate(
            HttpLoggingFields.None, 
            (loggingFields, loggingField) => loggingFields | loggingField);

        foreach (var mediaType in loggingConfiguration.AdditionalMediaTypes)
        {
            options.MediaTypeOptions.AddText(mediaType);
        }

        foreach (var header in loggingConfiguration.RequestHeaders)
        {
            options.RequestHeaders.Add(header);
        }

        foreach (var header in loggingConfiguration.ResponseHeaders)
        {
            options.ResponseHeaders.Add(header);
        }

        options.RequestBodyLogLimit = loggingConfiguration.RequestBodyLogLimitInBytes;
        options.ResponseBodyLogLimit = loggingConfiguration.ResponseBodyLogLimitInBytes;
    });

    return services;
}

appsettings.json

{
  "HttpLogging": {
    "HttpLoggingFields": [
      "All"
    ],
    "AdditionalMediaTypes": [],
    "RequestHeaders": [
      "x-user-id",
      "x-session-id",
      "x-transaction-id"
    ],
    "ResponseHeaders": [],
    "RequestBodyLogLimitInBytes": 32000,
    "ResponseBodyLogLimitInBytes": 32000
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment