Condividi tramite


Monitorare le API con Gestione API di Azure, Hub eventi e Moesif

SI APPLICA A: Tutti i livelli di Gestione API

Il servizio Gestione API offre molte capacità per migliorare l'elaborazione di richieste HTTP inviate all'API HTTP. L'esistenza di richieste e risposte è tuttavia temporanea. La richiesta viene effettuata e passa attraverso al servizio Gestione API fino all'API back-end. L'API elabora la richiesta e una risposta viene restituita al consumer dell'API. Il servizio Gestione API mantiene alcune statistiche importanti sulle API da visualizzare nel dashboard del portale di Azure, ma eventuali altri dettagli verranno eliminati.

Usando i log-to-eventhub criteri nel servizio Gestione API, è possibile inviare qualsiasi dettaglio dalla richiesta e dalla risposta a un Hub eventi di Azure. Esistono diversi motivi per cui è possibile generare eventi dai messaggi HTTP inviati alle API. ad esempio per ottenere audit trail di aggiornamenti, analisi di utilizzo, avvisi relativi alle eccezioni e integrazioni di terze parti.

Questo articolo illustra come acquisire l'intero messaggio di richiesta e risposta HTTP, inviarlo a un hub eventi e quindi inoltrare il messaggio a un servizio di terze parti che fornisce servizi di registrazione e monitoraggio HTTP.

Vantaggi dell'invio dal servizio Gestione API

È possibile scrivere middleware HTTP in grado di collegarsi ai framework API HTTP per acquisire richieste e risposte HTTP e inserirli nei sistemi di registrazione e monitoraggio. Lo svantaggio di questo approccio consiste nel fatto che il middleware HTTP deve essere integrato nell'API back-end e deve corrispondere alla piattaforma dell'API. Se sono disponibili più API, ognuna dovrà distribuire il middleware. Spesso esistono motivi per cui non è possibile aggiornare le API back-end.

L'uso del servizio Gestione API di Azure per l'integrazione con l'infrastruttura di registrazione offre una soluzione centralizzata e indipendente dalla piattaforma, È anche scalabile, in parte grazie alle funzionalità di replica geografica di Azure Gestione API.

Perché inviare a un hub eventi?

È ragionevole chiedersi perché creare un criterio specifico per Hub eventi di Azure? È possibile registrare le richieste in molte posizione diverse. L'invio delle richieste direttamente alla destinazione finale è una delle opzioni disponibili. Tuttavia, quando si effettuano richieste di registrazione da un servizio gestione API, è necessario considerare il modo in cui la registrazione dei messaggi influisce sulle prestazioni dell'API. Gli incrementi graduali nel carico possono essere gestiti da istanze a disponibilità crescente dei componenti di sistema o sfruttando i vantaggi della replica geografica. I brevi picchi di traffico possono tuttavia provocare un ritardo delle richieste se le richieste all'infrastruttura di registrazione iniziano a rallentare a causa del carico.

Hub eventi di Azure è stato progettato per inserire volumi elevati di dati, con una capacità sufficiente per la gestione di un numero di eventi molto più elevato rispetto al numero di richieste HTTP elaborate dalla maggior parte delle API. L'hub eventi funge da tipo di buffer sofisticato tra il servizio Gestione API e l'infrastruttura che archivia ed elabora i messaggi. Ciò garantisce che le prestazioni dell'API non risentiranno a causa dell'infrastruttura di registrazione.

Dopo che i dati sono passati a un hub eventi, vengono mantenuti e attende che i consumer dell'hub eventi lo elaborino. L'hub eventi non importa come viene elaborato, ma è importante assicurarsi che il messaggio venga recapitato correttamente.

Gli hub eventi possono effettuare lo streaming di eventi a più gruppi di consumer. Ciò consente l'elaborazione degli eventi da parte di sistemi diversi. Sarà quindi possibile supportare molti scenari di integrazione senza ritardare ulteriormente l'elaborazione della richiesta API entro il servizio Gestione API, perché è necessario generare solo un evento.

