Monitorování rozhraní API pomocí Azure API Management, Event Hubs a Moesif

Služba API Management poskytuje mnoho možností pro vylepšení zpracování požadavků HTTP odeslaných do rozhraní HTTP API. Existence požadavků a odpovědí je ale přechodná. Požadavek je proveden a prochází přes službu API Management do rozhraní API back-endu. Vaše rozhraní API zpracuje požadavek a odpověď se vrátí zpět ke spotřebiteli rozhraní API. Služba API Management uchovává důležité statistiky o rozhraních API pro zobrazení na řídicím panelu Azure Portal, ale podrobnosti jsou pryč.

Pomocí zásad log-to-eventhub ve službě API Management můžete odeslat jakékoli podrobnosti z požadavku a odpovědi do centra událostí Azure. Existuje několik důvodů, proč můžete chtít generovat události ze zpráv HTTP odesílaných do vašich rozhraní API. Mezi příklady patří záznam pro audit aktualizací, analýza využití, upozornění na výjimky a integrace třetích stran.

Tento článek ukazuje, jak zachytit celý požadavek HTTP a zprávu odpovědi, odeslat ji do centra událostí a pak tuto zprávu předat službě třetí strany, která poskytuje služby protokolování a monitorování HTTP.

Proč odesílat z API Management služby?

Je možné napsat middleware HTTP, který se může připojit k rozhraním HTTP API, aby zachytil požadavky a odpovědi HTTP a předal je do systémů protokolování a monitorování. Nevýhodou tohoto přístupu je, že middleware HTTP je potřeba integrovat do rozhraní API back-endu a musí odpovídat platformě rozhraní API. Pokud existuje více rozhraní API, musí každé z nich middleware nasadit. Často existují důvody, proč není možné aktualizovat rozhraní API back-endu.

Použití služby Azure API Management k integraci s protokolovací infrastrukturou poskytuje centralizované řešení nezávislé na platformě. Je také škálovatelný, částečně kvůli možnostem geografické replikace služby Azure API Management.

Proč odesílat do centra událostí Azure?

Je rozumné se zeptat, proč vytvořit zásadu specifickou pro Azure Event Hubs? Žádosti je možné protokolovat na mnoha různých místech. Proč neposílejte požadavky přímo do konečného cíle? To je možnost. Při vytváření požadavků na protokolování ze služby API Management je však potřeba zvážit, jak protokolování zpráv ovlivňuje výkon rozhraní API. Postupné nárůsty zatížení je možné zvládnout zvýšením dostupných instancí systémových komponent nebo s výhodou geografické replikace. Krátké špičky v provozu ale mohou způsobit zpoždění požadavků v případě, že se požadavky na protokolovací infrastrukturu začnou při zatížení zpomalovat.

Rozhraní Azure Event Hubs navrženo pro příchozí přenos velkých objemů dat s kapacitou pro zpracování mnohem vyššího počtu událostí, než je počet požadavků HTTP, které většina rozhraní API zpracovává. Centrum událostí funguje jako druh sofistikované vyrovnávací paměti mezi vaší službou API Management a infrastrukturou, která ukládá a zpracovává zprávy. Tím se zajistí, že kvůli infrastruktuře protokolování nebude docílněn výkon rozhraní API.

Po přenosu dat do centra událostí se data zachovaná a počká, až je uživatelé centra událostí zpracují. Centru událostí je jedno, jak se zpracovává, jenom se stará o to, aby se zpráva úspěšně doručila.

Event Hubs má možnost streamovat události do více skupin uživatelů. To umožňuje zpracování událostí různými systémy. To umožňuje podporu mnoha scénářů integrace, aniž by se při zpracování požadavku rozhraní API ve službě API Management prodlevy, protože je potřeba vygenerovat pouze jednu událost.

Zásady pro odesílání zpráv aplikace/HTTP

Centrum událostí přijímá data událostí jako jednoduchý řetězec. Obsah tohoto řetězce je na vás. Aby bylo možné zabalit požadavek HTTP a odeslat ho do Event Hubs, musíme řetězec naformátovat pomocí informací o požadavku nebo odpovědi. V takových situacích, pokud existuje formát, který můžeme znovu použít, nemusíme psát vlastní parsovací kód. Nejprve jsem zvažoval použití souboru HAR k odesílání požadavků a odpovědí HTTP. Tento formát je ale optimalizovaný pro ukládání posloupnosti požadavků HTTP ve formátu založeném na FORMÁTU JSON. Obsahovala řadu povinných prvků, které přidávají zbytečné složitosti pro scénář předávání zprávy HTTP přes přenos.

Alternativní možností bylo použít typ média, jak je popsáno ve specifikaci application/http HTTP RFC 7230. Tento typ média používá přesně stejný formát, který se používá k posílání zpráv HTTP přes přenos, ale celá zpráva může být v těle jiného požadavku HTTP. V našem případě jenom použijeme tělo jako zprávu k odeslání do Event Hubs. Pohodlně je k dispozici analyzátor, který existuje v klientských knihovnách rozhraní Microsoft ASP.NET Web API 2.2, který dokáže parsovat tento formát a převést ho na nativní objekty a HttpRequestMessage HttpResponseMessage .

