English
Français

Blog of Denis VOITURON

for a better .NET world

Application Insights pour vos WebAPI

Posted on 2019-11-18

Application Insights est une fonctionnalité d’Azure Monitor. C’est un service extensible d’Application Performance Management (APM). Utilisez-le pour surveiller votre application. Il détectera automatiquement les anomalies de performance. Il inclut de puissants outils d’analyse pour vous aider à diagnostiquer les problèmes et à comprendre ce que les utilisateurs font réellement avec votre application.

1. Activer Application Insights.

Pour utiliser ce service, c’estr très simple.

  1. Il suffit d’ajouter ce service Application Insights à votre hébergement Azure. Si vous avez déjà un App Service (pour votre site web, par exemple), vous disposez déjà du service. Récupérez la clé Instrumentation Key, depuis l’écran Overview du service Application Insights.

  2. Dans votre projet ASP.NET Core, ajoutez le package NuGet ApplicationInsights.

  3. Rérérencez ce nouveau service dans votre classe Startup: services.AddApplicationInsightsTelemetry();

  4. Créez ou modifiez votre fichier appsettings.json en y spécifiant l’Instrumentation Key:

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

Démarrez votre projet et naviguez dans les API ou les écrans. Après quelques secondes, vous verrez apparaître les traces dans le portail Azure.

Application Insights Demo

2. Activer les traces ILogger.

De base, les traces personnalisées, utilisées en .NETCore, ne sont pas envoyées vers Application Insights. Il se charge d’envoyer les traces des appels aux API, aux pages, des Exceptions, … mais pas les traces manuelles. Pour cela, il faut configurer le service, au démarrage du serveur :

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. Afficher le contenu des requêtes (Raw Body).

Cette fonctionnalités n’est pas présente nativement dans Application Insights. A la date de cet article, cette suggestion est en cours d’analyse chez Microsoft.

En attendant, si vous souhaitez tracer le contenu de vos requêtes (Request et Response), vous devez capturer les requêtes, en héritant de ActionFilterAttribute, pour conserver le body en mémoire. Ensuite, en implémentant l’interface ITelemetryInitializer, vous pouvez envoyer ce contenu vers Application Insights.

3.1 Préparer un espace de stockage des Raw Body.

La classe suivante sert de container pour enregistrer, temporairement, le contenu des Request 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 Capturer le contenu des requêtes

Cela se fait simplement en écrivant une classe telle que celle-ci et en l’injectant dans les filtres MVC.

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);
    }
}

Et l’injecter dans les filtres MVC.

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

3.3 Envoyer le contenu vers Application Insights

Et finalement, créer une classe qui gère la télémétrie vers 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);
        }
    }
}

Qu’il faut injecter dans les services.

services.AddSingleton<ITelemetryInitializer, TelemetryRequestResponse>();

3.4 Tester

Après avoir démarré votre projet, toutes les WebAPI sont tracées dans Azure Application Insights. Les contenus des requêtes sont enregistées dans les champs customDimensions.requestBody et customDimensions.responseBody.

N’hésitez pas à utiliser Log Analytics pour vérifier le contenu complet des traces (il faut parfois quelques secondes pour que les données soient exploitables).

Exemple:

requests
| limit 50
| order by timestamp  desc

Langues

EnglishEnglish
FrenchFrançais

Suivez-moi

Articles récents