Partager via


Monitor vos API avec Gestion des API Azure, Event Hubs et Moesif

S'APPLIQUE À : Tous les niveaux de Gestion des API

Le service de gestion des API fournit de nombreuses fonctionnalités pour améliorer le traitement des requêtes HTTP envoyées à votre API HTTP. Toutefois, l’existence des demandes et réponses est temporaire. La requête est effectuée et transite par le service Gestion des API vers votre API principale. Votre API traite la requête et une réponse retourne vers le consommateur d’API. Le service Gestion des API conserve certaines statistiques importantes sur les API à des fins d’affichage dans le tableau de bord du portail Azure, mais au-delà, les détails disparaissent.

Utiliser la stratégie log-to-eventhub pour le service de gestion des API vous permet d’envoyer n’importe quel détail de la demande et de la réponse à un Azure Event Hub. Il existe plusieurs raisons pour lesquelles vous souhaiterez peut-être générer des événements à partir de messages HTTP envoyés à vos API. Certains exemples incluent une piste d’audit des mises à jour, une analyse des usages, une alerte en cas d’exception et l’intégration de tiers.

Cet article montre comment capturer l’intégralité du message de requête et de réponse HTTP, l’envoyer à un hub d’événements, puis relayer ce message à un service tiers qui fournit la journalisation HTTP et les services de surveillance.

Pourquoi envoyer à partir du service Gestion des API ?

Vous pouvez écrire un intergiciel HTTP qui peut se connecter à des infrastructures d’API HTTP pour capturer des requêtes et des réponses HTTP et les alimenter dans les systèmes de journalisation et de surveillance. L’inconvénient de cette approche est que le middleware HTTP doit être intégré à l’API principale et doit correspondre à la plateforme d’API. S’il existe plusieurs API, chacune d’elles doit déployer le middleware. Généralement, les API de serveur principal ne peuvent pas être mises à jour pour une raison donnée.

L’utilisation du service de gestion des API Azure à intégrer à l’infrastructure de journalisation fournit une solution centralisée et indépendante de la plate-forme. Il est également évolutif, en partie en raison des fonctionnalités de géoréplication de Gestion des API Azure.

Pourquoi procéder à des envois vers un Event Hub ?

Il est raisonnable de demander : pourquoi créer une stratégie spécifique à Azure Event Hubs ? Il existe de nombreux endroits différents où vous souhaiterez peut-être consigner vos demandes. Pourquoi ne pas simplement envoyer les demandes directement à la destination finale ? C’est une option. Toutefois, lorsque vous effectuez des demandes de journalisation à partir d’un service de gestion des API, il est nécessaire de prendre en compte la façon dont les messages de journalisation affectent les performances de l’API. L’augmentation progressive de charge peut être traitées par l’augmentation du nombre d’instances disponibles de composants système ou par le biais de la géo-réplication. Cependant, de courts pics de trafic peuvent entraîner des retards de demandes si les demandes d’infrastructures de journalisation commencent à ralentir en raison de la charge.

Azure Event Hubs est conçu pour ingresser d’énormes volumes de données, avec une capacité de traitement d’un nombre d’événements beaucoup plus élevé que le nombre de requêtes HTTP la plupart des processus d’API. Le hub d’événements agit comme une sorte de tampon sophistiqué entre votre service de gestion des API et l’infrastructure qui stocke et traite les messages. Cela garantit que les performances de votre API ne seront pas restreintes par l’infrastructure de journalisation.

Une fois que les données ont été transmises à un Event Hub, elles sont conservées dans l’attente d’être traitées par les consommateurs de l’Event Hub. L’Event Hub ne se préoccupe pas du mode de traitement ; il se contente de s’assurer que le message est bien remis.

Event Hubs a la possibilité de faire circuler les événements de flux de données à plusieurs groupes de consommateurs. Ainsi, les événements doivent être traités par des systèmes différents. Cela prend en charge de nombreux scénarios d’intégration sans retarder le traitement de la demande d’API au sein du service Gestion des API, car un seul événement doit être généré.

Stratégie d’envoi de messages application/HTTP

Un Event Hub accepte des données d’événement sous la forme d’une chaîne simple. C’est vous qui décidez du contenu de cette chaîne. Pour pouvoir empaqueter une requête HTTP et l’envoyer à Azure Event Hubs, vous devez mettre en forme la chaîne avec les informations de requête ou de réponse. Dans des situations comme celle-ci, s’il existe un format existant que vous pouvez réutiliser, vous n’avez peut-être pas besoin d’écrire votre propre code d’analyse. Au départ, vous pouvez envisager d’utiliser le har pour envoyer des requêtes et des réponses HTTP. Cependant, ce format est optimisé pour stocker une séquence de requêtes HTTP dans un format basé sur JSON. Il contenait un certain nombre d’éléments obligatoires qui ajoutait un degré de complexité inutile pour le scénario de transfert du message HTTP sur le réseau.

Une autre option consiste à utiliser le application/http type de média comme décrit dans la spécification HTTP RFC 7230. Ce type de média utilise exactement le même format que celui utilisé pour envoyer des messages HTTP sur le câble, mais l’intégralité du message peut être placée dans le corps d’une autre requête HTTP. Dans le cas qui nous occupe, nous allons simplement utiliser le corps comme message à envoyer à Event Hubs. Heureusement, il existe un analyseur dans les bibliothèques Microsoft ASP.NET Web API 2.2 Client qui peut analyser ce format et le convertir en objets HttpRequestMessage et HttpResponseMessage natifs.

Pour être en mesure de créer ce message, nous devons utiliser des expressions de stratégie en langage C# dans la Gestion des API Azure. Voici la stratégie, qui envoie un message de requête HTTP à Azure Event Hubs.

<log-to-eventhub logger-id="myapilogger" partition-id="0">
@{
   var requestLine = string.Format("{0} {1} HTTP/1.1\r\n",
                                               context.Request.Method,
                                               context.Request.Url.Path + context.Request.Url.QueryString);

   var body = context.Request.Body?.As<string>(true);
   if (body != null && body.Length > 1024)
   {
       body = body.Substring(0, 1024);
   }

   var headers = context.Request.Headers
                          .Where(h => h.Key != "Authorization" && h.Key != "Ocp-Apim-Subscription-Key")
                          .Select(h => string.Format("{0}: {1}", h.Key, String.Join(", ", h.Value)))
                          .ToArray<string>();

   var headerString = (headers.Any()) ? string.Join("\r\n", headers) + "\r\n" : string.Empty;

   return "request:"   + context.Variables["message-id"] + "\n"
                       + requestLine + headerString + "\r\n" + body;
}
</log-to-eventhub>

Déclaration de stratégie

Il convient d’apporter des précisions sur quelques particularités de cette expression de stratégie. La log-to-eventhub stratégie a un attribut appelé logger-id, qui fait référence au nom de l’enregistreur d’événements créé dans le service Gestion des API. Vous trouverez les détails de la configuration d’un enregistreur d’événements dans le service Gestion des API dans le document How to log events to Azure Event Hubs in Azure API Management. Le second attribut est un paramètre facultatif qui donne à Event Hubs la partition dans laquelle stocker le message. Event Hubs utilise des partitions pour permettre la scalabilité et en nécessite au moins deux. La livraison ordonnée des messages est garantie uniquement au sein d’une partition. Si nous n’indiquons pas à Azure Event Hubs la partition dans laquelle placer le message, il utilise un algorithme de tour de rôle pour répartir la charge. Toutefois, cela peut entraîner le traitement de certains messages en dehors de l’ordre.

Partitions

Pour vous assurer que les messages sont remis aux consommateurs afin de tirer parti de la fonctionnalité de distribution de charge des partitions, nous pouvons envoyer des messages de requête HTTP à une partition et des messages de réponse HTTP à une deuxième partition. Cela garantit une distribution de charge égale et peut garantir que toutes les requêtes et toutes les réponses sont consommées dans l’ordre. Il est possible qu’une réponse soit consommée avant la demande correspondante, mais ce n’est pas un problème, car nous disposons d’un mécanisme différent pour la corrélation des demandes aux réponses et nous savons que les demandes arrivent toujours avant les réponses.

Charges utiles HTTP

Après avoir généré requestLine, vérifiez si le corps de la demande doit être tronqué. Le corps de la demande est tronqué à 1024 uniquement. Cela pourrait être augmenté ; toutefois, les messages de hub d’événements individuels sont limités à 256 Ko, il est donc probable que certains corps de messages HTTP ne pourront pas tenir dans un seul message. Lorsque vous effectuez la journalisation et l’analytique, vous pouvez dériver une quantité importante d’informations provenant uniquement de la ligne et des en-têtes de requête HTTP. De plus, de nombreuses API ne renvoient que de petits corps, et donc la perte de valeur d’information en tronquant les corps volumineux est assez minime par rapport à la réduction des coûts de transfert, de traitement et de stockage pour conserver les contenus.

Une dernière remarque sur le traitement du corps est que nous devons passer true à la As<string>() méthode, car nous lisons le contenu du corps, mais nous voulons également que l’API back-end puisse lire le corps. En mettant cette méthode sur true, nous faisons en sorte que le corps soit mis en mémoire cache et puisse être lu une seconde fois. Cela est important si vous disposez d’une API qui charge des fichiers volumineux ou utilise une interrogation longue. Dans ces cas, il est préférable d’éviter de lire le corps complètement.

En-têtes HTTP

Les en-têtes HTTP peuvent être transférés au format du message sous forme de paire clé/valeur simple. Nous avons choisi de supprimer certains champs sensibles à la sécurité pour éviter de fuite inutile d’informations d’identification. Il est peu probable que les clés d’API et les autres informations d’identification soient utilisés à des fins d’analyse. Si nous souhaitons analyser l’utilisateur et le produit particulier qu’il utilise, nous pourrions obtenir cela à partir de l’objet context et l’ajouter au message.

Métadonnées de message

Lorsque vous créez un message complet à envoyer à l’Event Hub, la première ligne ne fait pas vraiment partie du message application/http. La première ligne est composée de métadonnées supplémentaires pour déterminer si le message est une demande ou un message de réponse et un ID de message qui est utilisé pour corréler les demandes aux réponses. L’ID de message est créé à l’aide d’une autre stratégie qui ressemble à ceci :

<set-variable name="message-id" value="@(Guid.NewGuid())" />

Nous pourrions créer le message de requête, stocker celui-ci dans une variable jusqu’à ce que la réponse soit retournée, puis envoyer la demande et la réponse en tant que message unique. Toutefois, en envoyant la demande et la réponse indépendamment et en utilisant un message-id pour mettre en corrélation les deux, nous obtenons un peu plus de flexibilité dans la taille du message, la possibilité de tirer parti de plusieurs partitions tout en conservant l’ordre des messages, et une demande plus tôt dans notre tableau de bord de journalisation. Il peut également y avoir certains scénarios où une réponse valide n’est jamais envoyée au hub d’événements (éventuellement en raison d’une erreur de requête irrécupérable dans le service Gestion des API), mais nous avons toujours un enregistrement de la demande.

La stratégie d’envoi du message de réponse HTTP étant très similaire à la demande, la configuration de la stratégie terminée ressemble à ce qui suit :

<policies>
  <inbound>
      <set-variable name="message-id" value="@(Guid.NewGuid())" />
      <log-to-eventhub logger-id="myapilogger" partition-id="0">
      @{
          var requestLine = string.Format("{0} {1} HTTP/1.1\r\n",
                                                      context.Request.Method,
                                                      context.Request.Url.Path + context.Request.Url.QueryString);

          var body = context.Request.Body?.As<string>(true);
          if (body != null && body.Length > 1024)
          {
              body = body.Substring(0, 1024);
          }

          var headers = context.Request.Headers
                               .Where(h => h.Key != "Authorization" && h.Key != "Ocp-Apim-Subscription-Key")
                               .Select(h => string.Format("{0}: {1}", h.Key, String.Join(", ", h.Value)))
                               .ToArray<string>();

          var headerString = (headers.Any()) ? string.Join("\r\n", headers) + "\r\n" : string.Empty;

          return "request:"   + context.Variables["message-id"] + "\n"
                              + requestLine + headerString + "\r\n" + body;
      }
  </log-to-eventhub>
  </inbound>
  <backend>
      <forward-request follow-redirects="true" />
  </backend>
  <outbound>
      <log-to-eventhub logger-id="myapilogger" partition-id="1">
      @{
          var statusLine = string.Format("HTTP/1.1 {0} {1}\r\n",
                                              context.Response.StatusCode,
                                              context.Response.StatusReason);

          var body = context.Response.Body?.As<string>(true);
          if (body != null && body.Length > 1024)
          {
              body = body.Substring(0, 1024);
          }

          var headers = context.Response.Headers
                                          .Select(h => string.Format("{0}: {1}", h.Key, String.Join(", ", h.Value)))
                                          .ToArray<string>();

          var headerString = (headers.Any()) ? string.Join("\r\n", headers) + "\r\n" : string.Empty;

          return "response:"  + context.Variables["message-id"] + "\n"
                              + statusLine + headerString + "\r\n" + body;
     }
  </log-to-eventhub>
  </outbound>
</policies>

La stratégie set-variable crée une valeur accessible à la fois par la stratégie log-to-eventhub dans la section <inbound> et la section <outbound>.

