Övervaka dina API:er med Azure API Management, Event Hubs och Moesif

Tjänsten API Management många funktioner för att förbättra bearbetningen av HTTP-begäranden som skickas till http-API:et. Förekomsten av begäranden och svar är dock tillfälligt. Begäran görs och flödar genom tjänsten API Management till ditt backend-API. Ditt API bearbetar begäran och ett svar flödar tillbaka till API-konsumenten. Tjänsten API Management viktig statistik om API:erna för visning i Azure Portal instrumentpanel, men utöver det är informationen borta.

Genom att använda principen log-to-eventhub i API Management-tjänsten kan du skicka all information från begäran och svaret till en Azure Event Hub. Det finns en mängd olika orsaker till varför du kanske vill generera händelser från HTTP-meddelanden som skickas till dina API:er. Några exempel är granskningsloggen för uppdateringar, användningsanalys, undantagsaviseringar och integreringar från tredje part.

Den här artikeln visar hur du samlar in hela HTTP-begäran och svarsmeddelandet, skickar det till en händelsehubb och sedan vidarebefordrar meddelandet till en tjänst från tredje part som tillhandahåller HTTP-loggnings- och övervakningstjänster.

Varför ska jag skicka från API Management Service?

Det är möjligt att skriva HTTP-mellanprogram som kan anslutas till HTTP API-ramverk för att samla in HTTP-begäranden och svar och mata in dem i loggnings- och övervakningssystem. Nackdelen med den här metoden är att HTTP-mellanprogram måste integreras i backend-API:et och måste matcha plattformen för API:et. Om det finns flera API:er måste var och en distribuera mellanprogram. Det finns ofta orsaker till varför backend-API:er inte kan uppdateras.

Med azure-API Management tjänsten för att integrera med loggningsinfrastrukturen får du en centraliserad och plattformsoberoende lösning. Den är också skalbar, delvis på grund av funktionerna för geo-replikering i Azure API Management.

Varför ska jag skicka till en Azure Event Hub?

Det är rimligt att fråga varför man skapar en princip som är specifik för Azure Event Hubs? Det finns många olika platser där jag kanske vill logga mina begäranden. Varför inte bara skicka begäranden direkt till slutmålet? Det är ett alternativ. När du gör loggningsbegäranden från en API Management-tjänst är det dock nödvändigt att överväga hur loggningsmeddelanden påverkar API:ets prestanda. Gradvis ökning av belastningen kan hanteras genom att öka tillgängliga instanser av systemkomponenter eller genom att dra nytta av geo-replikering. Korta trafiktoppar kan dock orsaka att begäranden fördröjs om begäranden om loggning av infrastruktur börjar ta lång tid under belastningen.

Den Azure Event Hubs är utformad för att ingressera stora mängder data, med kapacitet för att hantera ett mycket högre antal händelser än antalet HTTP-begäranden som de flesta API:er bearbetar. Händelsehubben fungerar som en typ av avancerad buffert mellan din API Management-tjänst och infrastrukturen som lagrar och bearbetar meddelandena. Detta säkerställer att API-prestandan inte drabbas av loggningsinfrastrukturen.

När data har skickas till en händelsehubb bevaras de och väntar på att Event Hub-konsumenter bearbetar dem. Händelsehubben bryr sig inte om hur den bearbetas, den bryr sig bara om att se till att meddelandet levereras korrekt.

Event Hubs har möjlighet att strömma händelser till flera konsumentgrupper. Detta gör att händelser kan bearbetas av olika system. Detta möjliggör stöd för många integreringsscenarier utan att lägga till fördröjningar i bearbetningen av API-begäran i API Management-tjänsten eftersom endast en händelse behöver genereras.

En princip för att skicka program-/HTTP-meddelanden

En händelsehubb accepterar händelsedata som en enkel sträng. Innehållet i strängen är upp till dig. För att kunna paketera en HTTP-begäran och skicka den till Event Hubs måste vi formatera strängen med information om begäran eller svar. Om det finns ett befintligt format som vi kan återanvända i sådana här situationer behöver vi kanske inte skriva vår egen parsningskod. Inledningsvis övervägde jag att använda HAR för att skicka HTTP-begäranden och -svar. Det här formatet är dock optimerat för att lagra en sekvens med HTTP-begäranden i ett JSON-baserat format. Den innehöll ett antal obligatoriska element som lade till onödig komplexitet för scenariot med att skicka HTTP-meddelandet via kabel.

Ett alternativ var att använda medietypen application/http enligt beskrivningen i HTTP-specifikationen RFC 7230. Den här medietypen använder exakt samma format som används för att faktiskt skicka HTTP-meddelanden via kabel, men hela meddelandet kan läggas till i brödtexten i en annan HTTP-begäran. I vårt fall ska vi bara använda brödtexten som vårt meddelande för att skicka till Event Hubs. Smidigt finns det en parser i Microsoft ASP.NET Web API 2.2-klientbibliotek som kan parsa det här formatet och konvertera det till inbyggda objekt och HttpRequestMessage HttpResponseMessage .

