Loupe

Application Insights : mode de surveillance et suivi des dépendances

Pour rappel, Application Insights est une offre de service Azure (actuellement en preview) destinée aux développeurs web qui permet de surveiller en quasi temps réels l’activité, les performances de votre application ainsi que les erreurs survenues.

 

Comment Application Insights surveille votre application ?

C’est le mécanisme d’instrumentation du code qui permet de s’abonner à des événements pour ensuite envoyer les données collectées sur Azure. Ces données sont analysées pour permettre leur restitution via divers canaux (API REST, Portail Azure, PowerBI, Analytics, etc…) et au besoin de lever des alertes si certaines valeurs dépassent les seuils configurés.

Application Insights Overview.png

Sur le schéma ci-dessus, Application Insights collecte des informations sur les requêtes HTTP reçues par votre Application ainsi que les appels vers les dépendances. Une dépendance représente un composant externe appelé par votre code comme une requête SQL ou un appel HTTP vers une Web API.

 

Quels sont les dépendances suivies ?

Par défaut, Application Insights surveille les appels vers les types de dépendances ci-dessous mais il possible d’en ajouter d’autres via l’API TrackDependency():

  • Serveur
    • Appel HTTP locale ou externe
    • Base de données SQL
    • Azure DocumentDb
    • Azure Storage
  • Client
    • Appel AJAX

 

Quel est le type de surveillance ?

Il existe trois manières d’ajouter Application Insights à votre application :

  • A l’exécution : nécessite d’ajouter un agent.
  • A la compilation : nécessite d’ajouter le SDK Application Insights à votre projet.
  • Le duo : à la compilation + à l’exécution.

 

La première est transparente d’un point de vue applicatif car il n'est pas nécessaire d'avoir accès au code source et permet ainsi de mettre en place la surveillance d’une application déjà en production. Pour cela, il suffit d’installer l’agent Status Monitor dans le cas d’une application web gérée sur site/IaaS ou Application Insights Extension dans le cas PaaS Azure. La seconde nécessite d’avoir accès au code source de l’application et engendre une recompilation et une publication de votre code pour démarrer la surveillance.

De plus, les possibilités de surveillance ne sont pas les mêmes selon le type de surveillance :

 

A la compilation

A l’exécution

Requêtes et exceptions

Oui

Oui

Détails avancés

 

Oui

Diagnostiques de dépendances

Nécessite .NET 4.6 ou supérieur (surveillance peu détaillé)

Oui avec plus de détails : code de résultat, requête SQL et verbe HTTP

Compteur de performance système

Oui

IIS, Azure Cloud Service ou Azure WebApp

Télémétrie personnalisée

Oui

 

Journaux de traces

Oui

 

Page vues et données utilisateur

Oui

 

Nécessite compilation du code

Oui

 

 

Comment fonctionne l’instrumentation de code pour le suivi des dépendances ?

Pour avoir une compréhension exhaustive, il est nécessaire de récupérer le SDK Application Insights. L’installation du paquet Nuget "Microsoft.ApplicationInsights.Web" permet de récupérer tout ce qu'il faut. Lorsque vous ajoutez le SDK voici les changements apportés à votre projet :

    • Ajout des binaires du SDK
    • Ajout d’un fichier ApplicationInsights.config à la racine
      • Contient la liste des modules initialisés par Application Insights au démarrage de l’application
    • Ajout des éléments suivants dans le fichier web.config
      <system.web>
      	<httpModules>
      		<add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" />
      	</httpModules>
      </system.web>
      <system.webServer>
      	<modules>
      		<remove name="ApplicationInsightsWebTracking" />
      		<add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" preCondition="managedHandler" />
      	</modules>
      </system.webServer>
      

 Lors du démarrage de votre application, le module « ApplicationInsightsHttpModule » charge l’ensemble des modules présents dans le fichier ApplicationInsights.config en appelant la méthode Initiale() pour chaque, notamment celui responsable du suivi des dépendances nommé « DependencyTrackingTelemetryModule ».

 

OK mais avant d’aller plus loin j’aimerais comprendre comment Application Insights est enregistré lorsque j’utilise la surveillance à l’exécution car je ne suis pas censé modifier ni le code ni le fichier web.config, non ?

