16

I have a web API based on .net core 3.1.

I'm using the SeriLog library as a logger.

Here is my SeriLog Configuration. Serilog has been configured from 'appsettings.json'.

enter image description here

I need to add 'request body' parameters to log's output if it exists. Is there any way to configure this. Also, I share my log output.

enter image description here

OguzKaanAkyalcin
  • 621
  • 2
  • 8
  • 25
  • You may use Serilog `IDiagnosticContext` interface and set an additional properties, this question [Serilog logging web-api methods, adding context properties inside middleware](https://stackoverflow.com/questions/60076922/serilog-logging-web-api-methods-adding-context-properties-inside-middleware) is good starting point and looks like a duplicate – Pavel Anikhouski Aug 25 '20 at 09:33

9 Answers9

13

I wrote a custom middleware to capture both HTTP requests and responses. It is compatible with ASP.NET Core 3.X and should also work with 2.X and .NET 5.0, although I haven't tested it with those framework versions.

Here's the link to my git repo: https://github.com/matthew-daddario/AspNetCoreRequestResponseLogger

The relevant code is this:

    public class RequestResponseLoggerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly bool _isRequestResponseLoggingEnabled;

    public RequestResponseLoggerMiddleware(RequestDelegate next, IConfiguration config)
    {
        _next = next;
        _isRequestResponseLoggingEnabled = config.GetValue<bool>("EnableRequestResponseLogging", false);
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        // Middleware is enabled only when the EnableRequestResponseLogging config value is set.
        if (_isRequestResponseLoggingEnabled)
        {
            Console.WriteLine($"HTTP request information:\n" +
                $"\tMethod: {httpContext.Request.Method}\n" +
                $"\tPath: {httpContext.Request.Path}\n" +
                $"\tQueryString: {httpContext.Request.QueryString}\n" +
                $"\tHeaders: {FormatHeaders(httpContext.Request.Headers)}\n" +
                $"\tSchema: {httpContext.Request.Scheme}\n" +
                $"\tHost: {httpContext.Request.Host}\n" +
                $"\tBody: {await ReadBodyFromRequest(httpContext.Request)}");

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

            // Call the next middleware in the pipeline
            await _next(httpContext);

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

            Console.WriteLine($"HTTP request information:\n" +
                $"\tStatusCode: {httpContext.Response.StatusCode}\n" +
                $"\tContentType: {httpContext.Response.ContentType}\n" +
                $"\tHeaders: {FormatHeaders(httpContext.Response.Headers)}\n" +
                $"\tBody: {responseBodyText}");

            newResponseBody.Seek(0, SeekOrigin.Begin);
            await newResponseBody.CopyToAsync(originalResponseBody);
        }
        else
        {
            await _next(httpContext);
        }
    }

    private static string FormatHeaders(IHeaderDictionary headers) => string.Join(", ", headers.Select(kvp => $"{{{kvp.Key}: {string.Join(", ", kvp.Value)}}}"));

    private static 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;
    }
}
matthewd98
  • 180
  • 1
  • 3
9

You can log body via options.EnrichDiagnosticContext implementing logic from ReadBodyFromRequest method or from FormatRequest method From matthewd98

But also you need to add your body into template messsage, because the default template message in Serilog is
HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms
and it doesn't contains any Body placeholder

app.UseSerilogRequestLogging(options =>
{
   options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
   {
     // string body = your logic to get body from httpContext.Request.Body
        diagnosticContext.Set("Body", body);
   };
   options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} {Body} responded {StatusCode} in {Elapsed:0.0000}";
});
Hakan Fıstık
  • 16,800
  • 14
  • 110
  • 131
Alexander
  • 111
  • 1
  • 6
  • 1
    The property does not necessarily be available in the MessageTemplate. It depends on the sink. an will always be available via its Properties. – cmxl Oct 25 '21 at 07:51
  • " your logic to get body" this is the hardest part, this doesn't answer anything – August Jun 15 '23 at 12:42
8

If you can upgrade to dotnet core 6 you dont have to write your own request/response body logging middleware. Microsoft have one in the aspnet framework.

This can be setup to work with serilog, although i found the serilog documentation to be wrong in that the startup code needs both Microsofts UseHttpLogging and Serilogs UseSerilogRequestLogging (Serilog doco says just use theirs ... that didnt write the bodies) Serilog documentation Microsofts new aspnet dotnet 6 logging documentation

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseHsts();
    }

    app
        .UseHttpsRedirection()
        .UseRouting()
        .UseHttpLogging()
        .UseSerilogRequestLogging()
        .UseSwagger()
        .UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Some.api"))
        .UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
}     

