Поделиться через


Мониторинг API-интерфейсов с помощью Управления API Azure, Центров событий и Moesif

ОБЛАСТЬ ПРИМЕНЕНИЯ: все уровни управления API

Служба управления API предоставляет множество возможностей для улучшения обработки HTTP-запросов, адресованных API-интерфейсу HTTP. Однако запросы и ответы носят временный характер. Запрос отправляется и проходит через службу управления API к вашему серверному API. API обрабатывает запрос и отправляет ответ обратно потребителю API. Служба управления API частично сохраняет важные статистические данные об интерфейсах API и отображает их на панели мониторинга портала Azure, но вся остальная информация теряется безвозвратно.

Используя log-to-eventhub политику в службе Управление API, вы можете отправить все сведения из запроса и ответа на Центры событий Azure. Существует несколько причин, по которым может потребоваться создать события из HTTP-сообщений, отправляемых в API. В частности, это происходит при создании журнала аудита обновлений, анализе использования, оповещении об исключениях или интеграции служб сторонних поставщиков.

В этой статье показано, как записать весь HTTP-запрос и ответное сообщение, отправить его в концентратор событий, а затем передать это сообщение в стороннюю службу, которая предоставляет службы ведения журнала HTTP и мониторинга.

Почему лучше отправлять сообщения из службы управления API?

Можно написать ПО промежуточного слоя HTTP, которое может подключаться к платформам API HTTP для записи HTTP-запросов и ответов, а также передавать их в системы ведения журнала и мониторинга. Но у этого подхода есть недостатки: ПО промежуточного слоя HTTP должно интегрироваться с серверной частью API и соответствовать платформе, на которой размещен API-интерфейс. Если используется несколько интерфейсов API, каждое промежуточное программное обеспечение должно быть развернуто на каждом из них. Часто существуют причины, по которым серверные API не могут быть обновлены.

Служба управления API Azure предоставляет вам централизованное решение для интеграции с инфраструктурой ведения журналов, не зависящее от платформы. Это также масштабируемо, в частности из-за возможностей георепликации Управление API Azure.

Зачем отправляться в концентратор событий?

Разумно спросить, зачем создавать политику, относящуюся к Центры событий Azure? Есть много различных служб, в которых можно регистрировать запросы. Что мешает отправлять запросы сразу конечному получателю? Такой вариант возможен. Однако при выполнении запросов на ведение журнала из службы управления API необходимо учитывать, как сообщения журнала влияют на производительность API. Постепенное увеличение нагрузки можно компенсировать, увеличив количество доступных экземпляров компонентов системы или используя георепликацию. Но короткие пиковые скачки трафика могут привести к задержке запросов, если при увеличении нагрузки замедляется обработка запросов к инфраструктуре ведения журнала.

Центры событий Azure рассчитаны на огромные объемы входящих данных. Их пропускная способность существенно превышает возможности большинства API-процессов по обработке HTTP-запросов. Концентратор событий выступает в качестве сложного буфера между службой управления API и инфраструктурой, в которой хранятся и обрабатываются сообщения. Это гарантирует, что производительность API не будет страдать из-за инфраструктуры ведения журнала.

После того как данные передаются в концентратор событий, они сохраняются и ожидают, пока потребители концентратора событий начнут их обрабатывать. Концентратор событий не беспокоится о способе обработки; его основная задача - обеспечить успешную доставку сообщения.

Центры событий могут выполнять потоковую передачу событий сразу нескольким группам потребителей. Это позволяет обрабатывать события различным системам. Благодаря этому можно реализовать множество сценариев интеграции без увеличения задержек на обработку запросов в службе управления API. Службе в любом случае достаточно создать только одно событие.

Политика отправки сообщений приложений и сообщений HTTP

Концентратор событий принимает данные событий в виде простой строки. Строка данных может содержать произвольное содержимое на ваш выбор. Чтобы упаковать HTTP-запрос и отправить его в Центры событий Azure, необходимо отформатировать строку с информацией о запросе или ответе. В таких ситуациях, если существует существующий формат, который можно использовать повторно, возможно, нам не придется писать собственный код синтаксического анализа. Сначала мне показалось, что для отправки HTTP-запросов и ответов подойдет формат HAR. Но этот формат оптимизирован для сохранения последовательности HTTP-запросов в формате JSON. Он содержал множество обязательных элементов, которые добавили ненужную сложность для сценария передачи HTTP-сообщения через провод.