En effet, lorsque vous installez l’agent celui-ci récupère automatiquement le SDK Application Insights, le place dans le dossier /bin de votre application et ajoute également le package Nuget « Microsoft.ApplicationInsights.StatusMonitorInstrumentation ». Ensuite toute la magie vient de cette librairie « Microsoft.AI.HttpModule.dll » contenue dans ce dernier package. En effet celle-ci possède la classe « WebRequestTrackingModuleRegister » responsable d’enregistrer le module « ApplicationInsightsHttpModule » comme si celui-ci était présent dans le fichier de web.config grâce au concept PreApplicationStart introduit dans ASP.NET 4. D’ailleurs c’est également utilisé pour enregistrer les modules OWIN ou Unity via l’attribut WebActivator, voici un lien qui explique tout. Avec ce mode, il n'y a donc qu'une copie du SDK avec application et l'enregistrement dynamique de la classe responsable de charger ce SDK sans modification du code source de votre application.

 

Revenons à la classe « DependencyTrackingTelemetryModule », l’appel à la méthode Initialize() permet de s’attacher au événement ETW du framework .NET dans le cas d’une surveillance à la compilation via la méthode InitializeForFrameworkEventSource() ou au runtime pour une surveillance à l’exécution avec la méthode InitializeForRuntimeProfiler().

/// <summary>
/// Initialize for framework event source (not supported for Net40).
/// </summary>
private void InitializeForFrameworkEventSource()
{
  TelemetryConfiguration telemetryConfiguration1 = this.telemetryConfiguration;
  TimeSpan retryInterval1 = TimeSpan.FromMilliseconds(10.0);
  int retryCount1 = 3;
  this.httpEventListener = RetryPolicy.Retry<InvalidOperationException, TelemetryConfiguration, FrameworkHttpEventListener>((Func<TelemetryConfiguration, FrameworkHttpEventListener>) (config => new FrameworkHttpEventListener(config)), telemetryConfiguration1, retryInterval1, retryCount1);
  TelemetryConfiguration telemetryConfiguration2 = this.telemetryConfiguration;
  TimeSpan retryInterval2 = TimeSpan.FromMilliseconds(10.0);
  int retryCount2 = 3;
  this.sqlEventListener = RetryPolicy.Retry<InvalidOperationException, TelemetryConfiguration, FrameworkSqlEventListener>((Func<TelemetryConfiguration, FrameworkSqlEventListener>) (config => new FrameworkSqlEventListener(config)), telemetryConfiguration2, retryInterval2, retryCount2);
}

 

On remarque que le suivi des dépendances est effectué grâce à deux listeners (HTTP et SQL) voici ci-dessous le code correspond à celui SQL :

/// <summary>
/// Provides methods for listening to events from FrameworkEventSource for SQL.
/// </summary>
internal class FrameworkSqlEventListener : EventListener
{
  /// <summary>The SQL processor.</summary>
  internal readonly FrameworkSqlProcessing SqlProcessingFramework;
  /// <summary>The Framework EventSource name for SQL.</summary>
  private const string AdoNetEventSourceName = "Microsoft-AdoNet-SystemData";
  /// <summary>BeginExecute Event ID.</summary>
  private const int BeginExecuteEventId = 1;
  /// <summary>EndExecute Event ID.</summary>
  private const int EndExecuteEventId = 2;

  internal FrameworkSqlEventListener(TelemetryConfiguration configuration)
  {
    this.SqlProcessingFramework = new FrameworkSqlProcessing(configuration, DependencyTableStore.Instance.SqlRequestCacheHolder);
  }

  /// <summary>
  /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
  /// </summary>
  public override void Dispose()
  {
    this.SqlProcessingFramework.Dispose();
    base.Dispose();
  }

  /// <summary>
  /// Enables SQL event source when EventSource is created. Called for all existing
  /// event sources when the event listener is created and when a new event source is attached to the listener.
  /// </summary>
  protected override void OnEventSourceCreated(EventSource eventSource)
  {
    if (eventSource != null && eventSource.Name == "Microsoft-AdoNet-SystemData")
    {
      this.EnableEvents(eventSource, EventLevel.Informational, (EventKeywords) 1);
      DependencyCollectorEventSource.Log.RemoteDependencyModuleVerbose("SqlEventListener initialized for event source:Microsoft-AdoNet-SystemData", "Incorrect");
    }
    base.OnEventSourceCreated(eventSource);
  }

