Logging Response Body in AWS Cloudwatch

61 views Asked by At

I'm trying to log the response body of an http request in Cloudwatch logs. It works fine locally but the response body is always empty when deploying the lambda to AWS. Below is the code for my middleware and the associated Cloudwatch log that appears. Notice the ResponseBody element is empty.

 public class LoggingTestMiddleware
 {
     private readonly RequestDelegate _next;
     private readonly RequestResponseLoggerOption _options;
     private readonly IRequestResponseLogger _logger;

     public LoggingTestMiddleware
     (RequestDelegate next, IOptions<RequestResponseLoggerOption> options,
      IRequestResponseLogger logger)
     {
         _next = next;
         _options = options.Value;
         _logger = logger;
     }

     public async Task InvokeAsync(HttpContext httpContext,
            IRequestResponseLogModelCreator logCreator)
     {
         RequestResponseLogModel log = logCreator.LogModel;
         // Middleware is enabled only when the 
         // EnableRequestResponseLogging config value is set.
         if (_options == null || !_options.IsEnabled)
         {
             await _next(httpContext);
             return;
         }
         log.RequestDateTimeUtc = DateTime.UtcNow;
         HttpRequest request = httpContext.Request;

         /*log*/
         log.LogId = Guid.NewGuid().ToString();
         log.TraceId = httpContext.TraceIdentifier;
         var ip = request.HttpContext.Connection.RemoteIpAddress;
         log.ClientIp = ip == null ? null : ip.ToString();
         log.Node = _options.Name;

         /*request*/
         log.RequestMethod = request.Method;
         log.RequestPath = request.Path;
         log.RequestQuery = request.QueryString.ToString();
         log.RequestQueries = FormatQueries(request.QueryString.ToString());
         log.RequestHeaders = FormatHeaders(request.Headers);
         log.RequestBody = await ReadBodyFromRequest(request);
         log.RequestScheme = request.Scheme;
         log.RequestHost = request.Host.ToString();
         log.RequestContentType = request.ContentType;

         // Temporarily replace the HttpResponseStream, 
         // which is a write-only stream, with a MemoryStream to capture 
         // its value in-flight.
         HttpResponse response = httpContext.Response;
         var originalResponseBody = response.Body;
         using var newResponseBody = new MemoryStream();
         response.Body = newResponseBody;

         // Call the next middleware in the pipeline
         try
         {
             await _next(httpContext);
         }
         catch (Exception exception)
         {
             /*exception: but was not managed at app.UseExceptionHandler() 
               or by any middleware*/
             LogError(log, exception);
         }

         newResponseBody.Seek(0, SeekOrigin.Begin);
         var responseBodyText =
             await new StreamReader(response.Body).ReadToEndAsync();

         (string, bool) test = (Encoding.UTF8.GetString(((MemoryStream)newResponseBody).ToArray()), false);
         var responseBodyTest = test.Item1;
         log.ResponseBody = responseBodyTest;

         newResponseBody.Seek(0, SeekOrigin.Begin);
         await newResponseBody.CopyToAsync(originalResponseBody);

         /*response*/
         log.ResponseContentType = response.ContentType;
         log.ResponseStatus = response.StatusCode.ToString();
         log.ResponseHeaders = FormatHeaders(response.Headers);
         //log.ResponseBody = responseBodyText;
         log.ResponseDateTimeUtc = DateTime.UtcNow;


         /*exception: but was managed at app.UseExceptionHandler() 
           or by any middleware*/
         var contextFeature =
             httpContext.Features.Get<IExceptionHandlerPathFeature>();
         if (contextFeature != null && contextFeature.Error != null)
         {
             Exception exception = contextFeature.Error;
             LogError(log, exception);
         }

         //var jsonString = logCreator.LogString(); /*log json*/
         _logger.Log(logCreator);
     }

     private void LogError(RequestResponseLogModel log, Exception exception)
     {
         log.ExceptionMessage = exception.Message;
         log.ExceptionStackTrace = exception.StackTrace;
     }

     private Dictionary<string, string> FormatHeaders(IHeaderDictionary headers)
     {
         Dictionary<string, string> pairs = new Dictionary<string, string>();
         foreach (var header in headers)
         {
             pairs.Add(header.Key, header.Value);
         }
         return pairs;
     }

     private List<KeyValuePair<string, string>> FormatQueries(string queryString)
     {
         List<KeyValuePair<string, string>> pairs =
              new List<KeyValuePair<string, string>>();
         string key, value;
         foreach (var query in queryString.TrimStart('?').Split("&"))
         {
             var items = query.Split("=");
             key = items.Count() >= 1 ? items[0] : string.Empty;
             value = items.Count() >= 2 ? items[1] : string.Empty;
             if (!String.IsNullOrEmpty(key))
             {
                 pairs.Add(new KeyValuePair<string, string>(key, value));
             }
         }
         return pairs;
     }

     private async Task<string> ReadBodyFromRequest(HttpRequest request)
     {
         // Ensure the request's body can be read multiple times 
         // (for the next middlewares in the pipeline).
         request.EnableBuffering();
         using var streamReader = new StreamReader(request.Body, leaveOpen: true);
         var requestBody = await streamReader.ReadToEndAsync();
         // Reset the request's body stream position for 
         // next middleware in the pipeline.
         request.Body.Position = 0;
         return requestBody;
     }
 }