Aby bylo možné tuto zprávu vytvořit, musíme využít výrazy zásad založené na jazyce C# v Azure API Management. Tady je zásada, která odešle zprávu požadavku HTTP do 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>

Deklarace zásad

O tomto výrazu zásad stojí za zmínku několik konkrétních věcí. Zásady log-to-eventhub mají atribut s názvem logger-id, který odkazuje na název protokolovacího nástroje, který byl vytvořen v rámci API Management služby. Podrobnosti o tom, jak nastavit protokolovací nástroj centra událostí ve službě API Management, najdete v dokumentu Jak protokolovat události do Azure Event Hubs v Azure API Management. Druhý atribut je volitelný parametr, který Event Hubs, do kterého oddílu se má zpráva uložit. Event Hubs používá oddíly k umožnění škálovatelnosti a vyžaduje minimálně dva oddíly. Seřazené doručování zpráv je zaručeno pouze v rámci oddílu. Pokud centru událostí nesvědčíme, do kterého oddílu se má zpráva umístit, použije se k distribuci zatížení algoritmu kruhového dotazování. To ale může způsobit, že se některé zprávy zpracují mimo pořadí.

Oddíly

Abychom zajistili, že se naše zprávy doručí spotřebitelům v pořadí a využili možnosti distribuce zatížení oddílů, rozhodli jsme se odeslat zprávy požadavku HTTP do jednoho oddílu a zpráv odpovědí HTTP do druhého oddílu. Tím se zajistí rovnoměrně rozdělení zatížení a můžeme zaručit, že všechny požadavky budou spotřebovány v pořadí a všechny odpovědi budou spotřebovány v pořadí. Odpověď může být spotřebována před odpovídajícím požadavkem, ale protože to není problém, protože máme jiný mechanismus pro korelaci požadavků na odpovědi a víme, že požadavky vždy přicházejí před odpověďmi.

Datové části HTTP

Po sestavení zkontrolujeme, jestli se má text požadavku requestLine oříznou. Text požadavku se zkrátí pouze na 1024. To se může zvýšit, ale jednotlivé zprávy centra událostí jsou omezené na 256 kB, takže je pravděpodobné, že některá těla zpráv HTTP se nevejdou do jedné zprávy. Při protokolování a analýze může být velké množství informací odvozeno pouze z řádku požadavku HTTP a hlaviček. Mnoho požadavků rozhraní API navíc vrací pouze malá těla, takže ztráta informační hodnoty zkrácením velkých textů je v porovnání se snížením nákladů na přenos, zpracování a úložiště poměrně minimální, aby se uchoval veškerý obsah těla. Poslední poznámka ke zpracování těla je, že musíme metodě předat , protože čteme obsah těla, ale zároveň jsme chtěli, aby back-endové rozhraní API dokázalo tělo true As<string>() přečíst. Předáním true této metodě způsobíme ukládání těla do vyrovnávací paměti, aby bylo možné ho podruhé přečíst. To je důležité vědět, pokud máte rozhraní API, které nahrává velké soubory nebo používá dlouhé cyklické dotazování. V těchto případech by bylo nejlepší se vůbec vyhnout čtení těla.

Hlavičky PROTOKOLU HTTP

Hlavičky PROTOKOLU HTTP je možné přenést do formátu zprávy v jednoduchém formátu páru klíč/hodnota. Rozhodli jsme se prozrazení určitých polí citlivých na zabezpečení, aby se zabránilo zbytečnému úniku informací o přihlašovacích údajůch. Je nepravděpodobné, že by se pro analytické účely použily klíče rozhraní API a další přihlašovací údaje. Pokud chceme provést analýzu uživatele a konkrétního produktu, který používá, můžeme ho získat z objektu a přidat context ho do zprávy.

Metadata zpráv

Při sestavování úplné zprávy pro odeslání do centra událostí není první řádek ve skutečnosti součástí application/http zprávy. První řádek obsahuje další metadata, která se skládají z toho, jestli se jedná o zprávu požadavku nebo odpovědi, a ID zprávy, které se používá ke korelaci požadavků s odpověďmi. ID zprávy se vytvoří pomocí jiné zásady, která vypadá podobně jako tato:

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

Mohli jsme vytvořit zprávu požadavku, uložit ji do proměnné až do vrácení odpovědi a pak odeslat požadavek a odpověď jako jednu zprávu. Odesláním požadavku a odpovědi nezávisle na sobě a použitím ID zprávy ke korelaci těchto dvou zpráv ale získáte o něco větší flexibilitu velikosti zprávy, možnost využívat více oddílů při zachování pořadí zpráv a požadavek se zobrazí na řídicím panelu protokolování dříve. Může také dojít k situacím, kdy se do centra událostí nikdy neodesílala platná odpověď, pravděpodobně kvůli závažné chybě požadavku ve službě API Management, ale stále máme záznam požadavku.