  /// <summary>
  /// Called whenever an event has been written by an event source for which the event listener has enabled events.
  /// </summary>
  /// <param name="eventData">The event arguments that describe the event.</param>
  protected override void OnEventWritten(EventWrittenEventArgs eventData)
  {
    if (eventData == null)
      return;
    if (eventData.Payload == null)
      return;
    try
    {
      switch (eventData.EventId)
      {
        case 1:
          this.OnBeginExecute(eventData);
          break;
        case 2:
          this.OnEndExecute(eventData);
          break;
      }
    }
    catch (Exception ex)
    {
      DependencyCollectorEventSource.Log.CallbackError(0L, "FrameworkSqlEventListener.OnEventWritten", ex);
    }
  }

  /// <summary>
  /// Called when a postfix of a SQLCommand begin methods have been invoked.
  /// </summary>
  /// <param name="eventData">The event arguments that describe the event.</param>
  private void OnBeginExecute(EventWrittenEventArgs eventData)
  {
    if (eventData.Payload.Count < 4)
      return;
    long int64 = Convert.ToInt64(eventData.Payload[0], (IFormatProvider) CultureInfo.InvariantCulture);
    string dataSource = Convert.ToString(eventData.Payload[1], (IFormatProvider) CultureInfo.InvariantCulture);
    string database = Convert.ToString(eventData.Payload[2], (IFormatProvider) CultureInfo.InvariantCulture);
    string commandText = Convert.ToString(eventData.Payload[3], (IFormatProvider) CultureInfo.InvariantCulture);
    if (this.SqlProcessingFramework == null)
      return;
    this.SqlProcessingFramework.OnBeginExecuteCallback(int64, dataSource, database, commandText);
  }

  /// <summary>
  /// Called when a postfix of a postfix of a SQLCommand end methods have been invoked.
  /// </summary>
  /// <param name="eventData">The event arguments that describe the event.</param>
  private void OnEndExecute(EventWrittenEventArgs eventData)
  {
    if (eventData.Payload.Count < 3)
      return;
    int int32_1 = Convert.ToInt32(eventData.Payload[0], (IFormatProvider) CultureInfo.InvariantCulture);
    int int32_2 = Convert.ToInt32(eventData.Payload[1], (IFormatProvider) CultureInfo.InvariantCulture);
    int num1 = 1;
    bool success = (int32_2 & num1) == 1;
    int num2 = 4;
    bool synchronous = (int32_2 & num2) == 4;
    int int32_3 = Convert.ToInt32(eventData.Payload[2], (IFormatProvider) CultureInfo.InvariantCulture);
    if (this.SqlProcessingFramework == null)
      return;
    this.SqlProcessingFramework.OnEndExecuteCallback((long) int32_1, success, synchronous, int32_3);
  }

  private enum CompositeState
  {
    Success = 1,
    IsSqlException = 2,
    Synchronous = 4,
  }
}

 

On constate que la surveillance à la compilation est liée aux événement ETW du Framework .NET. Ci-dessous le code qui initialise la surveillance au runtime :

internal virtual void InitializeForRuntimeProfiler()
{
  string modulePath = string.IsNullOrWhiteSpace(AppDomain.CurrentDomain.RelativeSearchPath) ? AppDomain.CurrentDomain.BaseDirectory : AppDomain.CurrentDomain.RelativeSearchPath;
  DependencyCollectorEventSource.Log.RemoteDependencyModuleInformation("extesionBaseDirectrory is " + modulePath, "Incorrect");
  Decorator.InitializeExtension(modulePath);
  string agentVersion = Decorator.GetAgentVersion();
  DependencyCollectorEventSource.Log.RemoteDependencyModuleInformation("AgentVersion is " + agentVersion, "Incorrect");
  this.httpProcessing = new ProfilerHttpProcessing(this.telemetryConfiguration, agentVersion, DependencyTableStore.Instance.WebRequestConditionalHolder);
  this.sqlProcessing = new ProfilerSqlProcessing(this.telemetryConfiguration, agentVersion, DependencyTableStore.Instance.SqlRequestConditionalHolder);
  ProfilerRuntimeInstrumentation.DecorateProfilerForHttp(ref this.httpProcessing);
  ProfilerRuntimeInstrumentation.DecorateProfilerForSql(ref this.sqlProcessing);
}

 

Avec le pattern Decorator pour instrumenter le code comme ci-dessous :

