Monitore suas APIs com o Gerenciamento de API do Azure, Hubs de Eventos e Moesif

APLICA-SE A: Todas as camadas de gerenciamento de API

O serviço de Gerenciamento de API fornece muitos recursos para aprimorar o processamento de solicitações HTTP enviadas para sua API HTTP. No entanto, a existência dos pedidos e respostas é transitória. A solicitação é feita e flui através do serviço de Gerenciamento de API para sua API de back-end. Sua API processa a solicitação e uma resposta flui de volta para o consumidor da API. O serviço de Gerenciamento de API mantém algumas estatísticas importantes sobre as APIs para exibição no painel do portal do Azure, mas além disso, os detalhes desapareceram.

Usando a política de log para eventhub no serviço de Gerenciamento de API, você pode enviar todos os detalhes da solicitação e resposta para um Hub de Eventos do Azure. Há uma variedade de razões pelas quais você pode querer gerar eventos de mensagens HTTP que estão sendo enviadas para suas APIs. Alguns exemplos incluem trilha de auditoria de atualizações, análise de uso, alerta de exceção e integrações de terceiros.

Este artigo demonstra como capturar toda a solicitação HTTP e mensagem de resposta, enviá-la para um Hub de Eventos e, em seguida, retransmitir essa mensagem para um serviço de terceiros que fornece serviços de registro e monitoramento HTTP.

Por que enviar do API Management Service?

É possível escrever middleware HTTP que pode se conectar a estruturas de API HTTP para capturar solicitações e respostas HTTP e alimentá-las em sistemas de registro e monitoramento. A desvantagem dessa abordagem é que o middleware HTTP precisa ser integrado à API de back-end e deve corresponder à plataforma da API. Se houver várias APIs, cada uma delas deverá implantar o middleware. Muitas vezes, há razões pelas quais as APIs de back-end não podem ser atualizadas.

Usar o serviço de Gerenciamento de API do Azure para integração com a infraestrutura de log fornece uma solução centralizada e independente de plataforma. Também é escalável, em parte devido aos recursos de replicação geográfica do Gerenciamento de API do Azure.

Por que enviar para um Hub de Eventos do Azure?

É razoável perguntar, por que criar uma política específica para os Hubs de Eventos do Azure? Há muitos lugares diferentes onde eu posso querer registrar minhas solicitações. Por que não apenas enviar os pedidos diretamente para o destino final? Essa é uma opção. No entanto, ao fazer solicitações de log de um serviço de gerenciamento de API, é necessário considerar como as mensagens de log afetam o desempenho da API. Os aumentos graduais na carga podem ser tratados aumentando as instâncias disponíveis dos componentes do sistema ou aproveitando a replicação geográfica. No entanto, picos curtos no tráfego podem fazer com que as solicitações sejam atrasadas se as solicitações para a infraestrutura de registro começarem a ficar lentas sob carga.

Os Hubs de Eventos do Azure foram projetados para ingressar grandes volumes de dados, com capacidade para lidar com um número muito maior de eventos do que o número de solicitações HTTP processadas pela maioria das APIs. O Hub de Eventos atua como uma espécie de buffer sofisticado entre o serviço de gerenciamento de API e a infraestrutura que armazena e processa as mensagens. Isso garante que o desempenho da API não será prejudicado devido à infraestrutura de registro.

Depois que os dados forem passados para um Hub de Eventos, eles serão mantidos e aguardarão que os consumidores do Hub de Eventos os processem. O Hub de Eventos não se importa com a forma como é processado, apenas se preocupa em garantir que a mensagem será entregue com sucesso.

Os Hubs de Eventos têm a capacidade de transmitir eventos para vários grupos de consumidores. Isso permite que os eventos sejam processados por diferentes sistemas. Isso permite oferecer suporte a muitos cenários de integração sem colocar atrasos adicionais no processamento da solicitação de API dentro do serviço de Gerenciamento de API, pois apenas um evento precisa ser gerado.

Uma política para enviar mensagens de aplicativo/http

Um Hub de Eventos aceita dados de eventos como uma cadeia de caracteres simples. O conteúdo dessa string depende de você. Para poder empacotar uma solicitação HTTP e enviá-la para Hubs de Eventos, precisamos formatar a cadeia de caracteres com as informações de solicitação ou resposta. Em situações como esta, se houver um formato existente que podemos reutilizar, então talvez não tenhamos que escrever nosso próprio código de análise. Inicialmente considerei usar o HAR para enviar solicitações e respostas HTTP. No entanto, esse formato é otimizado para armazenar uma sequência de solicitações HTTP em um formato baseado em JSON. Ele continha uma série de elementos obrigatórios que adicionavam complexidade desnecessária para o cenário de passar a mensagem HTTP pelo fio.

Uma opção alternativa era usar o application/http tipo de mídia conforme descrito na especificação HTTP RFC 7230. Esse tipo de mídia usa exatamente o mesmo formato que é usado para realmente enviar mensagens HTTP pelo fio, mas a mensagem inteira pode ser colocada no corpo de outra solicitação HTTP. No nosso caso, vamos apenas usar o corpo como nossa mensagem para enviar aos Hubs de Eventos. Convenientemente, há um analisador que existe no Microsoft ASP.NET Web API 2.2 Client bibliotecas que pode analisar esse formato e convertê-lo em nativo HttpRequestMessage e HttpResponseMessage objetos.