Еще один вариант — тип носителя application/http , описанный в спецификации HTTP RFC 7230. Этот тип носителя имеет точно такой же формат, как и обычные HTTP-сообщения, отправляемые по сети, но позволяет разместить сообщение целиком в тексте другого HTTP-запроса. В нашем случае мы просто используем тело в качестве сообщения для отправки в Центры событий. Удобно, что в клиентских библиотеках Microsoft ASP.NET Web API 2.2 существует средство синтаксического анализа, которое может анализировать этот формат и преобразовывать его в родные объекты HttpRequestMessage и HttpResponseMessage.

Чтобы создать такое сообщение, следует воспользоваться выражениями политики службы управления API Azure, которые используют синтаксис C#. Приведенная ниже политика будет отправлять сообщение с HTTP-запросом в Центры событий 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>

Объявление политики

Существует несколько конкретных вещей, которые стоит упомянуть об этом выражении политики. Политика log-to-eventhub имеет атрибутlogger-id, который ссылается на имя средства ведения журнала, созданного в службе Управление API. Сведения о настройке регистратора концентратора событий в службе управления API см. в документе "Как регистрировать события в Azure Event Hub с помощью Azure API Management". Второй (необязательный) атрибут сообщает Центрам событий, в каком разделе нужно хранить сообщение. Центры событий используют секции для обеспечения масштабируемости и требуют не менее двух. Упорядоченная доставка сообщений гарантируется только в пределах раздела. Если мы не указываем для Azure Event Hubs, в каком разделе размещать сообщение, он использует алгоритм циклического перебора для распределения нагрузки. Но это может привести к тому, что сообщения будут обрабатываться не по порядку.

Разделы

Чтобы обеспечить правильный порядок доставки сообщений потребителям, не теряя при этом преимущества распределения нагрузки по разделам, я решил отправлять сообщения с HTTP-запросами в один раздел, а сообщения с ответами — в другой. Это обеспечивает равномерное распределение нагрузки и одновременно гарантирует, что все запросы и ответы обрабатываются по порядку. Возможно использование ответа до соответствующего запроса, но это не проблема, так как у нас есть другой механизм сопоставления запросов с ответами, и мы знаем, что запросы всегда предшествуют ответам.

Полезная нагрузка HTTP

После создания requestLine мы проверяем, нужно ли обрезать текст запроса. Длина текста запроса ограничена 1024 символами. Это может быть увеличено, однако отдельные сообщения центра обработки событий ограничены 256 КБ, поэтому, скорее всего, некоторые тела сообщений HTTP не уместятся в одном сообщении. При ведении журнала и анализе значительный объем информации можно получить уже из строки запроса HTTP и заголовка. Кроме того, многие API возвращают запросы с небольшими объемами данных, поэтому потеря информации при усечении больших объемов данных почти незаметна по сравнению с уменьшением затрат на передачу, обработку и хранение всего содержимого данных. Одна из последних заметок об обработке тела заключается в том, что нам нужно передать true методу As<string>(), так как мы считываем содержимое тела, но также необходимо, чтобы бэкенд API мог читать тело. Если передать в этот метод значение true, текст запроса сохраняется в буфере и может быть прочитан повторно. Важно об этом помнить, если API выполняет загрузку файлов большого объема или использует длинный опрос. В таких случаях лучше вовсе не читать текст полностью.

Заголовки HTTP

HTTP-заголовки можно поместить в сообщение в простом формате пар "ключ— значение". Мы решили исключить из обработки некоторые поля с секретной информацией, чтобы избежать утечки учетных данных. Вряд ли ключи API и другие учетные данные будут использоваться для анализа. Если мы хотим выполнить анализ для пользователя и конкретного продукта, который они используют, мы можем получить это из context объекта и добавить его в сообщение.

Метаданные сообщения

При формировании полного сообщения для отправки в концентратор событий передний план фактически не входит в состав application/http сообщения. В первой строке содержатся дополнительные метаданные. Они указывают, является ли сообщение запросом или ответом, и содержат идентификатор сообщения, по которому можно сопоставить запросы и ответы. Идентификатор сообщения создается с помощью другой политики, которая выглядит следующим образом:

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

Мы могли бы создать сообщение с запросом, сохранить его в переменной до получения ответа, а затем отправить запрос и ответ в одном сообщении. Тем не менее, отправляя запрос и ответ раздельно и используя message-id для их корреляции, мы получаем большую гибкость в размере сообщений, возможность использовать несколько разделов, сохраняя порядок сообщений, и запрос появится в нашей панели мониторинга журналирования быстрее. Также возможны ситуации, когда допустимый ответ вообще не отправляется в шлюз событий, возможно, из-за неустранимой ошибки запроса в службе управления API, но у нас все еще есть запись о запросе.