namespace Microsoft.ApplicationInsights.DependencyCollector.Implementation
{
  internal static class ProfilerRuntimeInstrumentation
  {
    internal static void DecorateProfilerForHttp(ref ProfilerHttpProcessing httpCallbacks)
    {
      Functions.Decorate("System", "System.dll", "System.Net.HttpWebRequest.GetResponse", new Func<object, object>(httpCallbacks.OnBeginForGetResponse), new Func<object, object, object, object>(httpCallbacks.OnEndForGetResponse), new Action<object, Exception, object>(httpCallbacks.OnExceptionForGetResponse), false, true);
      Functions.Decorate("System", "System.dll", "System.Net.HttpWebRequest.GetRequestStream", new Func<object, object, object>(httpCallbacks.OnBeginForGetRequestStream), (Func<object, object, object, object, object>) null, new Action<object, Exception, object, object>(httpCallbacks.OnExceptionForGetRequestStream), false, true);
      Functions.Decorate("System", "System.dll", "System.Net.HttpWebRequest.BeginGetResponse", new Func<object, object, object, object>(httpCallbacks.OnBeginForBeginGetResponse), (Func<object, object, object, object, object, object>) null, (Action<object, Exception, object, object, object>) null, false, true);
      Functions.Decorate("System", "System.dll", "System.Net.HttpWebRequest.EndGetResponse", (Func<object, object, object>) null, new Func<object, object, object, object, object>(httpCallbacks.OnEndForEndGetResponse), new Action<object, Exception, object, object>(httpCallbacks.OnExceptionForEndGetResponse), false, true);
      Functions.Decorate("System", "System.dll", "System.Net.HttpWebRequest.BeginGetRequestStream", new Func<object, object, object, object>(httpCallbacks.OnBeginForBeginGetRequestStream), (Func<object, object, object, object, object, object>) null, (Action<object, Exception, object, object, object>) null, false, true);
      Functions.Decorate("System", "System.dll", "System.Net.HttpWebRequest.EndGetRequestStream", (Func<object, object, object, object>) null, (Func<object, object, object, object, object, object>) null, new Action<object, Exception, object, object, object>(httpCallbacks.OnExceptionForEndGetRequestStream), false, true);
    }

