View POST request body in Application Insights

You can simply implement your own Telemetry Initializer:

For example, below an implementation that extracts the payload and adds it as a custom dimension of the request telemetry:

public class RequestBodyInitializer : ITelemetryInitializer
{
    public void Initialize(ITelemetry telemetry)
    {
        var requestTelemetry = telemetry as RequestTelemetry;
        if (requestTelemetry != null && (requestTelemetry.HttpMethod == HttpMethod.Post.ToString() || requestTelemetry.HttpMethod == HttpMethod.Put.ToString()))
        {
            using (var reader = new StreamReader(HttpContext.Current.Request.InputStream))
            {
                string requestBody = reader.ReadToEnd();
                requestTelemetry.Properties.Add("body", requestBody);
            }
        }
    }
}

Then add it to the configuration either by configuration file or via code:

TelemetryConfiguration.Active.TelemetryInitializers.Add(new RequestBodyInitializer());

Then query it in Analytics:

requests | limit 1 | project customDimensions.body

The solution provided by @yonisha is in my opinion the cleanest one available. However you still need to get your HttpContext in there and for that you need some more code. I have also inserted some comments which are based or taken from code examples above. It is important to reset the position of your request else you will lose its data.

This is my solution that I have tested and gives me the jsonbody:

public class RequestBodyInitializer : ITelemetryInitializer
{
    readonly IHttpContextAccessor httpContextAccessor;

    public RequestBodyInitializer(IHttpContextAccessor httpContextAccessor)
    {
        this.httpContextAccessor = httpContextAccessor;
    }

    public void Initialize(ITelemetry telemetry)
    {
        if (telemetry is RequestTelemetry requestTelemetry)
        {
            if ((httpContextAccessor.HttpContext.Request.Method == HttpMethods.Post ||
                 httpContextAccessor.HttpContext.Request.Method == HttpMethods.Put) &&
                httpContextAccessor.HttpContext.Request.Body.CanRead)
            {
                const string jsonBody = "JsonBody";

                if (requestTelemetry.Properties.ContainsKey(jsonBody))
                {
                    return;
                }

                //Allows re-usage of the stream
                httpContextAccessor.HttpContext.Request.EnableRewind();

                var stream = new StreamReader(httpContextAccessor.HttpContext.Request.Body);
                var body = stream.ReadToEnd();

                //Reset the stream so data is not lost
                httpContextAccessor.HttpContext.Request.Body.Position = 0;
                requestTelemetry.Properties.Add(jsonBody, body);
            }
        }
    }

Then also be sure to add this to your Startup -> ConfigureServices

services.AddSingleton<ITelemetryInitializer, RequestBodyInitializer>();

EDIT:

If you also want to get the response body I found it useful to create a piece of middleware (.NET Core, not sure about Framework). At first I took above approach where you log a response and a request but most of the time you want these together:

    public async Task Invoke(HttpContext context)
    {
        var reqBody = await this.GetRequestBodyForTelemetry(context.Request);

        var respBody = await this.GetResponseBodyForTelemetry(context);
        this.SendDataToTelemetryLog(reqBody, respBody, context);
    }

This awaits both a request and a response. GetRequestBodyForTelemetry is almost identical to the code from the telemetry initializer, except using Task. For the response body I have used the code below, I also excluded a 204 since that leads to a nullref:

public async Task<string> GetResponseBodyForTelemetry(HttpContext context)
{
    var originalBody = context.Response.Body;

    try
    {
        using (var memStream = new MemoryStream())
        {
            context.Response.Body = memStream;

            //await the responsebody
            await next(context);
            if (context.Response.StatusCode == 204)
            {
                return null;
            }

            memStream.Position = 0;
            var responseBody = new StreamReader(memStream).ReadToEnd();

            //make sure to reset the position so the actual body is still available for the client
            memStream.Position = 0;
            await memStream.CopyToAsync(originalBody);

            return responseBody;
        }
    }
    finally
    {
        context.Response.Body = originalBody;
    }
}

Few days back, I got a similar requirement to log the request Body in Application insights with filtering out sensitive input user data from the payload. So sharing my solution. The below solution is developed for ASP.NET Core 2.0 Web API.

ActionFilterAttribute

I've used ActionFilterAttribute from (Microsoft.AspNetCore.Mvc.Filters namespace) which provides the Model via ActionArgument so that by reflection, those properties can be extracted which are marked as sensitive.

public class LogActionFilterAttribute : ActionFilterAttribute
{
    private readonly IHttpContextAccessor httpContextAccessor;

