Monitorowanie interfejsów API za pomocą usług Azure API Management, Event Hubs i Moesif

DOTYCZY: Wszystkie warstwy usługi API Management

Usługa API Management udostępnia wiele funkcji w celu zwiększenia przetwarzania żądań HTTP wysyłanych do interfejsu API HTTP. Jednak istnienie żądań i odpowiedzi jest przejściowe. Żądanie jest wykonywane i przepływa przez usługę API Management do interfejsu API zaplecza. Interfejs API przetwarza żądanie i odpowiedź przepływa z powrotem do odbiorcy interfejsu API. Usługa API Management przechowuje ważne statystyki dotyczące interfejsów API do wyświetlania na pulpicie nawigacyjnym witryny Azure Portal, ale poza tym szczegóły nie zostały wyświetlone.

Korzystając z zasad log-to-eventhub w usłudze API Management, możesz wysłać wszelkie szczegóły z żądania i odpowiedzi na usługę Azure Event Hub. Istnieje wiele powodów, dla których możesz chcieć wygenerować zdarzenia z komunikatów HTTP wysyłanych do interfejsów API. Niektóre przykłady obejmują dziennik inspekcji aktualizacji, analizę użycia, alerty wyjątków i integracje innych firm.

W tym artykule pokazano, jak przechwycić całe żądanie HTTP i komunikat odpowiedzi, wysłać go do centrum zdarzeń, a następnie przekazać ten komunikat do usługi innej firmy, która zapewnia usługi rejestrowania i monitorowania HTTP.

Dlaczego warto wysłać z usługi API Management?

Istnieje możliwość zapisania oprogramowania pośredniczącego HTTP, które może podłączyć się do struktur interfejsu API HTTP w celu przechwytywania żądań HTTP i odpowiedzi oraz podawania ich do systemów rejestrowania i monitorowania. Wadą tego podejścia jest to, że oprogramowanie pośredniczące HTTP musi zostać zintegrowane z interfejsem API zaplecza i musi być zgodne z platformą interfejsu API. Jeśli istnieje wiele interfejsów API, każdy z nich musi wdrożyć oprogramowanie pośredniczące. Często istnieją powody, dla których nie można zaktualizować interfejsów API zaplecza.

Korzystanie z usługi Azure API Management do integracji z infrastrukturą rejestrowania zapewnia scentralizowane i niezależne od platformy rozwiązanie. Jest również skalowalna, częściowo ze względu na możliwości replikacji geograficznej usługi Azure API Management.

Dlaczego warto wysłać do centrum zdarzeń platformy Azure?

Warto zapytać, dlaczego warto utworzyć zasady specyficzne dla usługi Azure Event Hubs? Istnieje wiele różnych miejsc, w których mogę chcieć rejestrować moje żądania. Dlaczego nie tylko wysyłać żądania bezpośrednio do końcowego miejsca docelowego? Jest to opcja. Jednak podczas rejestrowania żądań z usługi API Management należy wziąć pod uwagę wpływ rejestrowania komunikatów na wydajność interfejsu API. Stopniowy wzrost obciążenia można obsłużyć przez zwiększenie dostępnych wystąpień składników systemu lub wykorzystanie replikacji geograficznej. Jednak krótkie skoki ruchu mogą spowodować opóźnienie żądań, jeśli żądania rejestrowania infrastruktury zaczynają spowalniać pod obciążeniem.

Usługa Azure Event Hubs jest przeznaczona do ściągnięcia ogromnych ilości danych z pojemnością do obsługi znacznie większej liczby zdarzeń niż liczba żądań HTTP, które przetwarza większość interfejsów API. Centrum zdarzeń działa jako rodzaj zaawansowanego buforu między usługą API Management a infrastrukturą, która przechowuje i przetwarza komunikaty. Gwarantuje to, że wydajność interfejsu API nie będzie cierpieć z powodu infrastruktury rejestrowania.

Po przekazaniu danych do centrum zdarzeń są utrwalane i będą czekać na przetworzenie ich przez użytkowników centrum zdarzeń. Centrum zdarzeń nie obchodzi, w jaki sposób jest przetwarzany, po prostu dba o upewnienie się, że komunikat zostanie pomyślnie dostarczony.

