Compartilhar via


Monitorar suas APIs com Gerenciamento de API do Azure, os Hubs de Eventos e Moesif

APLICA-SE A: todas as camadas do Gerenciamento de API

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

Ao usar a política log-to-eventhub no serviço Gerenciamento de API, você pode enviar quaisquer detalhes da solicitação e resposta para Hubs de Eventos do Azure. Há vários motivos pelos quais você pode querer gerar eventos a partir de mensagens HTTP 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 mensagem de solicitação e resposta HTTP, enviá-la para um hub de eventos e, em seguida, retransmitir essa mensagem para um serviço de terceiros que fornece serviços de log e monitoramento HTTP.

Por que enviar do serviço Gerenciamento de API?

É possível escrever um middleware HTTP que pode ser conectado a estruturas de API HTTP para capturar solicitações e respostas HTTP e alimentá-las em sistemas de log 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. Geralmente, há motivos pelos quais as APIs de back-end não podem ser atualizadas.

O uso do serviço Gerenciamento de API do Azure para se integrar à infraestrutura de registro em log fornece uma solução centralizada independente de plataforma. Ele também é escaloná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?

É razoável perguntar por que criar uma política específica para Hubs de Eventos do Azure? Há muitos locais diferentes onde eu posso querer registrar em log minhas solicitações. Por que não basta enviar as solicitações 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 caso as solicitações para infraestrutura de registro em log comecem a ficar lentas sob carga.

Os Hubs de Eventos do Azure foram desenvolvidos para acomodar volumes gigantes de dados, com capacidade para lidar com um número muito maior de eventos do que o número de solicitações HTTP processadas pelas APIs. O hub de eventos atua como uma espécie de buffer sofisticado entre seu serviço de gerenciamento de API e a infraestrutura que armazena e processa as mensagens. Isso garante que o desempenho da sua API não seja prejudicado devido à infraestrutura de registro em log.

Depois que os dados são passados para um hub de eventos, eles são mantidos e aguardam o processamento dos consumidores do hub de eventos. O hub de eventos não se importa com a forma como ele é processado, apenas se preocupa em garantir que a mensagem seja entregue com êxito.

Os Hubs de Eventos conseguem transmitir eventos a vários grupos de consumidores. Isso permite que os eventos sejam processados por sistemas diferentes. Desse modo, é possível oferecer suporte a muitos cenários de integração, sem impor atrasos adicionais no processamento da solicitação de API no serviço Gerenciamento de API, pois somente 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. Você é que define o conteúdo dessa cadeia de caracteres. Para poder empacotar uma solicitação HTTP e enviá-la para Hubs de Eventos do Azure, precisamos formatar a cadeia de caracteres com as informações da solicitação ou resposta. Em situações como essa, se houver um formato existente que possamos reutilizar, talvez não precisemos escrever nosso próprio código de análise. Inicialmente, considerei o uso do 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 muitos elementos obrigatórios que adicionavam complexidade desnecessária ao cenário de passagem da mensagem HTTP pela rede.

Uma opção alternativa foi usar o tipo de mídia application/http , como descrito na RFC 7230da especificação HTTP. Esse tipo de mídia usa o mesmo formato que é utilizado para enviar de fato mensagens HTTP por cabo, mas toda a mensagem pode ser colocada no corpo de outra solicitação HTTP. No nosso caso, usamos apenas o corpo como nossa mensagem a ser enviada para Hubs de Eventos. Convenientemente, há um analisador existente nas bibliotecas Microsoft ASP.NET Web API 2.2 Client que pode analisar esse formato e convertê-lo em objetos HttpRequestMessage e HttpResponseMessage nativos.

Para poder criar essa mensagem, precisamos aproveitar as Expressões de política baseadas em C# no Gerenciamento de API do Azure. Veja a política que envia uma mensagem de solicitação HTTP aos Hubs de Eventos do 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>

Declaração de política

Há algumas coisas específicas que vale a pena mencionar sobre esta expressão de política. A política log-to-eventhub tem um atributo chamado logger-id, que se refere ao nome do registrador que foi criado no serviço Gerenciamento de API. Os detalhes sobre como configurar um registrador de hubs de eventos no serviço 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 indica aos Hubs de Eventos em qual partição armazenar a mensagem. Os Hubs de Eventos usam partições para habilitar a escalabilidade e exigem, no mínimo, duas. A entrega ordenada das mensagens é garantida apenas dentro de uma partição. Se não instruirmos os Hubs de Eventos do Azure em qual partição colocar a mensagem, ele usará um algoritmo de balanceamento de carga 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 aproveitar o recurso de distribuição de carga das partições, optei por enviar mensagens de solicitação HTTP a uma partição e mensagens de resposta HTTP a uma segunda partição. Isso garante uma distribuição de carga uniforme e faz com que todas as solicitações e respostas sejam consumidas na ordem certa. É possível que uma resposta seja consumida antes da solicitação correspondente, mas isso não é um problema, pois temos um mecanismo diferente para correlacionar solicitações a respostas e sabemos que as solicitações sempre vêm antes das respostas.

Cargas HTTP