    public LogActionFilterAttribute(IHttpContextAccessor httpContextAccessor)
    {
        this.httpContextAccessor = httpContextAccessor;
    }

    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        if (context.HttpContext.Request.Method == HttpMethods.Post || context.HttpContext.Request.Method == HttpMethods.Put)
        {
            // Check parameter those are marked for not to log.
            var methodInfo = ((Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor)context.ActionDescriptor).MethodInfo;
            var noLogParameters = methodInfo.GetParameters().Where(p => p.GetCustomAttributes(true).Any(t => t.GetType() == typeof(NoLogAttribute))).Select(p => p.Name);

            StringBuilder logBuilder = new StringBuilder();

            foreach (var argument in context.ActionArguments.Where(a => !noLogParameters.Contains(a.Key)))
            {
                var serializedModel = JsonConvert.SerializeObject(argument.Value, new JsonSerializerSettings() { ContractResolver = new NoPIILogContractResolver() });
                logBuilder.AppendLine($"key: {argument.Key}; value : {serializedModel}");
            }

            var telemetry = this.httpContextAccessor.HttpContext.Items["Telemetry"] as Microsoft.ApplicationInsights.DataContracts.RequestTelemetry;
            if (telemetry != null)
            {
                telemetry.Context.GlobalProperties.Add("jsonBody", logBuilder.ToString());
            }

        }

        await next();
    }
}

The 'LogActionFilterAttribute' is injected in MVC pipeline as Filter.

 services.AddMvc(options =>
 {
       options.Filters.Add<LogActionFilterAttribute>();
 });

NoLogAttribute

In above code, NoLogAttribute attribute is used which should be applied on Model/Model's Properties or method parameter to indicate that value should not be logged.

public class NoLogAttribute : Attribute
{
}

NoPIILogContractResolver

Also, NoPIILogContractResolver is used in JsonSerializerSettings during serialization process

internal class NoPIILogContractResolver : DefaultContractResolver
{
    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var properties = new List<JsonProperty>();

        if (!type.GetCustomAttributes(true).Any(t => t.GetType() == typeof(NoLogAttribute)))
        {
            IList<JsonProperty> retval = base.CreateProperties(type, memberSerialization);
            var excludedProperties = type.GetProperties().Where(p => p.GetCustomAttributes(true).Any(t => t.GetType() == typeof(NoLogAttribute))).Select(s => s.Name);
            foreach (var property in retval)
            {
                if (excludedProperties.Contains(property.PropertyName))
                {
                    property.PropertyType = typeof(string);
                    property.ValueProvider = new PIIValueProvider("PII Data");
                }

                properties.Add(property);
            }
        }

        return properties;
    }
}

internal class PIIValueProvider : IValueProvider
{
    private object defaultValue;

    public PIIValueProvider(string defaultValue)
    {
        this.defaultValue = defaultValue;
    }

    public object GetValue(object target)
    {
        return this.defaultValue;
    }

    public void SetValue(object target, object value)
    {

    }
}

PIITelemetryInitializer

To inject the RequestTelemetry object, I've to use ITelemetryInitializer so that RequestTelemetry can be retrieved in LogActionFilterAttribute class.

public class PIITelemetryInitializer : ITelemetryInitializer
{
    IHttpContextAccessor httpContextAccessor;

    public PIITelemetryInitializer(IHttpContextAccessor httpContextAccessor)
    {
        this.httpContextAccessor = httpContextAccessor;
    }

    public void Initialize(ITelemetry telemetry)
    {
        if (this.httpContextAccessor.HttpContext != null)
        {
            if (telemetry is Microsoft.ApplicationInsights.DataContracts.RequestTelemetry)
            {
                this.httpContextAccessor.HttpContext.Items.TryAdd("Telemetry", telemetry);
            }
        }
    }
}

The PIITelemetryInitializer is registered as

services.AddSingleton<ITelemetryInitializer, PIITelemetryInitializer>();

Testing feature

Following code demonstrates the usage of above code

Created a controller

[Route("api/[controller]")]
public class ValuesController : Controller
{
    private readonly ILogger _logger;

    public ValuesController(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<ValuesController>();
    }

    // POST api/values
    [HttpPost]
    public void Post([FromBody, NoLog]string value)
    {

    }

    [HttpPost]
    [Route("user")]
    public void AddUser(string id, [FromBody]User user)
    {

    }
}

Where User Model is defined as

public class User
{
    [NoLog]
    public string Id { get; set; }