Usługa Event Hubs ma możliwość przesyłania strumieniowego zdarzeń do wielu grup odbiorców. Dzięki temu zdarzenia mogą być przetwarzane przez różne systemy. Umożliwia to obsługę wielu scenariuszy integracji bez wprowadzania dodatkowych opóźnień w przetwarzaniu żądania interfejsu API w usłudze API Management, ponieważ należy wygenerować tylko jedno zdarzenie.

Zasady wysyłania komunikatów aplikacji/http

Centrum zdarzeń akceptuje dane zdarzeń jako prosty ciąg. Zawartość tego ciągu jest do Ciebie. Aby móc spakować żądanie HTTP i wysłać je do usługi Event Hubs, musimy sformatować ciąg przy użyciu informacji o żądaniu lub odpowiedzi. W takich sytuacjach, jeśli istnieje istniejący format, który możemy użyć ponownie, być może nie będziemy musieli pisać własnego kodu analizy. Początkowo rozważałem użycie har do wysyłania żądań HTTP i odpowiedzi. Jednak ten format jest zoptymalizowany pod kątem przechowywania sekwencji żądań HTTP w formacie opartym na formacie JSON. Zawierał szereg obowiązkowych elementów, które dodały niepotrzebną złożoność scenariusza przekazywania komunikatu HTTP przez przewody.

Alternatywną opcją było użycie typu nośnika application/http zgodnie z opisem w specyfikacji HTTP RFC 7230. Ten typ nośnika używa dokładnie tego samego formatu, który jest używany do rzeczywistego wysyłania komunikatów HTTP za pośrednictwem przewodu, ale cały komunikat można umieścić w treści innego żądania HTTP. W naszym przypadku po prostu użyjemy treści jako komunikatu do wysłania do usługi Event Hubs. Wygodnie istnieje analizator, który istnieje w bibliotece klienta microsoft ASP.NET Web API 2.2, które mogą analizować ten format i konwertować go na obiekty natywne HttpRequestMessage i HttpResponseMessage .

Aby móc utworzyć ten komunikat, musimy skorzystać z wyrażeń zasad opartych na języku C# w usłudze Azure API Management. Poniżej przedstawiono zasady, które wysyła komunikat żądania HTTP do usługi Azure Event Hubs.

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

Deklaracja zasad

Istnieje kilka konkretnych kwestii, o których warto wspomnieć o tym wyrażeniu zasad. Zasady log-to-eventhub mają atrybut o nazwie logger-id, który odwołuje się do nazwy rejestratora, który został utworzony w usłudze API Management. Szczegółowe informacje na temat konfigurowania rejestratora centrum zdarzeń w usłudze API Management można znaleźć w dokumencie Jak rejestrować zdarzenia w usłudze Azure Event Hubs w usłudze Azure API Management. Drugi atrybut to opcjonalny parametr, który instruuje usługę Event Hubs, w której partycji ma być przechowywany komunikat. Usługa Event Hubs używa partycji, aby umożliwić skalowalność i wymagać co najmniej dwóch. Uporządkowana dostawa komunikatów jest gwarantowana tylko w ramach partycji. Jeśli nie poinstruujemy centrum zdarzeń, w którym partycja ma umieścić komunikat, używa algorytmu działania okrężnego do dystrybucji obciążenia. Może to jednak spowodować przetworzenie niektórych komunikatów poza zamówieniem.

Partycje

Aby upewnić się, że nasze komunikaty są dostarczane do użytkowników w kolejności i korzystają z możliwości dystrybucji obciążenia partycji, zdecydowałem się wysyłać komunikaty żądań HTTP do jednej partycji i komunikatów odpowiedzi HTTP do drugiej partycji. Zapewnia to równomierną dystrybucję obciążenia i możemy zagwarantować, że wszystkie żądania będą używane w kolejności, a wszystkie odpowiedzi są używane w kolejności. Istnieje możliwość, aby odpowiedź była zużywana przed odpowiednim żądaniem, ale ponieważ nie jest to problem, ponieważ mamy inny mechanizm korelowania żądań do odpowiedzi i wiemy, że żądania zawsze pojawiają się przed odpowiedziami.

Ładunki HTTP

