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

PLATÍ PRO: Všechny úrovně služby API Management

Služba API Management poskytuje řadu možností, jak vylepšit zpracování požadavků HTTP odesílaných do vašeho rozhraní HTTP API. Existence požadavků a odpovědí je však přechodná. Požadavek se provede a prochází službou API Management do vašeho back-endového rozhraní API. Vaše rozhraní API zpracuje požadavek a odpověď se vrátí zpět příjemci rozhraní API. Služba API Management uchovává některé důležité statistiky o rozhraních API pro zobrazení na řídicím panelu webu Azure Portal, ale kromě toho jsou podrobnosti 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 řada 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 auditu aktualizací, analýzy využití, upozorňování výjimek a integrace třetích stran.

Tento článek ukazuje, jak zachytit celou zprávu požadavku HTTP a 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í PROTOKOLU HTTP.

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

Je možné napsat middleware HTTP, který se může připojit k rozhraním ROZHRANÍ API HTTP za účelem zachycení požadavků a odpovědí HTTP a jejich odesílání do systémů protokolování a monitorování. Nevýhodou tohoto přístupu je, že middleware HTTP musí být integrovaný do back-endového rozhraní API a musí odpovídat platformě rozhraní API. Pokud existuje více rozhraní API, musí každý z nich nasadit middleware. Často existují důvody, proč se back-endová rozhraní API nedají aktualizovat.

Použití služby Azure API Management k integraci s infrastrukturou protokolování 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 vhodné se zeptat, proč vytvořit zásadu specifickou pro Službu Azure Event Hubs? Existuje mnoho různých míst, kde bych mohl chtít protokolovat své žádosti. Proč žádosti neodesílat přímo do konečného cíle? To je možnost. Při provádění žádostí o protokolování ze služby API Management je však nutné zvážit, jak zprávy protokolování ovlivňují výkon rozhraní API. Postupné zvýšení zatížení je možné zvládnout zvýšením dostupných instancí systémových komponent nebo využitím geografické replikace. Krátké špičky provozu však můžou způsobit zpoždění požadavků, pokud se požadavky na protokolování infrastruktury začnou zpomalit při zatížení.

Služba Azure Event Hubs je navržená tak, aby ingress velké objemy dat s kapacitou pro zpracování mnohem vyššího počtu událostí, než je počet požadavků HTTP, které většina procesů 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 výkon rozhraní API nebude trpět kvůli infrastruktuře protokolování.

Jakmile se data předají do centra událostí, zůstanou trvalá a budou čekat na zpracování příjemců centra událostí. Centrum událostí se nezajímá, jak se zpracovává, záleží jen na tom, že se zpráva úspěšně doručí.

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

Zásada pro odesílání zpráv aplikace nebo HTTP

Centrum událostí přijímá data událostí jako jednoduchý řetězec. Obsah tohoto řetězce je na vás. Abychom mohli zabalit požadavek HTTP a odeslat ho do služby Event Hubs, musíme řetězec naformátovat s informacemi o požadavku nebo odpovědi. V takových situacích, pokud existuje existující formát, který můžeme znovu použít, nemusíme psát vlastní parsovací kód. Zpočátku jsem uvažoval o použití HAR pro odesílání požadavků a odpovědí HTTP. Tento formát je však optimalizovaný pro ukládání posloupnosti požadavků HTTP ve formátu založeném na formátu JSON. Obsahoval řadu povinných prvků, které pro scénář předávání zprávy HTTP přes drát přidaly zbytečnou složitost.

Alternativní možností bylo použít application/http typ média, jak je popsáno ve specifikaci HTTP RFC 7230. Tento typ média používá stejný formát, který se používá k skutečnému odesílání zpráv HTTP přes drát, ale celá zpráva může být vložena do textu jiného požadavku HTTP. V našem případě použijeme text jako zprávu k odeslání do služby Event Hubs. Pohodlně existuje analyzátor, který existuje v microsoft ASP.NET webových knihoven API 2.2, které mohou analyzovat tento formát a převést ho na nativní HttpRequestMessage a HttpResponseMessage objekty.