Политика отправки HTTP-сообщения с ответом похожа на политику для запроса, поэтому полная конфигурация политики выглядит так:

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

Политика set-variable создает значение, которое политика log-to-eventhub может использовать в разделах <inbound> и <outbound>.

Получение событий от Центров событий

События из Центра событий Azure принимаются с помощью протокола AMQP. Команда служебной шины Microsoft предоставила в открытом доступе клиентские библиотеки, которые упрощают обработку событий. Служба поддерживает два различных подхода: быть прямым потребителем или использовать класс EventProcessorHost. Примеры использования этих подходов можно найти в руководстве по программированию Центров событий. Вкратце рассмотрим различия между ними. Direct Consumer дает вам полный контроль, а EventProcessorHost берет часть работы на себя, но делает некоторые предположения о том, как вы обрабатываете события.

EventProcessorHost

В этом примере мы для простоты используем EventProcessorHost, хоть это и не лучший выбор в данном случае. EventProcessorHost берет на себя управление потоками внутри конкретного класса обработчика событий, чтобы вам не пришлось об этом беспокоиться. Однако в нашем сценарии мы просто преобразуем сообщение в другой формат и передаем его в другую службу с помощью асинхронного метода. Нет необходимости обновлять общее состояние, и, следовательно, отсутствует риск проблем с потоками. Для большинства сценариев EventProcessorHost вероятно является лучшим выбором и, безусловно, более простым вариантом.

IEventProcessor

Основной задачей при использовании EventProcessorHost является реализация интерфейса IEventProcessor, который содержит метод ProcessEventAsync. Суть этого метода показана ниже.

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

Список объектов EventData передается в метод, и мы итерируем по этому списку. Содержимое каждого метода преобразуется в объект HttpMessage, и этот объект передается в экземпляр IHttpMessageProcessor.

HttpMessage

Экземпляр HttpMessage содержит три фрагмента данных:

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

}

Экземпляр HttpMessage содержит идентификатор GUID MessageId, который позволяет связать HTTP-запрос с соответствующим ответом, а также логическое значение, которое сообщает, содержит ли этот объект экземпляр HttpRequestMessage и HttpResponseMessage. Я применил встроенные классы HTTP из System.Net.Http, что позволило воспользоваться кодом синтаксического анализа application/http, который включен в System.Net.Http.Formatting.

IHttpMessageProcessor

Затем экземпляр HttpMessage перенаправляется в реализацию IHttpMessageProcessor, который является интерфейсом, созданным мной для разделения получения и интерпретации события от Azure Event Hubs и его фактической обработки.

Пересылка сообщения HTTP

Мне показалось интересным использовать в нашем примере отправку HTTP-запроса в API Analytics Moesif. Moesif — это облачная служба, которая специализируется на анализе и отладке протокола HTTP. У службы есть бесплатный уровень, поэтому ее легко испытать в работе. Мы сможем в реальном времени отслеживать HTTP-запросы, которые проходят через службу управления API.

Реализация IHttpMessageProcessor выглядит следующим образом:

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 использует библиотеку API C# для Moesif, которая упрощает передачу данных событий HTTP в их службу. Для отправки данных HTTP в API сборщика Moesif, вам потребуется учетная запись и идентификатор приложения. Идентификатор приложения Moesif можно получить, создав учетную запись на веб-сайте Moesif, а затем выбрав в меню справа вверху ->Настройка приложения.

Полный пример

Исходный код и тесты для этого примера доступны на сайте GitHub. Чтобы запустить этот пример, вам потребуются служба управления API, подключенный к ней концентратор событий и учетная запись хранения.

Пример представляет собой простое консольное приложение, которое прослушивает события, поступающие от концентратора событий, преобразует их в объекты Moesif EventRequestModel и EventResponseModel, а затем пересылает их на API сборщика Moesif.

На следующем анимированном изображении вы увидите запрос, сделанный в API на портале разработчика, консольное приложение, показывающее получение, обработку и пересылку сообщения, а затем запрос и ответ, отображаемый в потоке событий.

Демонстрация передачи запроса в Runscope

Итоги

Служба управления API Azure— это идеальное место для фиксации HTTP-трафика, поступающего на API-интерфейсы и в обратном направлении. Центры событий Azure — это высокомасштабируемое недорогое решение, которое собирает информацию о трафике и передает ее в системы дополнительной обработки для регистрации, мониторинга и сложного анализа. Для подключения к сторонним системам мониторинга трафика, например Moesif, потребуется всего несколько десятков строк кода.