Réception d’événements de hubs d’événements

Des événements sont reçus des Azure Event Hubs à l’aide du protocole AMQP (Advance Message Queueing Protocol). L’équipe de Microsoft Service Bus met à disposition les bibliothèques client pour faciliter l’utilisation des événements. Il existe deux approches différentes de prise en charge, l’une par un consommateur Direct et l’autre utilisant la classe EventProcessorHost. Vous trouverez des exemples de ces deux approches dans le référentiel d’exemples Event Hubs. La version courte des différences : Direct Consumer vous donne un contrôle complet, et le EventProcessorHost s'occupe de certaines tâches de fond pour vous, mais suppose certaines méthodes pour traiter ces événements.

EventProcessorHost

Dans cet exemple, nous utilisons le EventProcessorHost pour simplifier, mais ce n’est peut-être pas le meilleur choix pour ce scénario particulier. EventProcessorHost effectue le travail difficile qui consiste à s’assurer que n’avez pas à vous soucier des problèmes de threading dans une classe particulière de processeur d’événements. Toutefois, dans notre scénario, nous convertissons le message dans un autre format et le transmettons à un autre service à l’aide d’une méthode asynchrone. Il n’est pas nécessaire de mettre à jour l’état partagé, et par conséquent, aucun risque de problèmes de gestion des threads. Pour la plupart des scénarios, EventProcessorHost est probablement le meilleur choix, et certainement l’option la plus facile.

IEventProcessor

Le concept central de l’utilisation de EventProcessorHost consiste à créer une implémentation de l’interface IEventProcessor, qui contient la méthode ProcessEventAsync. Voici l’essence de cette méthode :

async Task IEventProcessor.ProcessEventsAsync(PartitionContext context, IEnumerable<EventData> messages)
{

    foreach (EventData eventData in messages)
    {
        _Logger.LogInfo(string.Format("Event received from partition: {0} - {1}", context.Lease.PartitionId,eventData.PartitionKey));

        try
        {
            var httpMessage = HttpMessage.Parse(eventData.GetBodyStream());
            await _MessageContentProcessor.ProcessHttpMessage(httpMessage);
        }
        catch (Exception ex)
        {
            _Logger.LogError(ex.Message);
        }
    }
    ... checkpointing code snipped ...
}

Une liste d’objets EventData est transmise à la méthode et nous exécutons une itération sur cette liste. Les octets de chaque méthode sont analysés dans un objet HttpMessage et cet objet est transmis à une instance de IHttpMessageProcessor.

HttpMessage

L’instance de HttpMessage contient trois éléments de données :

public class HttpMessage
{
    public Guid MessageId { get; set; }
    public bool IsRequest { get; set; }
    public HttpRequestMessage HttpRequestMessage { get; set; }
    public HttpResponseMessage HttpResponseMessage { get; set; }

... parsing code snipped ...

}

L’instance HttpMessage contient une GUID MessageId qui vous permet de connecter la requête HTTP à la réponse HTTP correspondante et une valeur booléenne qui indique si l’objet contient une instance de HttpRequestMessage et HttpResponseMessage. En utilisant les classes intégrées HTTP de System.Net.Http, j’ai été capable de tirer parti du code d’analyse application/http inclus dans System.Net.Http.Formatting.

IHttpMessageProcessor

L’instance HttpMessage est ensuite transmise pour implémentation de IHttpMessageProcessor, qui est une interface que j’ai créée pour découpler la réception et l’interprétation de l’événement des Azure Event Hubs et du traitement réel de celui-ci.

Transfert du message HTTP

Pour cet exemple, nous avons décidé d’envoyer (push) la requête HTTP vers Moesif API Analytics. Moesif est un service cloud spécialisé dans l’analytique et le débogage HTTP. Ils ont un niveau gratuit, donc il est facile d’essayer. Moesif nous permet de voir les requêtes HTTP en temps réel transitant par notre service Gestion des API.

L’implémentation IHttpMessageProcessor ressemble à ce qui suit,

public class MoesifHttpMessageProcessor : IHttpMessageProcessor
{
    private readonly string RequestTimeName = "MoRequestTime";
    private MoesifApiClient _MoesifClient;
    private ILogger _Logger;
    private string _SessionTokenKey;
    private string _ApiVersion;
    public MoesifHttpMessageProcessor(ILogger logger)
    {
        var appId = Environment.GetEnvironmentVariable("APIMEVENTS-MOESIF-APP-ID", EnvironmentVariableTarget.Process);
        _MoesifClient = new MoesifApiClient(appId);
        _SessionTokenKey = Environment.GetEnvironmentVariable("APIMEVENTS-MOESIF-SESSION-TOKEN", EnvironmentVariableTarget.Process);
        _ApiVersion = Environment.GetEnvironmentVariable("APIMEVENTS-MOESIF-API-VERSION", EnvironmentVariableTarget.Process);
        _Logger = logger;
    }