Criterio per l'invio di messaggi applicazione/HTTP

Un hub eventi accetta i dati dell'evento come stringa semplice. I contenuti della stringa possono essere definiti dall'utente. Per poter creare un pacchetto di una richiesta HTTP e inviarla a Hub eventi di Azure, è necessario formattare la stringa con le informazioni sulla richiesta o sulla risposta. In situazioni come questa, se è disponibile un formato esistente, potrebbe non essere necessario scrivere codice di analisi personalizzato. È stata inizialmente valutata la possibilità di usare HAR per l'invio di richieste e risposte HTTP, ma questo formato è ottimizzato per l'archiviazione di una sequenza di richieste HTTP in un formato basato su JSON. Contiene molti elementi obbligatori che hanno aggiunto complessità non necessaria per lo scenario di passaggio del messaggio HTTP in rete.

Un'opzione alternativa consiste nell'usare il tipo di dati multimediali application/http , come illustrato nella specifica HTTP RFC 7230. Questo tipo di dati multimediali usa esattamente lo stesso formato adottato per inviare effettivamente i messaggi HTTP in rete, ma l'intero messaggio può essere inserito nel corpo di un'altra richiesta HTTP. In questo caso, si usa semplicemente il corpo come messaggio per l'invio a Hub eventi. È praticamente disponibile un parser presente nelle librerie client di Microsoft API Web ASP.NET 2.2 che possono analizzare questo formato e convertirlo in oggetti e HttpResponseMessage nativiHttpRequestMessage.

Per potere creare questo messaggio, è necessario sfruttare le espressioni di criteri basate su C# disponibili in Gestione API di Azure. Ecco il criterio che invia un messaggio di richiesta HTTP all'Hub eventi di Azure.

<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>

Dichiarazione di criteri

Ci sono alcune cose particolari che vale la pena menzionare su questa espressione di criteri. Il log-to-eventhub criterio ha un attributo denominato logger-id, che fa riferimento al nome del logger creato all'interno del servizio Gestione API. I dettagli su come configurare un logger dell'hub eventi nel servizio Gestione API sono disponibili nel documento Come registrare gli eventi per Hub eventi di Azure in Azure Gestione API. Il secondo attributo è un parametro opzionale che indica all'Hub eventi la partizione in cui archiviare il messaggio. Hub eventi usa partizioni per abilitare la scalabilità e richiede almeno due. Il recapito ordinato dei messaggi è garantito solo entro una partizione. Se non si indica Hub eventi di Azure in quale partizione inserire il messaggio, usa un algoritmo round robin per distribuire il carico. È tuttavia possibile che ciò provochi l'elaborazione non ordinata di alcuni messaggi.

Partizioni

Per assicurarsi che i messaggi vengano recapitati ai consumer in base all'ordine stabilito e sfruttare i vantaggi della capacità di distribuzione del carico delle partizioni, è possibile scegliere di inviare messaggi di richiesta HTTP a una partizione e messaggi di risposta HTTP a una seconda partizione. In questo modo si assicura una distribuzione uniforme del carico e sarà possibile garantire che tutte le richieste e le risposte vengano utilizzate nell'ordine stabilito. È possibile che una risposta venga usata prima della richiesta corrispondente, ma in quanto non è un problema perché è presente un meccanismo diverso per correlare le richieste alle risposte e sappiamo che le richieste vengono sempre prima delle risposte.

Payload HTTP