    public string Name { get; set; }

    public DateTime AnneviseryDate { get; set; }

    [NoLog]
    public int LinkId { get; set; }

    public List<Address> Addresses { get; set; }
}

public class Address
{
    public string AddressLine { get; set; }

    [NoLog]
    public string City { get; set; }

    [NoLog]
    public string Country { get; set; }
}

So when API is invoked by Swagger tool

enter image description here

The jsonBody is logged in Request without sensitive data. All sensitive data is replaced by 'PII Data' string literal.

enter image description here


Update: I have put the logic below into a ready-to-use NuGet package. You can find more about the package here and about the topic itself here.


I choose the custom middleware path as it made things easier with HttpContext already being there.

public class RequestBodyLoggingMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var method = context.Request.Method;

        // Ensure the request body can be read multiple times
        context.Request.EnableBuffering();

        // Only if we are dealing with POST or PUT, GET and others shouldn't have a body
        if (context.Request.Body.CanRead && (method == HttpMethods.Post || method == HttpMethods.Put))
        {
            // Leave stream open so next middleware can read it
            using var reader = new StreamReader(
                context.Request.Body,
                Encoding.UTF8,
                detectEncodingFromByteOrderMarks: false,
                bufferSize: 512, leaveOpen: true);

            var requestBody = await reader.ReadToEndAsync();

            // Reset stream position, so next middleware can read it
            context.Request.Body.Position = 0;

            // Write request body to App Insights
            var requestTelemetry = context.Features.Get<RequestTelemetry>();                              
            requestTelemetry?.Properties.Add("RequestBody", requestBody);
        }

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

And this is how I log the response body

public class ResponseBodyLoggingMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var originalBodyStream = context.Response.Body;

        try
        {
            // Swap out stream with one that is buffered and suports seeking
            using var memoryStream = new MemoryStream();
            context.Response.Body = memoryStream;

            // hand over to the next middleware and wait for the call to return
            await next(context);

            // Read response body from memory stream
            memoryStream.Position = 0;
            var reader = new StreamReader(memoryStream);
            var responseBody = await reader.ReadToEndAsync();

            // Copy body back to so its available to the user agent
            memoryStream.Position = 0;
            await memoryStream.CopyToAsync(originalBodyStream);

            // Write response body to App Insights
            var requestTelemetry = context.Features.Get<RequestTelemetry>();
            requestTelemetry?.Properties.Add("ResponseBody", responseBody);
        }
        finally
        {
            context.Response.Body = originalBodyStream;
        }
    }
}

Than add an extension method...

public static class ApplicationInsightExtensions
{
    public static IApplicationBuilder UseRequestBodyLogging(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestBodyLoggingMiddleware>();
    }

    public static IApplicationBuilder UseResponseBodyLogging(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ResponseBodyLoggingMiddleware>();
    }            
}

...that allows for a clean integration inside Startup.cs

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        
        // Enable our custom middleware
        app.UseRequestBodyLogging();
        app.UseResponseBodyLogging();
    }
    
    // ...
}

Don't forget to register the custom middleware components inside ConfigureServices()

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddApplicationInsightsTelemetry(Configuration["APPINSIGHTS_CONNECTIONSTRING"]);
            
    services.AddTransient<RequestBodyLoggingMiddleware>();
    services.AddTransient<ResponseBodyLoggingMiddleware>();
}

In Asp.Net core it looks like we dont have to use ITelemetryInitializer. We can use the middleware to log the requests to application insights. Thanks to @IanKemp https://github.com/microsoft/ApplicationInsights-aspnetcore/issues/686

 public async Task Invoke(HttpContext httpContext)
    {
        var requestTelemetry = httpContext.Features.Get<RequestTelemetry>();

        //Handle Request 
        var request = httpContext.Request;
        if (request?.Body?.CanRead == true)
        {
            request.EnableBuffering();

            var bodySize = (int)(request.ContentLength ?? request.Body.Length);
            if (bodySize > 0)
            {
                request.Body.Position = 0;

                byte[] body;

                using (var ms = new MemoryStream(bodySize))
                {
                    await request.Body.CopyToAsync(ms);

                    body = ms.ToArray();
                }

                request.Body.Position = 0;

                if (requestTelemetry != null)
                {
                    var requestBodyString = Encoding.UTF8.GetString(body);

                    requestTelemetry.Properties.Add("RequestBody", requestBodyString);
                }
            }
        }

        await _next(httpContext); // calling next middleware
    }