Depois de criar requestLine, verificamos se o corpo da solicitação deve ser truncado. O corpo da solicitação é truncado a apenas 1024. Isso pode ser aumentado, no entanto, 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 registrar em log e analisar, uma quantidade significativa de informações só poderá ser derivada da linha e dos cabeçalhos da solicitação HTTP. Além disso, muitas solicitações de API retornam apenas corpos pequenos e, portanto, a perda do valor das informações ao truncar corpos grandes é razoavelmente mínima em comparação com a redução nos custos de transferir, processar e armazenar para manter todo o conteúdo do corpo. Uma última observação sobre o processamento do corpo é que precisamos passar true para o método As<string>() porque estamos lendo o conteúdo do corpo, mas também queremos que a API de back-end possa ler o corpo. Ao passar true para esse método, fazemos com que o corpo seja armazenado em buffer, de modo que ele possa ser lido uma segunda vez. Será importante lembrar-se disso, caso você tenha uma API que carregue arquivos grandes ou use sondagens longas. Nesses casos, é melhor evitar a leitura do corpo.

Cabeçalhos HTTP

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

Metadados da mensagem

Ao criar a mensagem completa a ser enviada ao hub de eventos, a linha de frente não fará parte da mensagem application/http. A primeira linha é composta de metadados adicionais que consistem em apontar se a mensagem é de solicitação ou de resposta e em uma ID de mensagem usada para correlacionar solicitações com as respostas. A ID da mensagem é criada usando outra política que se parece com esta:

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

Poderíamos ter criado a mensagem de solicitação, tê-la 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 message-id 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 das mensagens e a solicitação aparecerá em nosso painel de logs mais rapidamente. Também pode haver alguns cenários em que uma resposta válida nunca é enviada para o hub de evento, possivelmente devido a um erro fatal de solicitação no serviço Gerenciamento de API, mas ainda ficamos com um registro da solicitação.

A política para enviar a mensagem de resposta HTTP é semelhante à solicitação e, portanto, a configuração da política completa se parece com esta:

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

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

Recebendo eventos dos Hubs de Eventos

Os eventos dos Hubs de Eventos do Azure são recebidos usando o protocolo AMQP. A equipe do Barramento de Serviço da Microsoft disponibilizou bibliotecas de cliente para facilitar o consumo de eventos. Duas abordagens diferentes são aceitas, uma é ser um Consumidor Direto e a outra é usar a classe EventProcessorHost. Exemplos dessas duas abordagens podem ser encontrados no Guia de Programação dos Hubs de Eventos. A principal diferença é: o Direct Consumer dá a você controle total e o EventProcessorHost executa alguns trabalhos para você, mas faz determinadas suposições sobre como esses trabalhos são processados.

EventProcessorHost

Neste exemplo, usamos o EventProcessorHost para simplificar, mas ele pode não ser a melhor opção para esse cenário específico. EventProcessorHost faz o trabalho difícil, para que você não precise se preocupar com problemas de threading em uma classe específica de processador de eventos. No entanto, em nosso cenário, simplesmente convertemos a mensagem para outro formato e a passamos 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 thread. 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 interface IEventProcessor que contenha 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 no método e nós iteramos 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 instância de HttpMessage 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 instância HttpMessage contém um GUID MessageId que nos permite conectar a solicitação HTTP à resposta HTTP correspondente e um valor booliano que identifica se o objeto contém uma instância de HttpRequestMessage e HttpResponseMessage. Ao usar a compilação nas classes HTTP de System.Net.Http, pude aproveitar o código de análise application/http que está incluído em System.Net.Http.Formatting.

IHttpMessageProcessor

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

Encaminhando a mensagem HTTP

Para esse exemplo, decidi que seria interessante enviar a solicitação HTTP para a Análise de API do Moesif. O Moesif é um serviço baseado em nuvem especializado em depuração e análise de HTTP. Ele tem uma camada gratuita para ser fácil testá-lo e nos permite ver as solicitações HTTP em tempo real fluindo pelo nosso serviço Gerenciamento de API.

A implementação de IHttpMessageProcessor se parece com esta:

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 tira proveito de uma biblioteca de API C# para Moesif que facilita o envio por push dos dados de evento de HTTP para o respectivo serviço. Para enviar dados HTTP para a API do coletor do Moesif, você precisa de uma conta e uma ID do aplicativo. Para obter uma ID do aplicativo Moesif, crie uma conta no site do Moesif e, em seguida, acesse Menu Superior Direito ->Configuração do Aplicativo.

Exemplo completo

O código-fonte e os testes do exemplo estão no GitHub. Para executar o exemplo, você precisará de um Serviço de Gerenciamento de API, de um Hub de Eventos conectado e de uma Conta de Armazenamento.

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

Na imagem animada a seguir, você pode ver uma solicitação sendo feita a uma API no Portal do Desenvolvedor, o aplicativo de 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 da solicitação sendo encaminhada para o Runscope

Resumo

O serviço Gerenciamento de API do Azure fornece um lugar ideal para capturar o tráfego HTTP que entra e sai de suas APIs. Os Hubs de Eventos do Azure são uma solução escalonável de baixo custo para capturar esse tráfego e mantê-lo em sistemas de processamento secundários para registro em log, monitoramento e outras análise sofisticadas. A conexão a sistemas de monitoramento de tráfego de terceiros, como o Moesif, usa apenas algumas dezenas de linhas de código.

Próximas etapas