I Have an API with formatted response look's like this following JSON:
{
"statusCode": 200,
"totalRecord": 2,
"message": "Succesfully get merchants",
"data": [
{
"id": 1,
"name": "Leo Shop",
"address": "Flower Street 9A",
"isComplete": false,
"createdAt": "2021-05-30T14:16:27.654233",
"updatedAt": "2021-05-30T14:16:28.515476"
},
{
"id": 3,
"name": "Test Shop",
"address": "Playing Street 12A",
"isComplete": false,
"createdAt": "2021-05-30T14:16:27.654233",
"updatedAt": "2021-05-30T14:16:28.515476"
}
]
}
And code behind those response look's like this following code :
[HttpGet]
public async Task<ActionResult<IEnumerable<Merchant>>> GetMerchants()
{
var data = await _context.Merchants.ToListAsync();
ApiResponse res = new ApiResponse{StatusCode = 200, Message = "Succesfully get merchants", TotalRecord = data.Count, Data = data}; // Focus on this line
return Ok(res);
}
My question, how to automatically convert default response As ApiResponse model without repeating to write new ApiResponse() model on every action return inside every controller?
Hopefully anyone can help me.. Thanks in advance guys
You can use Result Filter in MVC Filter pipeline
MVC Filter pipeline
So change your controller looks like this
[HttpGet]
public Task<List<Merchant>> GetMerchants()
{
return _context.Merchants.ToListAsync();
}
Add new class, called ResultManipulator.cs
public class ResultManipulator : IResultFilter
{
public void OnResultExecuted(ResultExecutedContext context)
{
// do nothing
}
public void OnResultExecuting(ResultExecutingContext context)
{
//run code immediately before and after the execution of action results. They run only when the action method has executed successfully. They are useful for logic that must surround view or formatter execution.
var result = context.Result as ObjectResult;
var resultObj = result.Value;
//change this ResultApi with your ApiResponse class
var resp = new ResultApi
{
Path = context.HttpContext.Request.Path.HasValue ? context.HttpContext.Request.Path.Value : "",
Method = context.HttpContext.Request.Method
};
if (resultObj is not null && resultObj is not Unit)
resp.Payload = resultObj;
//you can also change this from System.Text.Json to newtonsoft if you use newtonsoft
context.Result = new JsonResult(resp, new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
});
}
}
And dont forget to add ResultManipulator class in Startup
services.AddControllers(options =>
{
options.Filters.Add(new ResultManipulator());
})
I hope it solve your problem
Try this as a middleware solution. It will transform the response.
public class YourResponseMiddleware
{
private readonly RequestDelegate _next;
public YourResponseMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
var existingBody = context.Response.Body;
using (var newBody = new MemoryStream())
{
context.Response.Body = newBody;
await _next(context);
var newResponse = await FormatResponse(context.Response);
context.Response.Body = new MemoryStream();
newBody.Seek(0, SeekOrigin.Begin);
context.Response.Body = existingBody;
var newContent = new StreamReader(newBody).ReadToEnd();
// Send modified content to the response body.
//
await context.Response.WriteAsync(newResponse);
}
}
private async Task<string> FormatResponse(HttpResponse response)
{
//We need to read the response stream from the beginning...and copy it into a string...I'D LIKE TO SEE A BETTER WAY TO DO THIS
//
response.Body.Seek(0, SeekOrigin.Begin);
var content= await new StreamReader(response.Body).ReadToEndAsync();
var yourResponse = new YourResponse (); // CREATE THIS CLASS
yourResponse.StatusCode = response.StatusCode;
if(!IsResponseValid(response))
{
yourResponse.ErrorMessage = content;
}
else
{
yourResponse.Content = content;
}
yourResponse.Size = response.ToString().Length;
var json = JsonConvert.SerializeObject(yourResponse );
//We need to reset the reader for the response so that the client an read it
response.Body.Seek(0, SeekOrigin.Begin);
return $"{json}";
}
private bool IsResponseValid(HttpResponse response)
{
if ((response != null)
&& (response.StatusCode == 200
|| response.StatusCode == 201
|| response.StatusCode == 202))
{
return true;
}
return false;
}
}
public static class ResponseMiddleware
{
public static void UseYourResponseMiddleware(this IApplicationBuilder app)
{
app.UseMiddleware<YourResponseMiddleware>();
}
}
In Startup.cs (Configure())
app.UseYourResponseMiddleware();
When .Net web API returns Badrequest("this is an example of Badrequest".), IOwinContext object in my middleware only contains context.Response.StatusCode 400 and context.Response.ReasonPhrase as "Bad Request". I want actual error message so that I can log it somewhere. Is it possible to get actual error message from IOwinContext without writing any custom class?
EDIT:
Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
ICoreLogger dv_logger = new CoreLogger();
app.Use<InvalidAuthenticationMiddleware>(dv_logger);
ConfigureOAuth(app);
//register log4net
XmlConfigurator.Configure();
// configure log4net variables
GlobalContext.Properties["processId"] = "dv_logger";
System.Web.Http.GlobalConfiguration.Configure(WebApiConfig.Register);
RouteConfig.RegisterRoutes(RouteTable.Routes);
//entity framework
DbContext.Intialize();
AuthContext.Intialize();
.....
}
Middleware
public class InvalidAuthenticationMiddleware : OwinMiddleware
{
public override async Task Invoke(IOwinContext context)
{
var stream = context.Response.Body;
using (var buffer = new MemoryStream())
{
context.Response.Body = buffer;
await Next.Invoke(context);
buffer.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(buffer))
{
string responseBody = await reader.ReadToEndAsync();
if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest)
{
var definition = new { Message = "" };
var error = JsonConvert.DeserializeAnonymousType(responseBody, definition);
Debug.WriteLine(error.Message);
}
buffer.Seek(0, SeekOrigin.Begin);
await buffer.CopyToAsync(stream);
}
}
}
}
Controller
public class LoginController : BaseController
{
[Route("")]
public IHttpActionResult Get(string email)
{
return BadRequest("this is an example of bad request!");
}
}
You will have to read the response's body. BadRequest("this is an example of BadRequest") sets an object in the body of the response with a property Message containing that message. I am assuming you are using JSON as a serialization format:
{
"Message": "this is an example of BadRequest"
}
Here is the middleware code to log the error message when the response is a BadRequest:
public override async Task Invoke(IOwinContext context)
{
var stream = context.Response.Body;
using (var buffer = new MemoryStream())
{
context.Response.Body = buffer;
await next.Invoke();
buffer.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(buffer))
{
string responseBody = await reader.ReadToEndAsync();
if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest)
{
var definition = new { Message = "" };
var error = JsonConvert.DeserializeAnonymousType(responseBody, definition);
Console.WriteLine(error.Message);
}
buffer.Seek(0, SeekOrigin.Begin);
await buffer.CopyToAsync(stream);
}
}
}
Note that I used an anonymous type to avoid having to declare a class for the error message object.
have followed through some samples and creating a new buffer MemoryStream to replace the Response.Body before calling next().
Below is my middleware :
public class TrackingMiddleware
{
private readonly AppFunc next;
public TrackingMiddleware(AppFunc next)
{
this.next = next;
}
public async Task Invoke(IDictionary<string, object> env)
{
IOwinContext context = new OwinContext(env);
// Buffer the response
var stream = context.Response.Body;
var buffer = new MemoryStream();
context.Response.Body = buffer;
await this.next(env);
buffer.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(buffer);
string responseBody = await reader.ReadToEndAsync();
buffer.Seek(0, SeekOrigin.Begin);
await buffer.CopyToAsync(stream);
}
}
the responsBody is always empty, event my ApiController's Action returned List of data.
Below is my Owin Startup class (Did i miss out anything ? )
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
static Startup()
{
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/token"),
Provider = new OAuthAppProvider(),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(2),
AllowInsecureHttp = true
};
}
public void ConfigureAuth(IAppBuilder app)
{
app.UseOAuthBearerTokens(OAuthOptions);
app.Use<TrackingMiddleware>();
}
}
Below is my ApiController
public class TestController : ApiController
{
public IHttpActionResult Get()
{
return Json(new string[] {"123", "asdfsdf"});
}
}
and My Web api configuration is registered through Global.asax.cs
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
}
}
Is this the reason that messed up the sequence of the middleware?
Alternatively, if i use MVC method and it works with the HttpContext.Current.Response by referencing following sample
You have to put the original stream back into the response
context.Response.Body = stream;
That way it can be read by the previous middleware in the pipeline
You would also want to dispose of any resources that are no longer needed
public async Task Invoke(IDictionary<string, object> env) {
IOwinContext context = new OwinContext(env);
// Buffer the response
var stream = context.Response.Body;
using (var buffer = new MemoryStream()) {
context.Response.Body = buffer;
//call next in pipeline
await this.next(env);
//reset the original response body
context.Response.Body = stream;
//get data from buffer
buffer.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(buffer);
string responseBody = await reader.ReadToEndAsync();
buffer.Seek(0, SeekOrigin.Begin);
//put data into original stream to continue the flow.
await buffer.CopyToAsync(stream);
}
}
TL;DR Question: Is there a way to get the request body into an existing ExceptionTelemetry instance, in ASP.NET Core, without copying ALL request bodies?
I would like to be able to include the Request Body in the exception telemetry for application insights. I.e. I only want the request when an exception has occurred.
Browsing around for documentation on both ASP.NET Core and Application Insights, it seems the "right" way to enrich telemetry is using TelemetryProcessors or TelemetryInitializers, so I tried getting the request body in a custom telemetryinitializer, only to discover that the request body stream is closed/disposed when I want to read it (rewinding does not help because apparently it has already been disposed when the App Insights telemetryinitializer is being executed).
I ended up solving it by having a middleware that copies the request stream:
public async Task Invoke(HttpContext context)
{
var stream = context.Request.Body;
try
{
using (var buffer = new MemoryStream())
{
// Copy the request stream and rewind the copy
await stream.CopyToAsync(buffer);
buffer.Position = 0L;
// Create another copy and rewind both
var otherBuffer = new MemoryStream();
await buffer.CopyToAsync(otherBuffer);
buffer.Position = 0L;
otherBuffer.Position = 0L;
// Replace the request stream by the first copy
context.Request.Body = buffer;
// Put a separate copy in items collection for other things to use
context.Items["RequestStreamCopy"] = otherBuffer;
context.Response.RegisterForDispose(otherBuffer);
await next(context);
}
}
finally
{
context.Request.Body = stream;
}
}
And my initializer:
public AiExceptionInitializer(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException("httpContextAccessor");
}
public void Initialize(ITelemetry telemetry)
{
var context = this.httpContextAccessor.HttpContext;
if (context == null)
{
return;
}
lock (context)
{
var request = context.Features.Get<RequestTelemetry>();
if (request == null)
{
return;
}
this.OnInitializeTelemetry(context, request, telemetry);
}
}
protected void OnInitializeTelemetry(HttpContext platformContext, RequestTelemetry requestTelemetry, ITelemetry telemetry)
{
if (telemetry is ExceptionTelemetry exceptionTelemetry)
{
var stream = platformContext.Items["RequestStreamCopy"] as MemoryStream;
try
{
if (stream?.Length <= 0)
{
return;
}
// Rewind the stream position just to be on the safe side
stream.Position = 0L;
using (var reader = new StreamReader(stream, Encoding.UTF8, true, 1024, true))
{
string requestBody = reader.ReadToEnd();
exceptionTelemetry.Properties.Add("HttpRequestBody", requestBody);
}
}
finally
{
if (stream != null)
{
// Rewind the stream for others to use.
stream.Position = 0L;
}
}
}
}
However this having to copy the request stream (TWICE) for each request, to only have it used on failures seems rather inefficient to me.
So I am wondering if there is any other way to do something like this where I don't have to copy the stream of each and every request just to serialize the ones failing?
I am aware I could "just" write a middleware that would create new ExceptionTelemetry instances, but as far as I know (I might be wrong) it would leave me with two Exception instances in Application Insights (i.e. the one generated by me and the one generated by the AI extensions), instead of just one exception with the added property I need.
Thanks to the comment from #DmitryMatveev I found an alternate solution. I am not sure its the most effective, but it is better than what i had!
The middleware is "reduced" to only tracking exceptions, and then serialising the body right away (you might still have a stream copy, but I don't need it in my case), something like the following:
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
public class ExceptionBodyTrackingMiddleware
{
public const string ExceptionRequestBodyKey = "ExceptionRequestBody";
private readonly RequestDelegate next;
public ExceptionBodyTrackingMiddleware(RequestDelegate next)
{
this.next = next ?? throw new ArgumentNullException(nameof(next));
}
public async Task Invoke(HttpContext context)
{
try
{
context.Request.EnableRewind();
await this.next.Invoke(context);
}
catch (Exception)
{
RegisterRequestBody(context);
throw;
}
}
private static void RegisterRequestBody(HttpContext context)
{
if (context.Request.Body?.CanSeek == false)
{
return;
}
var body = CopyStreamToString(context.Request.Body);
context.Items[ExceptionRequestBodyKey] = body;
}
private static string CopyStreamToString(Stream stream)
{
var originalPosition = stream.Position;
RewindStream(stream);
string requestBody = null;
using (var reader = new StreamReader(stream, Encoding.UTF8, true, 1024, true))
{
requestBody = reader.ReadToEnd();
}
stream.Position = originalPosition;
return requestBody;
}
private static void RewindStream(Stream stream)
{
if (stream != null)
{
stream.Position = 0L;
}
}
}
Likewise the Initializer becomes a whole lot simpler:
public AiExceptionInitializer(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException("httpContextAccessor");
}
public void Initialize(ITelemetry telemetry)
{
var context = this.httpContextAccessor.HttpContext;
if (context == null)
{
return;
}
lock (context)
{
var request = context.Features.Get<RequestTelemetry>();
if (request == null)
{
return;
}
this.OnInitializeTelemetry(context, request, telemetry);
}
}
protected void OnInitializeTelemetry(HttpContext platformContext, RequestTelemetry requestTelemetry, ITelemetry telemetry)
{
if (telemetry is ExceptionTelemetry exceptionTelemetry)
{
var requestBody = platformContext.Items[ExceptionBodyTrackingMiddleware.ExceptionRequestBodyKey] as string;
exceptionTelemetry.Properties.Add("HttpRequestBody", requestBody);
}
}
Is it possible to view POST request body in Application Insights?
I can see request details, but not the payload being posted in application insights. Do I have to track this with some coding?
I am building a MVC core 1.1 Web Api.
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
The jsonBody is logged in Request without sensitive data. All sensitive data is replaced by 'PII Data' string literal.
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>();
}
I never got #yonisha's answer working so I used a DelegatingHandler instead:
public class MessageTracingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Trace the request
await TraceRequest(request);
// Execute the request
var response = await base.SendAsync(request, cancellationToken);
// Trace the response
await TraceResponse(response);
return response;
}
private async Task TraceRequest(HttpRequestMessage request)
{
try
{
var requestTelemetry = HttpContext.Current?.GetRequestTelemetry();
var requestTraceInfo = request.Content != null ? await request.Content.ReadAsByteArrayAsync() : null;
var body = requestTraceInfo.ToString();
if (!string.IsNullOrWhiteSpace(body) && requestTelemetry != null)
{
requestTelemetry.Properties.Add("Request Body", body);
}
}
catch (Exception exception)
{
// Log exception
}
}
private async Task TraceResponse(HttpResponseMessage response)
{
try
{
var requestTelemetry = HttpContext.Current?.GetRequestTelemetry();
var responseTraceInfo = response.Content != null ? await response.Content.ReadAsByteArrayAsync() : null;
var body = responseTraceInfo.ToString();
if (!string.IsNullOrWhiteSpace(body) && requestTelemetry != null)
{
requestTelemetry.Properties.Add("Response Body", body);
}
}
catch (Exception exception)
{
// Log exception
}
}
}
.GetRequestTelemetry() is an extension method from Microsoft.ApplicationInsights.Web.
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
}
I implemented a middleware for this,
Invoke method does,
if (context.Request.Method == "POST" || context.Request.Method == "PUT")
{
var bodyStr = GetRequestBody(context);
var telemetryClient = new TelemetryClient();
var traceTelemetry = new TraceTelemetry
{
Message = bodyStr,
SeverityLevel = SeverityLevel.Verbose
};
//Send a trace message for display in Diagnostic Search.
telemetryClient.TrackTrace(traceTelemetry);
}
Where, GetRequestBody is like,
private static string GetRequestBody(HttpContext context)
{
var bodyStr = "";
var req = context.Request;
//Allows using several time the stream in ASP.Net Core.
req.EnableRewind();
//Important: keep stream opened to read when handling the request.
using (var reader = new StreamReader(req.Body, Encoding.UTF8, true, 1024, true))
{
bodyStr = reader.ReadToEnd();
}
// Rewind, so the core is not lost when it looks the body for the request.
req.Body.Position = 0;
return bodyStr;
}
I can able to log the request message body in Application Insights using #yonisha method but I can't able to log the response message body. I am interested in logging the response message body. I am already logging the Post, Put, Delete Request message body using #yonisha method.
When I tried to access the response body in the TelemetryInitializer I keep getting an exception with an error message saying that "stream was not readable. When I researched more I found that AzureInitializer is running as part of HttpModule(ApplicationInsightsWebTracking) so by the time it gets control response object is disposed.
I got an idea from #Oskar answer. Why not have a delegate handler and record the response since the response object is not disposed at the stage of message handler. The message handler is part of the Web API life cycle i.e. similar to the HTTP module but confined to web API. When I developed and tested this idea, fortunately, It worked I recorded the response in the request message using message handler and retrieved it at the AzureInitializer (HTTP module whose execution happens later than the message handler). Here is the sample code.
public class AzureRequestResponseInitializer : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
var requestTelemetry = telemetry as RequestTelemetry;
if (requestTelemetry != null && HttpContext.Current != null && HttpContext.Current.Request != null)
{
if ((HttpContext.Current.Request.HttpMethod == HttpMethod.Post.ToString()
|| HttpContext.Current.Request.HttpMethod == HttpMethod.Put.ToString()) &&
HttpContext.Current.Request.Url.AbsoluteUri.Contains("api"))
using (var reader = new StreamReader(HttpContext.Current.Request.InputStream))
{
HttpContext.Current.Request.InputStream.Position = 0;
string requestBody = reader.ReadToEnd();
if (requestTelemetry.Properties.Keys.Contains("requestbody"))
{
requestTelemetry.Properties["requestbody"] = requestBody;
}
else
{
requestTelemetry.Properties.Add("requestbody", requestBody);
}
}
else if (HttpContext.Current.Request.HttpMethod == HttpMethod.Get.ToString()
&& HttpContext.Current.Response.ContentType.Contains("application/json"))
{
var netHttpRequestMessage = HttpContext.Current.Items["MS_HttpRequestMessage"] as HttpRequestMessage;
if (netHttpRequestMessage.Properties.Keys.Contains("responsejson"))
{
var responseJson = netHttpRequestMessage.Properties["responsejson"].ToString();
if (requestTelemetry.Properties.Keys.Contains("responsebody"))
{
requestTelemetry.Properties["responsebody"] = responseJson;
}
else
{
requestTelemetry.Properties.Add("responsebody", responseJson);
}
}
}
}
}
}
config.MessageHandlers.Add(new LoggingHandler());
public class LoggingHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return base.SendAsync(request, cancellationToken).ContinueWith(task =>
{
var response = task.Result;
StoreResponse(response);
return response;
});
}
private void StoreResponse(HttpResponseMessage response)
{
var request = response.RequestMessage;
(response.Content ?? new StringContent("")).ReadAsStringAsync().ContinueWith(x =>
{
var ctx = request.Properties["MS_HttpContext"] as HttpContextWrapper;
if (request.Properties.ContainsKey("responseJson"))
{
request.Properties["responsejson"] = x.Result;
}
else
{
request.Properties.Add("responsejson", x.Result);
}
});
}
}
The solution provided by yonisha is clean, but it does not work for me in .Net Core 2.0. This works if you have a JSON body:
public IActionResult MyAction ([FromBody] PayloadObject payloadObject)
{
//create a dictionary to store the json string
var customDataDict = new Dictionary<string, string>();
//convert the object to a json string
string activationRequestJson = JsonConvert.SerializeObject(
new
{
payloadObject = payloadObject
});
customDataDict.Add("body", activationRequestJson);
//Track this event, with the json string, in Application Insights
telemetryClient.TrackEvent("MyAction", customDataDict);
return Ok();
}
I am sorry, #yonisha's solution does not seem to work in .NET 4.7. The Application Insights part works OK, but there is actually no simple way to get the request body inside the telemetry initializer in .NET 4.7. .NET 4.7 uses GetBufferlessInputStream() to get the stream, and this stream is "read once". One potential code is like this:
private static void LogRequestBody(ISupportProperties requestTelemetry)
{
var requestStream = HttpContext.Current?.Request?.GetBufferlessInputStream();
if (requestStream?.Length > 0)
using (var reader = new StreamReader(requestStream))
{
string body = reader.ReadToEnd();
requestTelemetry.Properties["body"] = body.Substring(0, Math.Min(body.Length, 8192));
}
}
But the return from GetBufferlessInputStream() is already consumed, and does not support seeking. Therefore, the body will always be an empty string.