Dopo avere compilato requestLine, verificare se il corpo della richiesta deve essere troncato. Il corpo della richiesta viene troncato solo a 1024. Questo potrebbe essere aumentato, tuttavia i singoli messaggi dell'hub eventi sono limitati a 256 KB, quindi è probabile che alcuni corpi di messaggi HTTP non si adattino a un singolo messaggio. Durante la registrazione e l'analisi, è possibile ottenere una quantità significativa di informazioni semplicemente dalla riga e dalle intestazioni della richiesta HTTP. Molte richieste API restituiscono inoltre corpi piccoli, quindi la perdita del valore di informazioni derivante dal troncamento di corpi di grandi dimensioni è abbastanza ridotta rispetto alla riduzione dei costi di trasferimento, elaborazione e archiviazione per la conservazione di tutti i contenuti del corpo. Una nota finale sull'elaborazione del corpo è che è necessario passare true al As<string>() metodo perché si legge il contenuto del corpo, ma è stato anche voluto che l'API back-end sia in grado di leggere il corpo. Se si passa true a questo metodo, il corpo verrà sottoposto a buffering, in modo che sia possibile leggerlo una seconda volta. È importante tenere in considerazione questo aspetto se si usa un'API che carica file di grandi dimensioni o usa polling di lunga durata. In questi casi è consigliabile evitare completamente la lettura del corpo.

Intestazioni HTTP

Le intestazioni HTTP possono essere trasferite nel formato del messaggio sotto forma di semplice coppia chiave/valore. Alcuni campi che richiedono una sicurezza specifica sono stati eliminati per evitare la diffusione non necessaria delle informazioni relative alle credenziali. È improbabile che le chiavi API e altre credenziali vengano usate a scopo di analisi. Se si vuole eseguire analisi sull'utente e sul prodotto specifico in uso, è possibile ottenerlo dall'oggetto context e aggiungerlo al messaggio.

Metadati del messaggio

Quando si compila il messaggio completo da inviare all'hub eventi, il frontline non fa effettivamente parte del application/http messaggio. La prima riga include metadati aggiuntivi che indicano se il messaggio è un messaggio di richiesta o di risposta e l'ID del messaggio, che viene usato per correlare le richieste e l risposte. L'ID del messaggio viene creato mediante un altro criterio, analogo al seguente:

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

È anche possibile creare il messaggio di richiesta, archiviarlo in una variabile fino alla restituzione della risposta e quindi inviare la richiesta e la risposta come singolo messaggio, Tuttavia, inviando la richiesta e la risposta in modo indipendente e usando un message-id per correlare i due, si ottiene una maggiore flessibilità nelle dimensioni del messaggio, la possibilità di sfruttare i vantaggi di più partizioni mantenendo l'ordine dei messaggi e la richiesta verrà visualizzata prima nel dashboard di registrazione. In alcuni scenari è anche possibile che non venga mai inviata all'hub eventi alcuna risposta valida, probabilmente a causa di un errore della richiesta nel servizio Gestione API, ma viene comunque mantenuto un record della richiesta.

Il criterio per l'invio del messaggio di risposta HTTP è simile alla richiesta, quindi la configurazione completa del criterio sarà analoga alla seguente:

<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>

Il criterio set-variable crea un valore accessibile dal criterio log-to-eventhub nella sezione <inbound> e nella sezione <outbound>.

Ricezione di eventi dall'Hub eventi

Gli eventi di Hub eventi di Azure vengono ricevuti usando il protocollo AMQP. Il team del bus di servizio Microsoft ha reso disponibili le librerie client per semplificare l'utilizzo degli eventi. Sono supportati due approcci diversi, ovvero la modalità consumer diretto e l'uso della classe EventProcessorHost. Gli esempi relativi a questi due approcci sono disponibili nella Guida alla programmazione di Hub eventi. In breve, Direct Consumer offre il controllo completo e EventProcessorHost esegue alcune operazioni di base ma include alcune ipotesi in merito al modo in cui gli eventi vengono elaborati.

EventProcessorHost