Para poder criar essa mensagem, precisamos aproveitar as expressões de Política baseadas em C# no Gerenciamento de API do Azure. Aqui está a política, que envia uma mensagem de solicitação HTTP para os Hubs de Eventos do Azure.

<log-to-eventhub logger-id="conferencelogger" 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>

Declaração de política

Há algumas coisas particulares que merecem ser mencionadas sobre esta expressão política. A política log-to-eventhub tem um atributo chamado logger-id, que se refere ao nome do logger que foi criado dentro do serviço de Gerenciamento de API. Os detalhes de como configurar um logger do Hub de Eventos no serviço de Gerenciamento de API podem ser encontrados no documento Como registrar eventos nos Hubs de Eventos do Azure no Gerenciamento de API do Azure. O segundo atributo é um parâmetro opcional que instrui os Hubs de Eventos em qual partição armazenar a mensagem. Os Hubs de Eventos usam partições para habilitar a escalabilidade e exigem um mínimo de duas. A entrega ordenada de mensagens só é garantida dentro de uma partição. Se não instruirmos o Hub de Eventos em qual partição colocar a mensagem, ele usará um algoritmo round-robin para distribuir a carga. No entanto, isso pode fazer com que algumas das nossas mensagens sejam processadas fora de ordem.

Partições

Para garantir que nossas mensagens sejam entregues aos consumidores em ordem e aproveitem a capacidade de distribuição de carga das partições, optei por enviar mensagens de solicitação HTTP para uma partição e mensagens de resposta HTTP para uma segunda partição. Isso garante uma distribuição de carga uniforme e podemos garantir que todas as solicitações serão consumidas em ordem e todas as respostas serão consumidas em ordem. É possível que uma resposta seja consumida antes do pedido correspondente, mas como isso não é um problema, temos um mecanismo diferente para correlacionar pedidos com respostas e sabemos que os pedidos vêm sempre antes das respostas.

Cargas úteis HTTP

Depois de construir o requestLine, verificamos se o corpo da solicitação deve ser truncado. O corpo da solicitação é truncado para apenas 1024. Isso pode ser aumentado, no entanto, as mensagens individuais do Hub de Eventos são limitadas a 256 KB, portanto, é provável que alguns corpos de mensagens HTTP não caibam em uma única mensagem. Ao fazer registro e análise, uma quantidade significativa de informações pode ser derivada apenas da linha de solicitação HTTP e cabeçalhos. Além disso, muitas APIs solicitam apenas o retorno de corpos pequenos e, portanto, a perda de valor das informações ao truncar corpos grandes é bastante mínima em comparação com a redução nos custos de transferência, processamento e armazenamento para manter todo o conteúdo do corpo. Uma nota final sobre o processamento do corpo é que precisamos passar true para o As<string>() método porque estamos lendo o conteúdo do corpo, mas também queria que a API de back-end fosse capaz de ler o corpo. Ao passar fiel a este método, fazemos com que o corpo seja armazenado em buffer para que possa ser lido uma segunda vez. Isso é importante estar ciente se você tem uma API que faz upload de arquivos grandes ou usa sondagem longa. Nestes casos, o melhor seria evitar a leitura do corpo.

Cabeçalhos de HTTP

Os cabeçalhos HTTP podem ser transferidos para o formato de mensagem em um formato simples de par chave/valor. Optamos por remover certos campos sensíveis à segurança, para evitar o vazamento desnecessário de informações de credenciais. É improvável que chaves de API e outras credenciais sejam usadas para fins de análise. Se quisermos fazer uma análise sobre o usuário e o produto específico que ele está usando, então podemos obter isso do context objeto e adicionar isso à mensagem.

Metadados da mensagem

Ao criar a mensagem completa para enviar ao hub de eventos, a primeira linha não faz parte da application/http mensagem. A primeira linha são metadados adicionais que consistem em saber se a mensagem é uma solicitação ou mensagem de resposta e um ID de mensagem, que é usado para correlacionar solicitações a respostas. O ID da mensagem é criado usando outra política semelhante a esta:

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

Poderíamos ter criado a mensagem de solicitação, armazenado em uma variável até que a resposta fosse retornada e, em seguida, enviado a solicitação e a resposta como uma única mensagem. No entanto, ao enviar a solicitação e a resposta de forma independente e usar um ID de mensagem para correlacionar as duas, obtemos um pouco mais de flexibilidade no tamanho da mensagem, a capacidade de aproveitar várias partições enquanto mantemos a ordem da mensagem e a solicitação aparecerá em nosso painel de registro mais cedo. Também pode haver alguns cenários em que uma resposta válida nunca é enviada para o hub de eventos, possivelmente devido a um erro fatal de solicitação no serviço de Gerenciamento de API, mas ainda temos um registro da solicitação.