    internal static void DecorateProfilerForSql(ref ProfilerSqlProcessing sqlCallbacks)
    {
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.BeginExecuteNonQuery", new Func<object, object>(sqlCallbacks.OnBeginForOneParameter), (Func<object, object, object, object>) null, (Action<object, Exception, object>) null, false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.BeginExecuteNonQuery", new Func<object, object, object, object>(sqlCallbacks.OnBeginForThreeParameters), (Func<object, object, object, object, object, object>) null, (Action<object, Exception, object, object, object>) null, false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.EndExecuteNonQuery", (Func<object, object, object>) null, new Func<object, object, object, object, object>(sqlCallbacks.OnEndForTwoParameters), new Action<object, Exception, object, object>(sqlCallbacks.OnExceptionForTwoParameters), false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.ExecuteNonQuery", new Func<object, object>(sqlCallbacks.OnBeginForOneParameter), new Func<object, object, object, object>(sqlCallbacks.OnEndForOneParameter), new Action<object, Exception, object>(sqlCallbacks.OnExceptionForOneParameter), false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.BeginExecuteNonQueryAsync", new Func<object, object, object, object>(sqlCallbacks.OnBeginForThreeParameters), (Func<object, object, object, object, object, object>) null, (Action<object, Exception, object, object, object>) null, false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.EndExecuteNonQueryAsync", (Func<object, object, object>) null, new Func<object, object, object, object, object>(sqlCallbacks.OnEndForTwoParameters), new Action<object, Exception, object, object>(sqlCallbacks.OnExceptionForTwoParameters), false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.BeginExecuteReader", new Func<object, object>(sqlCallbacks.OnBeginForOneParameter), (Func<object, object, object, object>) null, (Action<object, Exception, object>) null, false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.BeginExecuteReader", new Func<object, object, object>(sqlCallbacks.OnBeginForTwoParameters), (Func<object, object, object, object, object>) null, (Action<object, Exception, object, object>) null, false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.BeginExecuteReader", new Func<object, object, object, object>(sqlCallbacks.OnBeginForThreeParameters), (Func<object, object, object, object, object, object>) null, (Action<object, Exception, object, object, object>) null, false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.BeginExecuteReader", new Func<object, object, object, object, object>(sqlCallbacks.OnBeginForFourParameters), (Func<object, object, object, object, object, object, object>) null, (Action<object, Exception, object, object, object, object>) null, false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.EndExecuteReader", (Func<object, object, object>) null, new Func<object, object, object, object, object>(sqlCallbacks.OnEndForTwoParameters), new Action<object, Exception, object, object>(sqlCallbacks.OnExceptionForTwoParameters), false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.ExecuteReader", new Func<object, object>(sqlCallbacks.OnBeginForOneParameter), new Func<object, object, object, object>(sqlCallbacks.OnEndForOneParameter), new Action<object, Exception, object>(sqlCallbacks.OnExceptionForOneParameter), false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.ExecuteReader", new Func<object, object, object, object>(sqlCallbacks.OnBeginForThreeParameters), new Func<object, object, object, object, object, object>(sqlCallbacks.OnEndForThreeParameters), new Action<object, Exception, object, object, object>(sqlCallbacks.OnExceptionForThreeParameters), false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.BeginExecuteReaderAsync", new Func<object, object, object, object, object>(sqlCallbacks.OnBeginForFourParameters), (Func<object, object, object, object, object, object, object>) null, (Action<object, Exception, object, object, object, object>) null, false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.EndExecuteReaderAsync", (Func<object, object, object>) null, new Func<object, object, object, object, object>(sqlCallbacks.OnEndForTwoParameters), new Action<object, Exception, object, object>(sqlCallbacks.OnExceptionForTwoParameters), false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.ExecuteScalar", new Func<object, object>(sqlCallbacks.OnBeginForOneParameter), new Func<object, object, object, object>(sqlCallbacks.OnEndForOneParameter), new Action<object, Exception, object>(sqlCallbacks.OnExceptionForOneParameter), false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.BeginExecuteXmlReader", new Func<object, object>(sqlCallbacks.OnBeginForOneParameter), (Func<object, object, object, object>) null, (Action<object, Exception, object>) null, false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.BeginExecuteXmlReader", new Func<object, object, object, object>(sqlCallbacks.OnBeginForThreeParameters), (Func<object, object, object, object, object, object>) null, (Action<object, Exception, object, object, object>) null, false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.EndExecuteXmlReader", (Func<object, object, object>) null, new Func<object, object, object, object, object>(sqlCallbacks.OnEndForTwoParameters), new Action<object, Exception, object, object>(sqlCallbacks.OnExceptionForTwoParameters), false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.ExecuteXmlReader", new Func<object, object>(sqlCallbacks.OnBeginForOneParameter), new Func<object, object, object, object>(sqlCallbacks.OnEndForOneParameter), new Action<object, Exception, object>(sqlCallbacks.OnExceptionForOneParameter), false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.BeginExecuteXmlReaderAsync", new Func<object, object, object, object>(sqlCallbacks.OnBeginForThreeParameters), (Func<object, object, object, object, object, object>) null, (Action<object, Exception, object, object, object>) null, false, true);
      Functions.Decorate("System.Data", "System.Data.dll", "System.Data.SqlClient.SqlCommand.EndExecuteXmlReaderAsync", (Func<object, object, object>) null, new Func<object, object, object, object, object>(sqlCallbacks.OnEndForTwoParameters), new Action<object, Exception, object, object>(sqlCallbacks.OnExceptionForTwoParameters), false, true);
    }
  }
}

 

Pour finir, une petite subtilité lorsque les deux types de surveillance sont activés c’est celui à l’exécution qui prévaut car il offre davantage d’information.

/// <summary>
/// Initialize for runtime instrumentation or framework event source.
/// </summary>
private void InitializeForRuntimeInstrumentationOrFramework()
{
    if (this.IsProfilerAvailable())
    {
        DependencyCollectorEventSource.Log.RemoteDependencyModuleInformation("Profiler is attached.",
            "Incorrect");
        if (!this.DisableRuntimeInstrumentation)
        {
            try
            {
                this.InitializeForRuntimeProfiler();
                DependencyTableStore.Instance.IsProfilerActivated = true;
            }
            catch (Exception ex)
            {
                this.InitializeForFrameworkEventSource();
                DependencyCollectorEventSource.Log.ProfilerFailedToAttachError(ex.ToInvariantString(),
                    "Incorrect");
            }
        }
        else
        {
            this.InitializeForFrameworkEventSource();
            DependencyCollectorEventSource.Log.RemoteDependencyModuleVerbose(
                "Runtime instrumentation is set to disabled. Initialize with framework event source instead.",
                "Incorrect");
        }
    }
    else
    {
        this.InitializeForFrameworkEventSource();
        DependencyCollectorEventSource.Log.RemoteDependencyModuleProfilerNotAttached("Incorrect");
    }
}

 

Enjoy !

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus