Мониторинг 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 предоставляет вам централизованное решение для интеграции с инфраструктурой ведения журналов, не зависящее от платформы. Это также масштабируемо, в частности из-за возможностей георепликации Azure Управление API.
Зачем отправляться в концентратор событий?
Разумно спросить, зачем создавать политику, относящуюся к Центры событий 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 веб-API ASP.NET 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 в Azure Управление API". Второй (необязательный) атрибут сообщает Центрам событий, в каком разделе нужно хранить сообщение. Центры событий используют секции для обеспечения масштабируемости и требуют не менее двух. Правильная последовательность доставки сообщений гарантируется только в пределах раздела. Если мы не определяем Центры событий Azure, в каком разделе будет размещаться сообщение, он использует алгоритм циклического перебора для распределения нагрузки. Но это может привести к тому, что сообщения будут обрабатываться не по порядку.
Секции
Чтобы обеспечить правильный порядок доставки сообщений потребителям, не теряя при этом преимущества распределения нагрузки по разделам, я решил отправлять сообщения с 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 и фактической обработки.
Пересылка сообщения 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 на портале разработчика, консольное приложение, показывающее получение, обработку и пересылку сообщения, а затем запрос и ответ, отображаемый в потоке событий.
Итоги
Служба управления API Azure— это идеальное место для фиксации HTTP-трафика, поступающего на API-интерфейсы и в обратном направлении. Центры событий Azure — это высокомасштабируемое недорогое решение, которое собирает информацию о трафике и передает ее в системы дополнительной обработки для регистрации, мониторинга и сложного анализа. Для подключения к сторонним системам мониторинга трафика, например Moesif, потребуется всего несколько десятков строк кода.
Следующие шаги
- Дополнительные сведения о Центры событий Azure
- Дополнительные сведения об интеграции Управление API и Центров событий