För att kunna skapa det här meddelandet måste vi dra nytta av C#-baserade principuttryck i Azure API Management. Här är principen, som skickar ett HTTP-begärandemeddelande till 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>

Principdeklaration

Det finns några saker som är värda att nämna om det här principuttrycket. Principen för log-to-eventhub har ett attribut som kallas logger-id, som refererar till namnet på loggaren som har skapats i API Management tjänsten. Information om hur du ställer in en händelsehubbloggare i API Management-tjänsten finns i dokumentet Så här loggar du händelser för att Azure Event Hubs i Azure API Management. Det andra attributet är en valfri parameter som instruerar Event Hubs vilken partition som meddelandet ska lagras i. Event Hubs använder partitioner för att möjliggöra skalbarhet och kräver minst två. Den sorterade leveransen av meddelanden garanteras endast inom en partition. Om vi inte instruerar Händelsehubb i vilken partition meddelandet ska skickas använder den en round-robin-algoritm för att distribuera belastningen. Det kan dock göra att vissa av våra meddelanden bearbetas i ordningsföljd.

Partitioner

För att säkerställa att våra meddelanden levereras till konsumenter i rätt ordning och dra nytta av funktionerna för belastningsutjämning i partitioner, valde jag att skicka HTTP-begärandemeddelanden till en partition och HTTP-svarsmeddelanden till en andra partition. Detta säkerställer en jämn belastningsfördelning och vi kan garantera att alla begäranden förbrukas i ordning och att alla svar förbrukas i ordning. Det är möjligt att ett svar används före motsvarande begäran, men eftersom det inte är ett problem eftersom vi har en annan mekanism för korrelering av begäranden till svar och vi vet att begäranden alltid kommer före svar.

HTTP-nyttolaster

När vi har requestLine byggt kontrollerar vi om begärandetexten ska trunkeras. Begärandetexten trunkeras till endast 1024. Detta kan ökas, men enskilda Event Hub-meddelanden är begränsade till 256 KB, så det är troligt att vissa HTTP-meddelandekroppar inte får plats i ett enda meddelande. När du utför loggning och analys kan en stor mängd information härledas från endast HTTP-begäranderaden och -huvudena. Dessutom returnerar många API:er endast små mängder, så att förlust av informationsvärde genom trunkering av stora mängder är ganska minimal i jämförelse med minskningen av överförings-, bearbetnings- och lagringskostnader för att behålla allt brödtextinnehåll. En sista kommentar om bearbetning av brödtexten är att vi måste skicka till metoden eftersom vi läser innehållet i brödtexten, men vi ville också att api:et för backend skulle kunna true As<string>() läsa brödtexten. Genom att skicka true till den här metoden gör vi att brödtexten buffrar så att den kan läsas en andra gång. Det här är viktigt att känna till om du har ett API som laddar upp stora filer eller använder lång avsökning. I dessa fall är det bäst att undvika att läsa in brödtexten alls.

HTTP-huvuden

HTTP-huvuden kan överföras till meddelandeformatet i ett enkelt nyckel/värde-parformat. Vi har valt att ta bort vissa säkerhetskänsliga fält för att undvika att autentiseringsuppgifter läcker ut i onödan. Det är osannolikt att API-nycklar och andra autentiseringsuppgifter skulle användas i analyssyfte. Om vi vill analysera användaren och den specifika produkt som de använder kan vi hämta det från objektet och context lägga till det i meddelandet.

Metadata för meddelanden

När du skapar det fullständiga meddelandet som ska skickas till händelsehubben är den första raden inte en del av application/http meddelandet. Den första raden är ytterligare metadata som består av om meddelandet är ett begärande- eller svarsmeddelande och ett meddelande-ID, som används för att korrelera begäranden till svar. Meddelande-ID:t skapas med hjälp av en annan princip som ser ut så här:

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

Vi kunde ha skapat begärandemeddelandet, lagrat det i en variabel tills svaret returnerades och sedan skickat begäran och svaret som ett enda meddelande. Men genom att skicka begäran och svaret oberoende av varandra och använda ett meddelande-ID för att korrelera de två, får vi lite mer flexibilitet i meddelandestorleken, möjligheten att dra nytta av flera partitioner samtidigt som meddelandeordningen upprätthålls och begäran visas i vår loggningsinstrumentpanel tidigare. Det kan också finnas vissa scenarier där ett giltigt svar aldrig skickas till händelsehubben, eventuellt på grund av ett allvarligt begärandefel i API Management-tjänsten, men vi har fortfarande en post för begäran.