Per semplificare, in questo esempio verrà usato l'approccio EventProcessorHost, anche se è possibile che non sia ottimale per questo scenario specifico. EventProcessorHost esegue le operazioni necessarie per gestire automaticamente eventuali errori di threading relativi a una classe specifica del processore di eventi. In questo scenario, tuttavia, il messaggio viene semplicemente convertito in un altro formato e passato a un altro servizio usando un metodo asincrono. Non è necessario aggiornare lo stato condiviso e quindi non è necessario alcun rischio di problemi di threading. Per la maggior parte degli scenari, EventProcessorHost è probabilmente la scelta migliore ed è certamente l'opzione più semplice.

IEventProcessor

Il concetto centrale dell'uso di EventProcessorHost consiste nel creare un'implementazione dell'interfaccia IEventProcessor, che include il metodo ProcessEventAsync. Gli elementi fondamentali del metodo sono illustrati di seguito:

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 ...
}

Un elenco di oggetti EventData viene passato al metodo e viene eseguita l'iterazione dell'elenco. I byte di ogni metodo vengono analizzati in un oggetto HttpMessage e questo oggetto viene passato a un'istanza di IHttpMessageProcessor.

HttpMessage

L'istanza HttpMessage contiene tre parti di dati:

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'istanza HttpMessage contiene un GUID MessageId che consente di connettere la richiesta HTTP alla risposta HTTP corrispondente e un valore booleano che indica se l'oggetto contiene un'istanza di HttpRequestMessage e HttpResponseMessage. Usando le classi HTTP predefinite da System.Net.Http, è possibile sfruttare i vantaggi del codice di analisi application/http incluso in System.Net.Http.Formatting.

IHttpMessageProcessor

L'istanza HttpMessage viene quindi inoltrata all'implementazione di IHttpMessageProcessor, che è un'interfaccia creata per separare la ricezione e l'interpretazione dell'evento da Hub eventi di Azure e l'elaborazione effettiva di esso.

Inoltro del messaggio HTTP

Per questo esempio è possibile provare a effettuare il push della richiesta HTTP nel servizio di analisi API Moesif. Moesif è un servizio basato sul cloud specializzato nell'analisi e nel debug HTTP. È disponibile un livello gratuito, quindi è possibile provare a usarlo per visualizzare il passaggio in tempo reale delle richieste HTTP nel servizio Gestione API.

L'implementazione IHttpMessageProcessor ha un aspetto analogo al seguente,

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

MoesifHttpMessageProcessor si avvale di una libreria di API C# per Moesif che facilita il push di dati di eventi HTTP nel proprio servizio. Per inviare dati HTTP all'API di raccolta Moesif sono necessari un account e un ID applicazione. Per ottenere un ID applicazione Moesif, creare un account nel sito Web di Moesif e quindi passare al menu in alto a destra ->Configurazione app.

Esempio completo

Il codice sorgente e i test per l'esempio sono disponibili su GitHub. Per eseguire l'esempio, è necessario disporre di un servizio Gestione API, un hub eventi connesso e un account di archiviazione.

L'esempio è costituito da una semplice applicazione console che rimane in attesa di eventi provenienti dall'Hub eventi, quindi li converte in oggetti Moesif EventRequestModel e EventResponseModel e li inoltra all'API di raccolta Moesif.

Nell'immagine animata seguente è possibile visualizzare una richiesta inviata a un'API nel portale per sviluppatori, l'applicazione console che mostra il messaggio ricevuto, elaborato e inoltrato e quindi la richiesta e la risposta visualizzata nel flusso di eventi.

Illustrazione dell'inoltro di una richiesta a Runscope

Riepilogo

Il servizio Gestione API di Azure è la posizione ideale per acquisire il traffico HTTP verso e dalle API. Hub eventi di Azure è una soluzione a scalabilità elevata e costi ridotti per l'acquisizione e l'inserimento del traffico in sistemi di elaborazione secondari per operazioni di registrazione e monitoraggio e per altre analisi avanzate. Per connettersi a sistemi di monitoraggio del traffico di terze parti come Moesif basta scrivere qualche decina di righe di codice.

Passaggi successivi