Po utworzeniu elementu requestLinesprawdzamy, czy treść żądania powinna zostać obcięta. Treść żądania jest obcięta tylko do 1024. Można to zwiększyć, jednak poszczególne komunikaty centrum zdarzeń są ograniczone do 256 KB, więc prawdopodobnie niektóre treści komunikatów HTTP nie mieszczą się w jednym komunikacie. Podczas rejestrowania i analizy można uzyskać znaczną ilość informacji tylko z wiersza i nagłówków żądania HTTP. Ponadto wiele interfejsów API żąda tylko małych ciał i dlatego utrata wartości informacji przez obcięcie dużych ciał jest dość minimalna w porównaniu z zmniejszeniem kosztów transferu, przetwarzania i magazynowania, aby zachować całą zawartość treści. Jedną z ostatnich uwag dotyczących przetwarzania treści jest to, że musimy przekazać true do As<string>() metody, ponieważ odczytujemy zawartość treści, ale interfejs API zaplecza chciał, aby można było odczytać treść. Przekazując wartość true do tej metody, powodujemy buforację treści, aby można było ją odczytać po raz drugi. Ważne jest, aby pamiętać o tym, czy masz interfejs API, który przekazuje duże pliki lub używa długiego sondowania. W takich przypadkach najlepiej byłoby unikać czytania ciała w ogóle.

Nagłówki HTTP

Nagłówki HTTP można przenosić do formatu komunikatu w prostym formacie pary klucz/wartość. Postanowiliśmy usunąć niektóre pola poufne zabezpieczeń, aby uniknąć niepotrzebnego wycieku informacji o poświadczeniach. Jest mało prawdopodobne, aby klucze interfejsu API i inne poświadczenia były używane do celów analitycznych. Jeśli chcemy przeprowadzić analizę użytkownika i konkretnego używanego produktu, możemy uzyskać to z context obiektu i dodać go do komunikatu.

Metadane komunikatu

Podczas kompilowania kompletnego komunikatu do wysłania do centrum zdarzeń pierwszy wiersz nie jest faktycznie częścią komunikatu application/http . Pierwszy wiersz to dodatkowe metadane składające się z tego, czy komunikat jest żądaniem, czy komunikatem odpowiedzi i identyfikatorem komunikatu, który służy do korelowania żądań z odpowiedziami. Identyfikator komunikatu jest tworzony przy użyciu innych zasad, które wyglądają następująco:

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

Moglibyśmy utworzyć komunikat żądania, przechowywany w zmiennej do momentu zwrócenia odpowiedzi, a następnie wysłania żądania i odpowiedzi jako pojedynczego komunikatu. Jednak wysyłając żądanie i odpowiedź niezależnie i używając identyfikatora komunikatu w celu skorelowania tych dwóch, uzyskamy nieco większą elastyczność w rozmiarze komunikatu, możliwość korzystania z wielu partycji przy zachowaniu kolejności komunikatów i żądania pojawi się wcześniej na pulpicie nawigacyjnym rejestrowania. Mogą również wystąpić pewne scenariusze, w których prawidłowa odpowiedź nigdy nie jest wysyłana do centrum zdarzeń, prawdopodobnie z powodu błędu żądania krytycznego w usłudze API Management, ale nadal mamy rekord żądania.

Zasady wysyłania komunikatu HTTP odpowiedzi wyglądają podobnie do żądania, więc kompletna konfiguracja zasad wygląda następująco:

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

Zasady set-variable tworzą wartość dostępną zarówno log-to-eventhub przez zasady w sekcji, jak <inbound> i w <outbound> sekcji.

Odbieranie zdarzeń z usługi Event Hubs

Zdarzenia z usługi Azure Event Hub są odbierane przy użyciu protokołu AMQP. Zespół usługi Microsoft Service Bus udostępnił biblioteki klienckie, aby ułatwić korzystanie z zdarzeń. Obsługiwane są dwa różne podejścia: jeden jest klientem bezpośrednim , a drugi korzysta z EventProcessorHost klasy . Przykłady tych dwóch podejść można znaleźć w Przewodniku programowania usługi Event Hubs. Krótka wersja różnic jest, daje pełną kontrolę i EventProcessorHost wykonuje niektóre prace hydrauliczne dla Ciebie, Direct Consumer ale sprawia, że pewne założenia dotyczące sposobu przetwarzania tych zdarzeń.

EventProcessorHost