A política para enviar a mensagem HTTP de resposta é semelhante à solicitação e, portanto, a configuração completa da política tem esta aparência:

<policies>
  <inbound>
      <set-variable name="message-id" value="@(Guid.NewGuid())" />
      <log-to-eventhub logger-id="conferencelogger" 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="conferencelogger" 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>

A set-variable política cria um valor que é acessível pela log-to-eventhub política na seção e na <inbound><outbound> seção.

Receber eventos de Hubs de Eventos

Os eventos do Hub de Eventos do Azure são recebidos usando o protocolo AMQP. A equipe do Microsoft Service Bus disponibilizou bibliotecas de cliente para facilitar os eventos de consumo. Existem duas abordagens diferentes suportadas, uma é ser um consumidor direto e a outra é usar a EventProcessorHost classe. Exemplos dessas duas abordagens podem ser encontrados no Guia de Programação dos Hubs de Eventos. A versão curta das diferenças é, dá-lhe controle completo e o EventProcessorHost faz parte do trabalho de encanamento para você, Direct Consumer mas faz certas suposições sobre como você processa esses eventos.

EventProcessorHost

Neste exemplo, usamos o EventProcessorHost para simplicidade, no entanto, pode não ser a melhor escolha para este cenário específico. EventProcessorHost faz o trabalho duro de garantir que você não precise se preocupar com problemas de threading dentro de uma classe de processador de eventos específica. No entanto, em nosso cenário, estamos simplesmente convertendo a mensagem para outro formato e passando-a para outro serviço usando um método assíncrono. Não há necessidade de atualizar o estado compartilhado e, portanto, não há risco de problemas de threading. Para a maioria dos cenários, EventProcessorHost é provavelmente a melhor escolha e é certamente a opção mais fácil.

IEventProcessor

O conceito central ao usar EventProcessorHost é criar uma implementação da IEventProcessor interface, que contém o método ProcessEventAsync. A essência desse método é mostrada aqui:

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

Uma lista de objetos EventData é passada para o método e nós iteramos sobre essa lista. Os bytes de cada método são analisados em um objeto HttpMessage e esse objeto é passado para uma instância de IHttpMessageProcessor.

HttpMessage

A HttpMessage instância contém três partes de dados:

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

}

A HttpMessage instância contém um MessageId GUID que nos permite conectar a solicitação HTTP à resposta HTTP correspondente e um valor booleano que identifica se o objeto contém uma instância de HttpRequestMessage e HttpResponseMessage. Usando as classes HTTP internas do System.Net.Http, pude aproveitar o application/http código de análise incluído no System.Net.Http.Formatting.

IHttpMessageProcessor

A HttpMessage instância é então encaminhada para a implementação do IHttpMessageProcessor, que é uma interface que criei para dissociar o recebimento e a interpretação do evento do Hub de Eventos do Azure e o processamento real dele.

Reencaminhando a mensagem HTTP

Para este exemplo, decidi que seria interessante enviar a solicitação HTTP para o Moesif API Analytics. Moesif é um serviço baseado em nuvem especializado em análise HTTP e depuração. Eles têm um nível gratuito, por isso é fácil de tentar e nos permite ver as solicitações HTTP em tempo real fluindo através do nosso serviço de Gerenciamento de API.

A IHttpMessageProcessor implementação tem esta aparência,

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

O MoesifHttpMessageProcessor aproveita uma biblioteca de API C# para Moesif que facilita o envio de dados de eventos HTTP para seu serviço. Para enviar dados HTTP para a API do Coletor Moesif, você precisa de uma conta e uma ID de aplicativo. Você obtém um ID de aplicativo Moesif criando uma conta no site do Moesif e, em seguida, vá para o menu superior direito ->Configuração do aplicativo.

Amostra completa

O código-fonte e os testes para o exemplo estão no GitHub. Você precisa de um Serviço de Gerenciamento de API, um Hub de Eventos conectado e uma Conta de Armazenamento para executar o exemplo por conta própria.

O exemplo é apenas um aplicativo de console simples que escuta eventos provenientes do Hub de Eventos, os converte em um Moesif EventRequestModel e EventResponseModel objetos e, em seguida, os encaminha para a API do Coletor Moesif.

Na imagem animada a seguir, você pode ver uma solicitação sendo feita para uma API no Portal do Desenvolvedor, o aplicativo Console mostrando a mensagem sendo recebida, processada e encaminhada e, em seguida, a solicitação e a resposta aparecendo no Fluxo de Eventos.

Demonstração do pedido que está a ser encaminhado para o Runscope

Resumo

O serviço de Gerenciamento de API do Azure fornece um local ideal para capturar o tráfego HTTP que viaja de e para suas APIs. Os Hubs de Eventos do Azure são uma solução altamente escalável e de baixo custo para capturar esse tráfego e alimentá-lo em sistemas de processamento secundários para registro, monitoramento e outras análises sofisticadas. Conectar-se a sistemas de monitoramento de tráfego de terceiros como o Moesif é tão simples quanto algumas dezenas de linhas de código.

Próximos passos