English
Français

Blog of Denis VOITURON

for a better .NET world

Application Insights for your WebAPI

Posted on 2019-11-18

Application Insights is a feature of Azure Monitor. It is an extensible service of Application Performance Management (APM). Use it to monitor your application. It will automatically detect performance anomalies. It includes powerful analysis tools to help you diagnose problems and understand what users are actually doing with your application.

1. Enable Application Insights.

To use this service, it is very simple.

  1. Simply add this service Application Insights to your Azure portal. If you already have a App Service (for your website, for example), you already have the service. Retrieve the Instrumentation Key key from the Overview screen of the Application Insights service.

  2. In your ASP.NET Core project, add the NuGet package ApplicationInsights.

  3. Refer this new service to your Startup class: services.AddApplicationInsightsTelemetry();

  4. Create or modify your file appsettings.json by specifying the Instrumentation Key:

{
  "ApplicationInsights": {
    "InstrumentationKey": "2212b9e0-4715-1765-1746-12cbe02edd67"
  }    
}

Start your project and navigate through the APIs or screens. After a few seconds, you will see the traces appear in the Azure portal.

Application Insights Demo

2. Enable ILogger traces.

Basically, custom traces, used in .NETCore, are not sent to Application Insights. It is in charge of sending the traces of calls to APIs, pages, Exceptions,… but not the manual traces. To do this, it is necessary to configure the service, when the server starts:

Host.CreateDefaultBuilder(args)
    .ConfigureLogging((hostingContext, logging) =>
    {
        string key = hostingContext.Configuration
                                   .GetSection("ApplicationInsights:InstrumentationKey")
                                   .Value;
        if (!String.IsNullOrEmpty(key))
        {
            logging.AddApplicationInsights(key);
            logging.AddFilter<ApplicationInsightsLoggerProvider>("", LogLevel.Trace);
            logging.AddFilter<ApplicationInsightsLoggerProvider>("Microsoft", LogLevel.Warning);
        }
    })

3. Display the content of requests (Raw Body).

This feature is not available natively in Application Insights. At the date of this article, this suggestion is being analyzed by Microsoft.

In the meantime, if you want to track the content of your requests (Request and Response), you must capture the requests, inheriting ActionFilterAttribute, to keep the body in memory. Then, by implementing the ITelemetryInitializer interface, you can send this content to Application Insights.

3.1 Prepare a storage space for Raw Body.

The following class is used as a container to temporarily record the content of the Request/Response Bodies.

using System;
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json;

public class GlobalStoredTraces
{
    public static readonly ConcurrentDictionary<string, GlobalStoredTraces> CurrentTraces = new ConcurrentDictionary<string, GlobalStoredTraces>();

    // Content to keep (and remove when sent).
    public string Id { get; private set; }
    public string Body { get; set; }
    public bool HasBody => !String.IsNullOrEmpty(Body);

    // Static methods to manage this ConcurrentDictionary.
    public static readonly GlobalStoredTraces Empty = new GlobalStoredTraces();
    
    public static GlobalStoredTraces AddRequestTrace(HttpContext context)
    {
        var trace = new GlobalStoredTraces()
        {
            Id = context.TraceIdentifier,
            Body = GetHttpRequest(context),
        };
        CurrentTraces.TryAdd($"Request_{trace.Id}", trace);
        return trace;
    }

    public static GlobalStoredTraces GetRequestTrace(string id)
    {
        if (CurrentTraces.TryRemove($"Request_{id}", out var trace))
            return trace;
        else
            return GlobalStoredTraces.Empty;
    }

    public static GlobalStoredTraces AddResponseTrace(ResultExecutedContext context)
    {
        var trace = new GlobalStoredTraces()
        {
            Id = context.HttpContext.TraceIdentifier,
            Body = GetHttpResult(context.Result),
        };
        CurrentTraces.TryAdd($"Response_{trace.Id}", trace);
        return trace;
    }

    public static GlobalStoredTraces GetResponseTrace(string id)
    {
        if (CurrentTraces.TryRemove($"Response_{id}", out var trace))
            return trace;
        else
            return GlobalStoredTraces.Empty;
    }

    private static string GetHttpRequest(HttpContext context)
    {
        string body;

        if (context?.Request?.Body == null) return string.Empty;
        if (!context.Request.Body.CanRead) return string.Empty;
        if (!context.Request.Body.CanSeek) return string.Empty;

        context.Request.Body.Position = 0;
        using (var reader = new System.IO.StreamReader(context.Request.Body))
        {
            body = reader.ReadToEnd();
        }

        return body;
    }

    private static string GetHttpResult(Microsoft.AspNetCore.Mvc.IActionResult result)
    {
        var objectResult = result as Microsoft.AspNetCore.Mvc.ObjectResult;

        if (objectResult != null)
            return JsonConvert.SerializeObject(objectResult.Value);
        else
            return JsonConvert.SerializeObject(result);
    }
}

3.2 Capture the content of requests

This is done simply by writing a class like this and injecting it into the MVC filters.

public class GlobalControllerAppInsightsAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        GlobalStoredTraces.AddRequestTrace(context.HttpContext);
        base.OnActionExecuting(context);
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
        GlobalStoredTraces.AddResponseTrace(context);
        base.OnResultExecuted(context);
    }
}

And inject it into the MVC filters.

services.AddApplicationInsightsTelemetry();
services.AddMvc(config =>
        {
            config.Filters.Add(typeof(GlobalControllerAppInsightsAttribute));
        });

3.3 Upload content to Application Insights

And finally, create a class that manages telemetry to Application Insights.

public class TelemetryRequestResponse : ITelemetryInitializer
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TelemetryRequestResponse(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void Initialize(ITelemetry telemetry)
    {
        var requestTelemetry = telemetry as RequestTelemetry;

        if (requestTelemetry != null)
        {
            string id = _httpContextAccessor.HttpContext.TraceIdentifier;
            var request = GlobalStoredTraces.GetRequestTrace(id);
            var response = GlobalStoredTraces.GetResponseTrace(id);
            requestTelemetry.Properties.Add("requestBody", request.Body);
            requestTelemetry.Properties.Add("responseBody", response.Body);
        }
    }
}

We need to inject into the services.

services.AddSingleton<ITelemetryInitializer, TelemetryRequestResponse>();

3.4 Testing

After starting your project, all WebAPIs are tracked in Azure Application Insights. The contents of the requests are saved in the fields customDimensions.requestBody and customDimensions.responseBody.

Feel free to use Log Analytics to check the complete content of the traces (it sometimes takes a few seconds for the data to be usable).

Example:

requests
| limit 50
| order by timestamp  desc

Languages

EnglishEnglish
FrenchFrançais

Follow me

Recent posts