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 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 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 talvez você queira 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 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 registro e monitoramento HTTP.

Por que enviar do serviço de Gerenciamento de API?

Você pode escrever um middleware HTTP que possa ser conectado a estruturas de API HTTP para capturar solicitações e respostas HTTP e enviá-las para sistemas de registro e monitoramento. A desvantagem dessa abordagem é que o middleware HTTP precisa ser integrado à API de back-end e deve corresponder à plataforma de 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 aos Hubs de Eventos do Azure? Há muitos lugares diferentes em que talvez você queira registrar suas solicitações em um log. 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 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 que a maioria das APIs processam. 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. Isso dá suporte a muitos cenários de integração sem colocar mais atrasos 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 aplicação/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 os Hubs de Eventos do Azure, você precisa 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 você possa reutilizar, talvez você não precise escrever seu próprio código de análise. Inicialmente, você pode considerar 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 é 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 usado para realmente enviar mensagens HTTP pelo fio, 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. Aqui está 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 log-to-eventhub política tem um atributo chamado logger-id, que se refere ao nome do registrador criado no serviço de Gerenciamento de API. Você pode encontrar os detalhes de como configurar um agente do hub de eventos no serviço de Gerenciamento de API 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 mensagens sejam processadas fora de ordem.

Partições

Para garantir que as mensagens sejam entregues aos consumidores em ordem e aproveitar a capacidade de distribuição de carga de partições, podemos 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 pode garantir que todas as solicitações e todas as respostas sejam consumidas em ordem. É possível que uma resposta seja consumida antes da solicitação correspondente, mas isso não é um problema porque 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 compilar o requestLine, verifique se o corpo da solicitação deve ser truncado. O corpo da solicitação é truncado a 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 log e análise, você pode derivar uma quantidade significativa de informações apenas da linha de solicitação HTTP e dos cabeçalhos. Além disso, muitas solicitações de API retornam apenas corpos pequenos, portanto, a perda de valor de informação 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 última observação sobre o processamento do corpo é que precisamos passar true para o método As<string>(), pois estamos lendo o conteúdo do corpo, mas também queremos que a API de backend seja capaz de 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. Isso é importante se você tiver uma API que carregue arquivos grandes ou use sondagem longa. 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 remover determinados campos confidenciais de segurança para evitar vazamento desnecessariamente de informações de credenciais. É improvável que chaves de API e outras credenciais sejam usadas para fins analíticos. Se quisermos analisar o usuário e o produto específico que ele está usando, poderemos obtê-lo do context objeto 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 criar a mensagem de solicitação, armazená-la em uma variável até que a resposta seja retornada e, em seguida, enviar 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 os dois, obtemos um pouco mais de flexibilidade no tamanho da mensagem, a capacidade de aproveitar várias partições, mantendo a ordem da mensagem e uma chegada de solicitação mais cedo em nosso painel de log. 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 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 acessível pela política log-to-eventhub na seção <inbound> e pela 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 repositório de exemplos dos Hubs de Eventos. A versão resumida das diferenças: Direct Consumer oferece controle total, e o EventProcessorHost faz parte do trabalho básico para você, mas faz certas suposições sobre como você processa esses eventos.

EventProcessorHost

Neste exemplo, usamos EventProcessorHost para simplicidade, no entanto, pode não ser a melhor escolha 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, convertemos a mensagem em outro formato e a passamos para outro serviço usando um método assíncrono. Não é necessário atualizar o estado compartilhado, 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 interface IEventProcessor que contenha o método ProcessEventAsync. Aqui está a essência desse método:

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 este exemplo, decidimos enviar a solicitação HTTP para o Moesif API Analytics. Moesif é um serviço baseado em nuvem especializado em análise e depuração de HTTP. Eles têm um plano gratuito, então é fácil experimentar. Moesif nos permite ver as solicitações HTTP em tempo real fluindo por meio de nosso serviço de 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 Moesif, você precisa de uma conta e uma ID do aplicativo. Você obtém uma ID de Aplicativo Moesif criando uma conta no site do Moesif e, em seguida, vá para o menu superior direito e selecione 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 provenientes do Hub de Eventos, os converte em objetos Moesif EventRequestModel e EventResponseModel, e em seguida, os encaminha para a API do Coletor Moesif.

Na imagem animada a seguir, você pode ver uma solicitação sendo feita a 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 de imagem animada de uma 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.