To make this complete (as per the doco on the logging from microsoft) you can setup options such as

        services.AddHttpLogging(logging =>
        {
            logging.LoggingFields = HttpLoggingFields.All;
            logging.RequestHeaders.Add(HeaderNames.Accept);
            logging.RequestHeaders.Add(HeaderNames.ContentType);
            logging.RequestHeaders.Add(HeaderNames.ContentDisposition);
            logging.RequestHeaders.Add(HeaderNames.ContentEncoding);
            logging.RequestHeaders.Add(HeaderNames.ContentLength);
            
            logging.MediaTypeOptions.AddText("application/json");
            logging.MediaTypeOptions.AddText("multipart/form-data");
            
            logging.RequestBodyLogLimit = 1024;
            logging.ResponseBodyLogLimit = 1024;
        });

And dont forget appsettings.json for microsoft logging and for serilog. Such as:

  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Information",
      "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
    }
  },
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Information",
        "Microsoft.Hosting.Lifetime": "Information",
        "Microsoft.AspNetCore": "Information",
        "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
      }
    },
    "WriteTo": [
      {
        "Name": "File",
        "Args": { "path":  "c:/data/logs/aspnetcore-log-.txt", "rollingInterval": "Day" }
      }
    ]
  },

And you need to call the UseSerilog extension, such as:

public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseSerilog((context, services, configuration) => configuration
                .ReadFrom.Configuration(context.Configuration)
                .ReadFrom.Services(services)
                .Enrich.FromLogContext()
                .WriteTo.Console())
            .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
Simon Dowdeswell
  • 1,001
  • 11
  • 19
  • 2
    This adds the default Microsoft chatty logger. Yes, you do get the request/response bodies but you also get a lot of other log lines. And the SerilogRequestLogging is created for the exact use case of not having multiple lines per log. So I falsely upvoted this answer, it is not a good answer in my opinion. In conclusion; it adds another logger that logs the same information so you end up with duplicate log information and blown up logs in addition to the request/response bodies. – MariWing Sep 16 '22 at 06:07
  • @MariWng " it is not a good answer in my opinion" .. you are welcome to your opinion, but the answer just is in the context of getting it working, which i found to be difficult due to the problems discussed in the solution". What to do in any given circumstance, be it production or just on your local development machine is not discussed at all. – Simon Dowdeswell Sep 19 '22 at 00:17
6

The above answer by @Alexander really did a great job, but it does not explain how to get the body which is quite hard to do correctly.
So here is the full answer

First, you will need a new middleware

public class ResetTheBodyStreamMiddleware
{
    private readonly RequestDelegate _next;

    public ResetTheBodyStreamMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Still enable buffering before anything reads
        context.Request.EnableBuffering();

        // Call the next delegate/middleware in the pipeline
        await _next(context);

        // Reset the request body stream position to the start so we can read it
        context.Request.Body.Position = 0;
    }
}

Then you will need to register the Middleware THEN register the UseSerilogRequestLogging method.

app.UseMiddleware<ResetTheBodyStreamMiddleware>();

app.UseSerilogRequestLogging(options =>
    options.EnrichDiagnosticContext = async (diagnosticContext, context) =>
    {
        // Reset the request body stream position to the start so we can read it
        context.Request.Body.Position = 0;

        // Leave the body open so the next middleware can read it.
        using StreamReader reader = new(
            context.Request.Body,
            encoding: Encoding.UTF8,
            detectEncodingFromByteOrderMarks: false);

        string body = await reader.ReadToEndAsync();

        if (body.Length is 0)
            return;

        object? obj = JsonSerializer.Deserialize<object>(body);
        if (obj is null)
            return;

        diagnosticContext.Set("Body", obj);
        options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} {Body} responded {StatusCode} in {Elapsed:0.0000}";
    }
);
Hakan Fıstık
  • 16,800
  • 14
  • 110
  • 131
  • I'm not sure this is "correct" because `EnrichDiagnosticContext` returns void not Task, so it should not be async. – Lee Grissom May 30 '22 at 21:32
  • @LeeGrissom I will investigate that deeper, but I can say now that the async/await part is not important to the answer, so everyone should be free to remove it, the idea of my answer is how to get the body, this is the important part in the answer – Hakan Fıstık May 31 '22 at 02:07
  • Thank you @HakanFıstık. the key is to use a middleware to keep the Request in the buffer to read it later. – Mahdi Jun 15 '22 at 06:44
4