I have the middleware registered in Startup.cs as follows:

public void Configure(IApplicationBuilder app)
{
    app.UseHsts();

    app.Use((context, next) =>
    {
        context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
        return next.Invoke();
    });

    app.UseErrorHandler();

    app.UseXRay(AssemblyName);

    app.UseRouting();

    app.UseOpenApi(settings =>
    {
        settings.Path = "/docs/{documentName}/swagger.json";
    });

    app.UseStaticFiles();

    app.UseSwaggerUi(x =>
    {
        x.Path = "/docs";
        x.DocumentTitle = "Audere API Documentation";
        x.DocumentPath = "/docs/{documentName}/swagger.json";
        x.DocExpansion = "list";
        x.CustomStylesheetPath = "/docs/styles.css";
        x.PersistAuthorization = !this.IsLambda;
        x.TagsSorter = "alpha";
        x.OperationsSorter = "method";
        x.AdditionalSettings["filter"] = true;
        x.AdditionalSettings["deepLinking"] = true;
        x.AdditionalSettings["displayRequestDuration"] = true;
    });

    app.UseApiGatewayAuthorizerAuthentication();

    app.UseAuthorization();

    app.UseLambdaRequestLogging();

    app.UseMiddleware<LoggingTestMiddleware>();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapHealthChecks();

        endpoints.MapGet("/", x =>
        {
            x.Response.StatusCode = StatusCodes.Status302Found;
            x.Response.Headers.Location = "/docs/index.html";

            return Task.CompletedTask;
        });
    });
}

enter image description here

I have tried several different implementations of the middleware but so far have had no success.They show the request body but the response body is empty.

1

There are 1 answers

0
Brian Rice On

Was able to figure out a solution to this using the code below. Apparently, it is necessary to swap out the response.Body with a new MemoryStream before using it.

public async Task InvokeAsync(HttpContext context)
{
 context.Request.EnableBuffering();

 Stream originalRequestBodyStream = context.Request.Body;
 using var requestReader = new StreamReader(originalRequestBodyStream);
 string requestBody = await requestReader.ReadToEndAsync().ConfigureAwait(false);
 context.Request.Body.Seek(0, SeekOrigin.Begin);

 Stream originalResponseBodyStream = context.Response.Body;
 using var responseBody = new MemoryStream();
 context.Response.Body = responseBody;

 await _next(context).ConfigureAwait(false);

 string originalResponseBody = string.Empty;

 context.Response.Body.Seek(0, SeekOrigin.Begin);
 using var responseReader = new StreamReader(originalResponseBodyStream);
 originalResponseBody = await responseReader.ReadToEndAsync().ConfigureAwait(false);

 context.Response.Body.Seek(0, SeekOrigin.Begin);
 await responseBody.CopyToAsync(originalResponseBodyStream).ConfigureAwait(false);

 logger.LogInformation("Original Request Body: {origRequestBody}", requestBody);
 logger.LogInformation("Original Response Body: {origResponseBody}", originalResponseBody);
}