W tym przykładzie używamy elementu EventProcessorHost dla uproszczenia, jednak może nie być najlepszym wyborem dla tego konkretnego scenariusza. EventProcessorHost Wykonuje ciężką pracę, upewniając się, że nie musisz martwić się o problemy wątkowe w określonej klasie procesora zdarzeń. Jednak w naszym scenariuszu po prostu konwertujemy komunikat na inny format i przekazujemy go do innej usługi przy użyciu metody asynchronicznej. Nie ma potrzeby aktualizowania stanu udostępnionego i dlatego nie ma ryzyka problemów z wątkami. W przypadku większości scenariuszy EventProcessorHost jest to prawdopodobnie najlepszy wybór i z pewnością jest to łatwiejsza opcja.

IEventProcessor

Centralną koncepcją podczas używania EventProcessorHost jest utworzenie implementacji interfejsu IEventProcessor , która zawiera metodę ProcessEventAsync. Istota tej metody jest pokazana tutaj:

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

Lista obiektów EventData jest przekazywana do metody i iterujemy tę listę. Bajty każdej metody są analizowane w obiekcie HttpMessage i ten obiekt jest przekazywany do wystąpienia klasy IHttpMessageProcessor.

HttpMessage

Wystąpienie HttpMessage zawiera trzy fragmenty danych:

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

}

Wystąpienie HttpMessage zawiera MessageId identyfikator GUID, który umożliwia nam połączenie żądania HTTP z odpowiednią odpowiedzią HTTP i wartością logiczną, która identyfikuje, czy obiekt zawiera wystąpienie httpRequestMessage i HttpResponseMessage. Korzystając z wbudowanych klas HTTP z System.Net.Httpklasy , byłem w stanie skorzystać z application/http kodu analizowania, który jest zawarty w pliku System.Net.Http.Formatting.

IHttpMessageProcessor

Wystąpienie HttpMessage jest następnie przekazywane do implementacji IHttpMessageProcessorelementu , który jest interfejsem utworzonym w celu oddzielenia odbierania i interpretacji zdarzenia z usługi Azure Event Hub oraz rzeczywistego przetwarzania go.

Przekazywanie komunikatu HTTP

W tym przykładzie zdecydowałem, że warto wypchnąć żądanie HTTP do usługi Moesif API Analytics. Moesif to usługa oparta na chmurze, która specjalizuje się w analizie i debugowaniu HTTP. Mają warstwę bezpłatną, więc można łatwo wypróbować i umożliwić nam wyświetlanie żądań HTTP w czasie rzeczywistym przepływających za pośrednictwem naszej usługi API Management.

Implementacja IHttpMessageProcessor wygląda następująco:

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

Funkcja MoesifHttpMessageProcessor korzysta z biblioteki interfejsu API języka C# dla środowiska Moesif , która ułatwia wypychanie danych zdarzeń HTTP do usługi. Aby wysyłać dane HTTP do interfejsu API modułu zbierającego Moesif, potrzebne jest konto i identyfikator aplikacji. Otrzymasz identyfikator aplikacji Moesif, tworząc konto w witrynie internetowej Moesif, a następnie przejdź do menu u góry po prawej stronie —> Konfiguracja aplikacji.

Kompletny przykład

Kod źródłowy i testy dla przykładu znajdują się w witrynie GitHub. Aby samodzielnie uruchomić przykład, potrzebujesz usługi API Management, połączonego centrum zdarzeń i konta magazynu.

Przykład jest tylko prostą aplikacją konsolową, która nasłuchuje zdarzeń pochodzących z centrum zdarzeń, konwertuje je na moesif EventRequestModel i EventResponseModel obiekty, a następnie przekazuje je do interfejsu API modułu zbierającego Moesif.

Na poniższej animowanej ilustracji można zobaczyć żądanie wysyłane do interfejsu API w portalu deweloperów, w aplikacji konsolowej z wyświetlonym komunikatem odbieranym, przetworzonym i przesłanym dalej, a następnie żądanie i odpowiedź wyświetlaną w strumieniu zdarzeń.

Demonstracja przekazywania żądania do elementu Runscope

Podsumowanie

Usługa Azure API Management zapewnia idealne miejsce do przechwytywania ruchu HTTP przychodzącego do i z interfejsów API. Usługa Azure Event Hubs to wysoce skalowalne, tanie rozwiązanie do przechwytywania tego ruchu i przekazywania go do pomocniczych systemów przetwarzania na potrzeby rejestrowania, monitorowania i innych zaawansowanych analiz. Połączenie do systemów monitorowania ruchu innych firm, takich jak Moesif, jest tak proste, jak kilkadziesiąt wierszy kodu.

Następne kroki