Add API Request data to Application Insights telemetry in ASP.NET Core using Filters

Robin Agten
delaware
Published in
6 min readJan 12, 2021

--

This post describes in detail how you can easily add API request data to Application Insights telemetry for all your API actions. By default, Application Insights already logs a lot of information about the API request like the method (GET, POST, …), the response code, the response time, … The image below show a subset of this data in Application Insights.

In many scenarios you may want to log other information as well, like the request body or the response for example. The proposed solution in this post will guide you through the needed steps to accomplish this for all your API controller actions.

Assumptions and Prerequisites

I will assume that you already have the basic Application Insights logging set up in your project. If not, the following link gives some explanations and examples on how to set up Application Insights in a ASP.NET Core application: Azure Application Insights for ASP.NET Core applications — Azure Monitor | Microsoft Docs.

At the end of this post there is a link to a sample repository. In order to test this example you will need the instrumentation key of an Application Insights resource in Azure.

Add custom properties to Application Insights

The first thing that you need to know is how to add custom properties to the Application Insights telemetry. The code snippet below does exactly that:

[HttpPost]
[Route("addMeasurement")]
public WeatherForecast AddMeasurement(
[FromBody] WeatherForecast forecast
)
{
RequestTelemetry reqTelemetry =
HttpContext?.Features.Get<RequestTelemetry>();

string requestBody = JsonConvert.SerializeObject(forecast);
if (reqTelemetry != null) {
reqTelemetry.Properties.Add("requestBody", requestBody);
}
return forecast;
}

As you can see in the snippet (which is just a sample that adds a weather forecast), a request telemetry object is available on the HttpContext from your controller. You can then easily add custom properties by extending the Properties dictionary on this request telemetry object. You can also add the response object and any other properties available in your request. Easy enough, right?

The main drawback here is that you will manually need to add this to all your controller actions. You could move some of the code to a controller base class, but you will still need to call this method in all actions. Fortunately there is a solution for this. Filters to the rescue.

ASP.NET Core Filters

Filters in ASP.NET Core allow code to be run before or after specific stages in the request processing pipeline.

This quote from the Microsoft documentation describes exactly what we need. We want some code to run before and/or after each controller action. All controllers that inherit from the ControllerBase class have an OnActionExecuting, OnActionExecuted and OnActionExecutionAsync method who actually wrap the filters that run for a given action. For more details about different types of filters you can check out the following link: Filters in ASP.NET Core | Microsoft Docs

The snippet below the custom TelemetryFilter

public class TelemetryFilter: IAsyncActionFilter {
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next
) {
// Code executed before the request

var result = await next();

// Code run after the request
}
}

As you can see I use the IAsyncActionFilter which needs to implement an OnActionExecutionAsync function. In this function you can run code before the request, await the result and run some code after the request. In this method we can implement our telemetry stuff. In the next chapter we will see how we can hook this filter up to our controller actions.

The ActionExecutingContext actually has some very interesting properties. It has the full HttpContext, which allows you to access all the request properties (Path, Query, Headers, Body, …). My first instinct was to read the body from the request and add it as a custom property to our telemetry. The problem with this is that the body object is of type System.IO.Stream which does not support rewinding, which means it can only be read once. To solve this you could use the EnableBuffering extension method, and set the stream position back to 0 after reading it (for more information on how to do this you can check out the following post: Re-reading ASP.Net Core request bodies with EnableBuffering() | ASP.NET Blog (microsoft.com))

However, there is a simpler solution to get the request body. The ActionExecutingContext has another very interesting property called ActionArguments. This neat little fellow is a dictionary containing all the parameters that were passed to the action (in our case the controller action). The keys correspond to the variable name. So we could actually get all the dictionary values and add them to our request telemetry:

public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next
) {
// Initialize the RequestTelemetry object
RequestTelemetry reqTelemetry =
context.HttpContext?.Features.Get<RequestTelemetry>();
if (reqTelemetry != null) {
// Add the request parameters
var requestParameters = context.ActionArguments;
foreach (var param in requestParameters) {
reqTelemetry.Properties.Add(
param.Key,
JsonConvert.SerializeObject(param.Value)
);
}
}

// Await the action result
var result = await next();

// Cast the result to an Mvc ObjectResult and add the value to the telemtry
var response = (ObjectResult)result.Result;
if (reqTelemetry != null) {
reqTelemetry.Properties.Add(
"Result",
JsonConvert.SerializeObject(response.Value)
);
}
}

The code snippet above combines the telemetry and the filter together. As you can see, all the request parameters are looped, serialized and added to the telemetry. I’ve also added some code after the execution to also add the result to the telemtry. We first cast the result to an Mvc ObjectResult object. We then serialze it and also add it to the request telemetry.

As you can see in the screenshot below, the request parameter (in this case forecast) and the result are added to the custom properties in Application Insights:

Scoping the Filter

The last step is to scope our Filter. We need to tell the application where the Filter needs to be used to get it to work. There are 3 ways to do this. On the Action level, the Controller level or Globally. In our case we want it to be scoped globally because we want to run it for all our actions. If you want to do it specific to an action or controller you can check out this great post: Action Filters in .NET Core Web API — How to Write Cleaner Actions (code-maze.com).

To globally scope the filter, you can just initialize it in the ConfigureServices method of your startup.cs:

public void ConfigureServices(IServiceCollection services) {
services.AddControllers(config => config.Filters.Add(
new TelemetryFilter()
));
services.AddApplicationInsightsTelemetry();
}

Conclusion

ASP.NET Core ActionFilters can easily be used to run code before or after controller actions. This allows us to easily add custom properties to our Application Insights request telemetry for all controller actions. The ActionFilter properties have some handy parameters to easily access the action parameters or the action request context.

Resources

Sample Code

Originally published at http://digitalworkplace365.wordpress.com on January 12, 2021.

--

--