UPD 2022 .Net 6+ ASP.NET Core supports request/response logging "from the box" docs here TL;DR: add a service configuration for Http Logging and use Http Logging middleware:

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.RequestBody | HttpLoggingFields.ResponseBody;
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;
});
/** Other Service configurations **/
app.UseHttpLogging();
Serg.ID
  • 1,604
  • 1
  • 21
  • 25
  • 2
    How do you get this to work with Serilog though? – BlabzX Mar 07 '23 at 23:04
  • @BlabzX I'm sorry, I'm not sure that I got your question right. You still need to register Serlog as your preferred logger with something like: var logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) .Enrich.FromLogContext() .CreateLogger(); builder.Logging.AddSerilog(logger); – Serg.ID Mar 09 '23 at 01:19
3

Please check this thread and this article. To log the request and response information (such as: request body), you could create a Middleware, and capture the request and response body (because they are streams, you have to read them first, then using the Serilogs method to log it).

Code as below:

public class SerilogRequestLogger
{
    readonly RequestDelegate _next;

    public SerilogRequestLogger(RequestDelegate next)
    {
        if (next == null) throw new ArgumentNullException(nameof(next));
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));

        // Push the user name into the log context so that it is included in all log entries
        LogContext.PushProperty("UserName", httpContext.User.Identity.Name);

        // Getting the request body is a little tricky because it's a stream
        // So, we need to read the stream and then rewind it back to the beginning
        string requestBody = "";
        HttpRequestRewindExtensions.EnableBuffering(httpContext.Request);
        Stream body = httpContext.Request.Body;
        byte[] buffer = new byte[Convert.ToInt32(httpContext.Request.ContentLength)];
        await httpContext.Request.Body.ReadAsync(buffer, 0, buffer.Length);
        requestBody = Encoding.UTF8.GetString(buffer);
        body.Seek(0, SeekOrigin.Begin);
        httpContext.Request.Body = body;

        Log.ForContext("RequestHeaders", httpContext.Request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString()), destructureObjects: true)
           .ForContext("RequestBody", requestBody)
           .Debug("Request information {RequestMethod} {RequestPath} information", httpContext.Request.Method, httpContext.Request.Path);

        Log.Information(string.Format("Request Body: {0} ", requestBody));
        // The reponse body is also a stream so we need to:
        // - hold a reference to the original response body stream
        // - re-point the response body to a new memory stream
        // - read the response body after the request is handled into our memory stream
        // - copy the response in the memory stream out to the original response stream
        using (var responseBodyMemoryStream = new MemoryStream())
        {
            var originalResponseBodyReference = httpContext.Response.Body;
            httpContext.Response.Body = responseBodyMemoryStream;

            await _next(httpContext);

            httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
            var responseBody = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
            httpContext.Response.Body.Seek(0, SeekOrigin.Begin);

            Log.ForContext("RequestBody", requestBody)
               .ForContext("ResponseBody", responseBody)
               .Debug("Response information {RequestMethod} {RequestPath} {statusCode}", httpContext.Request.Method, httpContext.Request.Path, httpContext.Response.StatusCode);

            await responseBodyMemoryStream.CopyToAsync(originalResponseBodyReference);
        }
    }
}

Register the Middleware:

     app.UseMiddleware<SerilogRequestLogger>();

Reference: using Serilog logging for ASP.NET Core.

Zhi Lv
  • 18,845
  • 1
  • 19
  • 30
  • 2
    Actually I want to add the request body to the existing log, The log which includes which method called who called, etc. This method adds as a new record but it works. Thank you, Zhi. – OguzKaanAkyalcin Sep 04 '20 at 07:26
1

From my understanding, you want to add the HttpRequest Body to your log.
Something like this should help get you started as long as its within a controller with an async method, if you dont have access to the HttpRequest you could add one with DI services.addhttpcontextaccessor() in your startup file

// Payload.
string payload = string.Empty;

// Create StreamReader And Starting Reading The Request Body.
using (StreamReader streamReader = new StreamReader(this.Request.Body, Encoding.UTF8, true, 1024, true))
{
    // Assign The Stream Content To The Payload Object
    payload = await streamReader.ReadToEndAsync();
}

// Check If The Payload Has Something.
if (!string.IsEmptyOrNull(payload))
{
    // LOG INFO HERE
}
traveler3468
  • 1,557
  • 2
  • 15
  • 28
1

I solved the same problem by implementing this wrapper.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System;
using System.IO;
using System.Threading.Tasks;

namespace Serilog
{
    [Flags]
    public enum HttpSessionInfoToLog
    {
        All = -1,
        None = 0,
        QueryString = 1,
        Request = RequestHeaders | RequestBody | QueryString,
        RequestHeaders = 64,
        RequestBody = 128,
        Response = ResponseHeaders | ResponseBody,
        ResponseHeaders = 2048,
        ResponseBody = 4096,
    }