Zásada pro odeslání zprávy HTTP odpovědi vypadá podobně jako požadavek, takže kompletní konfigurace zásad vypadá takhle:

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

Zásada set-variable vytvoří hodnotu, ke které má přístup log-to-eventhub zásada v oddílu i v oddílu <inbound> <outbound> .

Příjem událostí z Event Hubs

Události z centra událostí Azure se přijímou pomocí protokolu AMQP. Tým Microsoftu Service Bus, aby klientské knihovny byly k dispozici, aby byly náročnější události jednodušší. Podporují se dva různé přístupy, jeden je přímý příjemce a druhý používá EventProcessorHost třídu . Příklady těchto dvou přístupů najdete v průvodci programováním Event Hubs. Krátká verze rozdílů je, že poskytuje úplnou kontrolu a provádí některé práce při instalaci, ale vytváří určité předpoklady o tom, jak tyto Direct Consumer EventProcessorHost události zpracováváte.

EventProcessorHost

V této ukázce používáme pro jednoduchost , ale nemusí to být nejlepší EventProcessorHost volba pro tento konkrétní scénář. EventProcessorHost je obtížné zajistit, abyste si obavy o problémy s vlákny v rámci konkrétní třídy procesoru událostí. V našem scénáři ale jednoduše převádíme zprávu do jiného formátu a předáváme ji do jiné služby pomocí asynchronní metody. Není nutné aktualizovat sdílený stav, a proto není nutné riskovat problémy s vlákny. Ve většině EventProcessorHost scénářů je pravděpodobně nejlepší volbou a určitě je to jednodušší možnost.

IEventProcessor

Centrální koncept při použití je vytvořit implementaci rozhraní , která EventProcessorHost IEventProcessor obsahuje metodu ProcessEventAsync . Podstata této metody je znázorněna zde:

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

Do metody se předává seznam objektů EventData a my se na tento seznam opakuje. Bajty každé metody jsou analyzovány do objektu HttpMessage a tento objekt je předán instanci IHttpMessageProcessor.

HttpMessage

HttpMessageInstance obsahuje tři části dat:

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

}

HttpMessageInstance obsahuje MessageId identifikátor GUID, který nám umožňuje připojit požadavek HTTP k odpovídající odpovědi HTTP a logickou hodnotu, která identifikuje, zda objekt obsahuje instanci třídy zprávy HttpRequestMessage a HttpResponseMessage. Pomocí integrovaných tříd HTTP z System.Net.Http , jsem dokázal využít application/http analýzu kódu, který je součástí System.Net.Http.Formatting .

IHttpMessageProcessor

HttpMessageInstance se pak přepošle do implementace IHttpMessageProcessor , což je rozhraní, které jsem vytvořil, aby se odložilo přijímání a interpretace události z centra událostí Azure a jejich skutečné zpracování.

Předávání zprávy HTTP

V této ukázce jsme se rozhodli, že byste do služby MOESIF API Analyticsmohli odeslat požadavek HTTP. Moesif je cloudová služba, která se specializuje na službu HTTP Analytics a ladění. Mají bezplatnou úroveň, takže se snadno pokusíte a můžeme zobrazit požadavky HTTP v toku v reálném čase prostřednictvím naší služby API Management.

IHttpMessageProcessorImplementace vypadá takto.

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

Využívá MoesifHttpMessageProcessor knihovnu rozhraní C# API pro Moesif , která usnadňuje vkládání dat událostí http do služby. Aby bylo možné odesílat data HTTP do rozhraní API Moesif collector, potřebujete účet a ID aplikace. ID aplikace Moesif získáte vytvořením účtu na webu Moesif a následným přechodem k nastavení aplikace v pravém horním rohu -> .

Kompletní ukázka

Zdrojový kód a testy pro ukázku jsou na GitHub. potřebujete službu API Management, připojené centrum událostía účet Storage ke spuštění ukázky pro sebe sama.

Ukázka je pouze jednoduchá Konzolová aplikace, která naslouchá událostem přicházejících z centra událostí, převádí je do Moesif EventRequestModel a EventResponseModel objektů a pak je předávají do rozhraní API kolekce Moesif.

Na následujícím animovaném obrázku vidíte na portálu pro vývojáře požadavek na rozhraní API, konzolová aplikace zobrazuje zprávu, která se přijímá, zpracovává a předává, a pak požadavek a odpověď zobrazený v datovém proudu událostí.

Ukázka přesměrovaného požadavku na Runscope

Souhrn

Služba Azure API Management poskytuje ideální místo pro zachycení provozu HTTP na cestách a z vašich rozhraní API. Azure Event Hubs je vysoce škálovatelné řešení s nízkými náklady pro zachytávání provozu a jejich krmení do sekundárních zpracovatelských systémů pro protokolování, monitorování a další propracované analýzy. Připojení k systémům monitorování provozu třetích stran, jako je Moesif, je jednoduché jako několik desítek řádků kódu.

Další kroky