Principen för att skicka HTTP-svarsmeddelandet liknar begäran, så den fullständiga principkonfigurationen ser ut så här:

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

Principen set-variable skapar ett värde som kan nås av både principen i avsnittet och log-to-eventhub <inbound> <outbound> avsnittet.

Ta emot händelser från Event Hubs

Händelser från Azure Event Hub tas emot med AMQP-protokollet. Microsofts Service Bus har gjort klientbibliotek tillgängliga för att göra de konsumerande händelserna enklare. Det finns två olika metoder som stöds. Den ena är direktkonsument och den andra använder EventProcessorHost klassen . Exempel på dessa två metoder finns i Event Hubs programmeringsguide. Den korta versionen av skillnaderna är, ger dig fullständig kontroll och utför en del av grundarbetet åt dig men gör vissa antaganden om hur Direct Consumer EventProcessorHost du bearbetar dessa händelser.

EventProcessorHost

I det här exemplet använder vi EventProcessorHost för enkelhetens skull, men det kanske inte är det bästa valet för just det här scenariot. EventProcessorHost gör det svåra arbetet med att se till att du inte behöver bekymra dig om trådningsproblem i en viss händelseprocessorklass. Men i vårt scenario konverterar vi bara meddelandet till ett annat format och skicka det vidare till en annan tjänst med hjälp av en asynkron metod. Det finns inget behov av att uppdatera delat tillstånd och därför finns det ingen risk för trådningsproblem. I de flesta fall EventProcessorHost är förmodligen det bästa valet och det är verkligen det enklare alternativet.

IEventProcessor

Det centrala konceptet när du EventProcessorHost använder är att skapa en implementering av IEventProcessor -gränssnittet, som innehåller metoden ProcessEventAsync . Grunden i den här metoden visas här:

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

En lista över EventData-objekt skickas till metoden och vi itererar över listan. Byte för varje metod parsas till ett HttpMessage-objekt och objektet skickas till en instans av IHttpMessageProcessor.

HttpMessage

Instansen HttpMessage innehåller tre datadelar:

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

}

Instansen innehåller ett GUID som gör att vi kan ansluta HTTP-begäran till motsvarande HTTP-svar och ett booleskt värde som identifierar om objektet innehåller en instans av HttpMessage MessageId httpRequestMessage och HttpResponseMessage. Med hjälp av de inbyggda HTTP-klasserna från kunde jag dra nytta av System.Net.Http application/http parsningskoden som ingår i System.Net.Http.Formatting .

IHttpMessageProcessor

Instansen vidarebefordras sedan till implementeringen av , vilket är ett gränssnitt som jag har skapat för att frikoppla mottagande och tolkning av händelsen från Azure Event Hub och den faktiska HttpMessage IHttpMessageProcessor bearbetningen av den.

Vidarebefordra HTTP-meddelandet

I det här exemplet har jag bestämt att det skulle vara intressant att skicka HTTP-begäran till Moesif API Analytics. Moesif är en molnbaserad tjänst som är specialiserad på HTTP-analys och felsökning. De har en kostnadsfri nivå, så det är enkelt att prova och det gör att vi kan se HTTP-begäranden i realtid som flödar genom vår API Management tjänst.

Implementeringen IHttpMessageProcessor ser ut så här:

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

MoesifHttpMessageProcessordrar nytta av ett C# API-bibliotek för Moesif som gör det enkelt att skicka HTTP-händelsedata till tjänsten. För att kunna skicka HTTP-data till Moesif Collector-API:et behöver du ett konto och ett program-ID. Du får ett Moesif-program-ID genom att skapa ett konto på Moesifs webbplats och sedan gå till appkonfigurationen i -> den övre högra menyn.

Fullständigt exempel

Källkoden och testerna för exemplet finns på GitHub. Du behöver en API Management Service, en ansluten händelsehubboch ett Storage-konto för att köra exemplet själv.

Exemplet är bara ett enkelt konsolprogram som lyssnar efter händelser som kommer från Event Hub, konverterar dem till ett Moesif- och -objekt och sedan vidarebefordrar dem vidare till EventRequestModel EventResponseModel Moesif Collector-API:et.

I följande animerade bild kan du se en begäran som görs till ett API i utvecklarportalen, konsolprogrammet som visar meddelandet som tas emot, bearbetas och vidarebefordras och sedan visas begäran och svaret i Event Stream.

Demonstration av begäran som vidarebefordras till Runscope

Sammanfattning

Azure API Management-tjänsten är en perfekt plats för att samla in HTTP-trafik som går till och från dina API:er. Azure Event Hubs är en mycket skalbar och lågkostnadslösning för att samla in trafiken och mata in den i sekundära bearbetningssystem för loggning, övervakning och annan avancerad analys. Att ansluta till trafikövervakningssystem från tredje part som Moesif är lika enkelt som några dussin rader kod.

Nästa steg