Abychom mohli tuto zprávu vytvořit, musíme ve službě Azure API Management využít výrazy zásad založené na jazyce C#. Tady je zásada, která odešle zprávu požadavku HTTP do služby 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ásada log-to-eventhub má atribut s názvem logger-id, který odkazuje na název protokolovacího nástroje, který byl vytvořen ve službě API Management. Podrobnosti o tom, jak nastavit protokolovací nástroj centra událostí ve službě API Management, najdete v dokumentu Postup protokolování událostí do služby Azure Event Hubs ve službě Azure API Management. Druhý atribut je volitelný parametr, který dává službě Event Hubs pokyn, do kterého oddílu se má zpráva uložit. Služba Event Hubs používá oddíly k povolení škálovatelnosti a vyžaduje minimálně dvě. Seřazené doručování zpráv je zaručené pouze v rámci oddílu. Pokud neudělíme centru událostí, do kterého oddílu se má zpráva umístit, použije k distribuci zatížení algoritmus kruhového dotazování. To ale může způsobit zpracování některých zpráv mimo pořadí.

Oddíly

Abychom zajistili, že se naše zprávy doručují příjemcům v pořadí a využívají možnosti distribuce zatížení oddílů, rozhodl jsem se odesílat 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 budou všechny požadavky spotřebovány v pořadí a všechny odpovědi budou spotřebovány v daném pořadí. Odpověď může být spotřebována před odpovídající žádostí, ale protože to není problém, protože máme jiný mechanismus pro korelaci požadavků na odpovědi a víme, že žádosti vždy přicházejí před odpověďmi.

Datové části HTTP

Po sestavení requestLinezkontrolujeme, jestli se má text požadavku zkrátit. Text požadavku se zkrátí jenom 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ělo zprávy HTTP se nevejde do jedné zprávy. Při protokolování a analýze je možné odvodit značné množství informací pouze z řádku požadavku HTTP a hlaviček. Mnoho rozhraní API také vyžaduje, aby vracely jen malé tělo a ztráta hodnoty informací zkrácením velkých těl je oproti snížení nákladů na přenos, zpracování a ukládání poměrně minimální, aby se zachoval veškerý obsah těla. Poslední poznámka ke zpracování těla spočívá v tom, že musíme předat true As<string>() metodě, protože čteme obsah těla, ale chtěli jsme, aby rozhraní API back-endu bylo schopno přečíst tělo. Předáním true této metodě způsobíme uložení těla do vyrovnávací paměti, aby se mohl číst podruhé. To je důležité vědět, pokud máte rozhraní API, které provádí nahrávání velkých souborů nebo používá dlouhé dotazování. V těchto případech by bylo nejlepší se vyhnout čtení těla vůbec.

Záhlaví HTTP

Hlavičky HTTP lze přenést do formátu zprávy v jednoduchém formátu páru klíč/hodnota. Rozhodli jsme se odstranit určitá pole citlivá na zabezpečení, aby nedocházelo k zbytečně úniku informací o přihlašovacích údajích. Je nepravděpodobné, že by se klíče rozhraní API a další přihlašovací údaje používaly k analytickým účelům. Pokud chceme provést analýzu uživatele a konkrétního produktu, který používá, můžeme to získat z objektu context a přidat ho do zprávy.

Metadata zpráv

Při vytváření úplné zprávy pro odeslání do centra událostí není první řádek ve skutečnosti součástí application/http zprávy. První řádek je další metadata skládající se z toho, jestli je zpráva zprávou žádost nebo odpověď a ID zprávy, která se používá ke korelaci požadavků na odpovědi. ID zprávy se vytvoří pomocí jiné zásady, která vypadá takto:

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

Mohli jsme vytvořit zprávu požadavku, která byla uložena v proměnné, dokud se odpověď nevrátila a pak požadavek a odpověď odeslala jako jednu zprávu. Když ale odešlete požadavek a odpověď nezávisle a použijete ID zprávy ke korelaci těchto dvou, získáme trochu větší flexibilitu ve velikosti zprávy, možnost využít 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 se také stát, že se do centra událostí nikdy neodesílají platná odpověď, pravděpodobně kvůli závažné chybě požadavku ve službě API Management, ale stále máme záznam žádosti.

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