    public static class SerilogHttpSessionsLoggingMiddleware
    {
        /// <summary>
        /// Allows to log information about the http sessions processed.
        /// </summary>
        /// <param name="app">Application builder istance.</param>
        /// <param name="settings">Enum flags that defines what extra information should be logged.</param>
        /// <example>
        /// public class Startup
        /// {
        ///   public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        ///   {
        ///     app.UseHttpsRedirection();
        ///     app.UseStaticFiles();
        ///     app.UseSerilogHttpSessionsLogging(HttpSessionInfoToLog.All);
        ///     ...
        ///   }
        /// }
        /// </example>
        public static void UseSerilogHttpSessionsLogging(this IApplicationBuilder app, HttpSessionInfoToLog settings = HttpSessionInfoToLog.None)
        {
            if (settings.HasFlag(HttpSessionInfoToLog.RequestBody) || settings.HasFlag(HttpSessionInfoToLog.ResponseBody))
                app.Use(async (context, next) =>
                {
                    if (settings.HasFlag(HttpSessionInfoToLog.RequestBody))
                        context.Request.EnableBuffering();

                    if (settings.HasFlag(HttpSessionInfoToLog.ResponseBody))
                    {
                        var originalRespBody = context.Response.Body;
                        using var newResponseBody = new MemoryStream();
                        context.Response.Body = newResponseBody;    //to capture it's value in-flight.
                        await next.Invoke();
                        newResponseBody.Position = 0;
                        await newResponseBody.CopyToAsync(originalRespBody);
                        context.Response.Body = originalRespBody;
                    }
                });

            app.UseSerilogRequestLogging(x => x.EnrichDiagnosticContext = async (ctxDiag, ctxHttp)
                => await LogEnrichment(ctxDiag, ctxHttp, settings));
        }

        static async Task LogEnrichment(IDiagnosticContext ctxDiag, HttpContext ctxHttp, HttpSessionInfoToLog settings)
        {
            try
            {
                const string headersSeparator = ", ";
                if (settings.HasFlag(HttpSessionInfoToLog.QueryString))
                    ctxDiag.Set("QueryString", ctxHttp.Request.QueryString);

                if (settings.HasFlag(HttpSessionInfoToLog.RequestHeaders))
                    ctxDiag.Set("RequestHeaders", string.Join(headersSeparator, ctxHttp.Request.Headers));

                if (settings.HasFlag(HttpSessionInfoToLog.ResponseHeaders))
                    ctxDiag.Set("ResponseHeaders", string.Join(headersSeparator, ctxHttp.Response.Headers));

                if (settings.HasFlag(HttpSessionInfoToLog.RequestBody))
                {
                    ctxHttp.Request.EnableBuffering();
                    ctxDiag.Set("RequestBody", await ReadStream(ctxHttp.Request.Body), false);
                }

                if (settings.HasFlag(HttpSessionInfoToLog.ResponseBody))
                    ctxDiag.Set("ResponseBody", await ReadStream(ctxHttp.Response.Body), false);
            }
            catch (Exception ex)
            {
                ctxDiag.SetException(ex);
            }
        }

        static async Task<string> ReadStream(Stream stream)
        {
            stream.Position = 0;
            using var reader = new StreamReader(stream, leaveOpen: true);
            var requestBodyText = await reader.ReadToEndAsync();
            stream.Position = 0;
            return requestBodyText;
        }
    }
}
jangix
  • 49
  • 7
0

I have stumbled on this issue, but @Hakan Fıstık answer is not correct, his solution did not work.

The way it works for me is:

        app.UseMiddleware<ResetTheBodyStreamMiddleware>();
        app.UseSerilogRequestLogging(options =>
        {
            options.EnrichDiagnosticContext = (diagnosticContext, context) =>
            {
                // Reset the request body stream position to the start so we can read it
                context.Request.Body.Position = 0;
                string body = string.Empty;
                // Leave the body open so the next middleware can read it.
                StreamReader reader = new StreamReader(
                    context.Request.Body,
                    encoding: Encoding.UTF8,
                    detectEncodingFromByteOrderMarks: false);
                body = reader.ReadToEndAsync().GetAwaiter().GetResult();

                if (string.IsNullOrEmpty(body))
                    return;

                diagnosticContext.Set("Body", body);
            };
            options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} {Body} responded {StatusCode} in {Elapsed:0.0000}";
        });
Tomáš Filip
  • 727
  • 3
  • 6
  • 23