    public async Task ProcessHttpMessage(HttpMessage message)
    {
        if (message.IsRequest)
        {
            message.HttpRequestMessage.Properties.Add(RequestTimeName, DateTime.UtcNow);
            return;
        }

        EventRequestModel moesifRequest = new EventRequestModel()
        {
            Time = (DateTime) message.HttpRequestMessage.Properties[RequestTimeName],
            Uri = message.HttpRequestMessage.RequestUri.OriginalString,
            Verb = message.HttpRequestMessage.Method.ToString(),
            Headers = ToHeaders(message.HttpRequestMessage.Headers),
            ApiVersion = _ApiVersion,
            IpAddress = null,
            Body = message.HttpRequestMessage.Content != null ? System.Convert.ToBase64String(await message.HttpRequestMessage.Content.ReadAsByteArrayAsync()) : null,
            TransferEncoding = "base64"
        };

        EventResponseModel moesifResponse = new EventResponseModel()
        {
            Time = DateTime.UtcNow,
            Status = (int) message.HttpResponseMessage.StatusCode,
            IpAddress = Environment.MachineName,
            Headers = ToHeaders(message.HttpResponseMessage.Headers),
            Body = message.HttpResponseMessage.Content != null ? System.Convert.ToBase64String(await message.HttpResponseMessage.Content.ReadAsByteArrayAsync()) : null,
            TransferEncoding = "base64"
        };

        Dictionary<string, string> metadata = new Dictionary<string, string>();
        metadata.Add("ApimMessageId", message.MessageId.ToString());

        EventModel moesifEvent = new EventModel()
        {
            Request = moesifRequest,
            Response = moesifResponse,
            SessionToken = _SessionTokenKey != null ? message.HttpRequestMessage.Headers.GetValues(_SessionTokenKey).FirstOrDefault() : null,
            Tags = null,
            UserId = null,
            Metadata = metadata
        };

        Dictionary<string, string> response = await _MoesifClient.Api.CreateEventAsync(moesifEvent);

        _Logger.LogDebug("Message forwarded to Moesif");
    }

    private static Dictionary<string, string> ToHeaders(HttpHeaders headers)
    {
        IEnumerable<KeyValuePair<string, IEnumerable<string>>> enumerable = headers.GetEnumerator().ToEnumerable();
        return enumerable.ToDictionary(p => p.Key, p => p.Value.GetEnumerator()
                                                         .ToEnumerable()
                                                         .ToList()
                                                         .Aggregate((i, j) => i + ", " + j));
    }
}

Le MoesifHttpMessageProcessor tire parti d’une bibliothèque d’API C# pour Moesif qui facilite l’envoi des données d’événement HTTP dans leur service. Pour envoyer des données HTTP à l’API Collecteur Moesif, vous avez besoin d’un compte et d’un ID d’application. Vous obtenez un ID d’application Moesif en créant un compte sur le site web de Moesif, puis accédez au menu en haut à droite et sélectionnez Configuration de l’application.

Exemple complet

Le code source et les tests de l’exemple se trouvent sur GitHub. Vous avez besoin d’un service de gestion des API, d’un Event Hub connecté, et d’un compte de stockage pour exécuter l’exemple vous-même.

L’exemple est simplement une application console simple qui écoute les événements provenant d’Event Hub, les convertit en moesif EventRequestModel et EventResponseModel objets, puis les transfère à l’API collecteur Moesif.

Dans l’image animée suivante, vous pouvez voir une demande adressée à une API dans le portail des développeurs, l’application console montrant le message reçu, traité et transféré, puis la demande et la réponse affichées dans le flux d’événements.

Démonstration d’image animée d’une requête transférée à Runscope

Résumé

Le service Gestion des API Azure fournit un emplacement idéal pour capturer le trafic HTTP qui circule vers et depuis vos API. Azure Event Hubs est une solution pouvant être mise à l’échelle et économique permettant de capturer le trafic et de l’intégrer à des systèmes de traitement secondaire pour la journalisation, la surveillance et d’autres analyses sophistiquées. La connexion à des systèmes de surveillance de trafic tiers tels que Moesif se résume à la rédaction de quelques dizaines de lignes de code.