<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, která je přístupná jak log-to-eventhub zásadami v oddílu, <inbound> tak oddílu <outbound> .

Příjem událostí ze služby Event Hubs

Události z Centra událostí Azure se přijímají pomocí protokolu AMQP. Tým Microsoft Service Bus zpřístupnil klientské knihovny, aby byly náročné události jednodušší. Existují dva různé přístupy, jeden je přímým příjemcem a druhý používá EventProcessorHost třídu. Příklady těchto dvou přístupů najdete v průvodci programováním ve službě Event Hubs. Krátká verze rozdílů je, Direct Consumer poskytuje úplnou kontrolu a EventProcessorHost dělá některé z instalatérských prací za vás, ale dělá určité předpoklady o způsobu zpracování těchto událostí.

EventProcessorHost

V této ukázce použijeme EventProcessorHost jednoduchost, ale nemusí být nejlepší volbou pro tento konkrétní scénář. EventProcessorHost pracuje na tom, abyste se ujistili, že se nemusíte starat 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 jiné službě pomocí asynchronní metody. Není nutné aktualizovat sdílený stav, a proto žádné riziko problémů s vlákny. Ve většině scénářů EventProcessorHost je pravděpodobně nejlepší volbou a je to určitě jednodušší možnost.

IEventProcessor

Centrální koncept při použití EventProcessorHost je vytvořit implementaci IEventProcessor rozhraní, která 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á seznam objektů EventData a budeme iterovat přes tento seznam. Bajty každé metody jsou analyzovány do HttpMessage objektu a tento objekt je předán instanci IHttpMessageProcessor.

HttpMessage

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

}

Instance HttpMessage 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 HttpRequestMessage a HttpResponseMessage. Pomocí předdefinovaných tříd HTTP jsem System.Net.Httpbyl schopen využít parsování application/http kódu, který je součástí System.Net.Http.Formatting.

IHttpMessageProcessor

Instance HttpMessage se pak přepošla na implementaci IHttpMessageProcessor, což je rozhraní, které jsem vytvořil pro oddělení příjmu a interpretace události od centra událostí Azure a skutečné zpracování.

Přeposílání zprávy HTTP

Pro tuto ukázku jsem se rozhodl, že by bylo zajímavé odeslat požadavek HTTP do Služby Moesif API Analytics. Moesif je cloudová služba, která se specializuje na analýzy a ladění HTTP. Mají bezplatnou úroveň, takže je snadné ji vyzkoušet a umožňuje nám zobrazit požadavky HTTP v reálném čase procházející přes naši službu API Management.

Implementace IHttpMessageProcessor 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í API jazyka C# pro Moesif , která usnadňuje odesílání dat událostí HTTP do jejich služby. Abyste mohli odesílat data HTTP do rozhraní API kolekce Moesif, potřebujete účet a ID aplikace. ID aplikace Moesif získáte tak, že vytvoříte účet na webu Moesif a pak přejdete do nabídky Vpravo nahoře –> Nastavení aplikace.

Kompletní ukázka

Zdrojový kód a testy pro ukázku jsou na GitHubu. Abyste mohli ukázku spustit sami, potřebujete službu API Management, připojené centrum událostí a účet úložiště.

Ukázka je pouze jednoduchá konzolová aplikace, která naslouchá událostem pocházejícím z centra událostí, převede je na Moesif EventRequestModel a EventResponseModel objekty a pak je předá do rozhraní API kolekce Moesif.

Na následujícím animovaném obrázku uvidíte požadavek na rozhraní API na portálu pro vývojáře, konzolovou aplikaci zobrazující přijetí, zpracování a přeposílání zprávy a pak požadavek a odpověď, které se zobrazují ve službě Event Stream.

Ukázka předání požadavku do Runscopeu

Shrnutí

Služba Azure API Management poskytuje ideální místo pro zachycení provozu HTTP přenášeného do a z vašich rozhraní API. Azure Event Hubs je vysoce škálovatelné a nízkonákladové řešení pro zachycení provozu a jeho předávání do sekundárních systémů zpracování pro protokolování, monitorování a další sofistikované analýzy. Připojení monitorování provozu třetích stran, jako je Moesif, je stejně jednoduché jako několik desítek řádků kódu.

Další kroky