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

GÄLLER FÖR: Alla API Management-nivåer

API Management-tjänsten har 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ällig. Begäran görs och den flödar via API Management-tjänsten till ditt serverdels-API. Ditt API bearbetar begäran och ett svar flödar tillbaka till API-konsumenten. API Management-tjänsten behåller viktig statistik om API:erna för visning på Instrumentpanelen i Azure-portalen, men utöver det är informationen borta.

Genom att använda log-to-eventhub-principen i API Management-tjänsten kan du skicka all information från begäran och svar 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 spårningsspårning av uppdateringar, användningsanalys, undantagsaviseringar och integreringar från tredje part.

Den här artikeln visar hur du avbildar hela HTTP-begäran och svarsmeddelandet, skickar det till en händelsehubb och vidarebefordrar sedan meddelandet till en tredjepartstjänst som tillhandahåller HTTP-loggnings- och övervakningstjänster.

Varför 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-mellanprogrammet måste integreras i serverdels-API:et och måste matcha API:ets plattform. Om det finns flera API:er måste var och en distribuera mellanprogrammet. Det finns ofta anledningar till att serverdels-API:er inte kan uppdateras.

Att använda Azure API Management-tjänsten för att integrera med loggningsinfrastrukturen ger en centraliserad och plattformsoberoende lösning. Den är också skalbar, delvis på grund av geo-replikeringsfunktionerna i Azure API Management.

Varför skicka till en Azure Event Hub?

Det är rimligt att fråga varför du 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. Men när du gör loggningsbegäranden från en API-hanteringstjänst är det nödvändigt att överväga hur loggningsmeddelanden påverkar API:ets prestanda. Gradvisa ökningar av belastningen kan hanteras genom att öka tillgängliga instanser av systemkomponenter eller genom att dra nytta av geo-replikering. Korta trafiktoppar kan dock leda till att begäranden fördröjs om begäranden till loggningsinfrastrukturen börjar bli långsamma under belastningen.

Azure Event Hubs är utformat för att inkommande 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 slags avancerad buffert mellan DIN API-hanteringstjänst och infrastrukturen som lagrar och bearbetar meddelandena. Detta säkerställer att API-prestandan inte påverkas på grund av loggningsinfrastrukturen.

När data har skickats till en händelsehubb sparas de och väntar på att Event Hub-konsumenter ska bearbeta 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 gör det möjligt att stödja 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 situationer kanske vi inte behöver skriva vår egen parsningskod. Ursprungligen ö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 att skicka HTTP-meddelandet över tråden.

Ett annat alternativ var att använda application/http medietypen 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 kabeln, men hela meddelandet kan placeras 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. Det finns en parser som finns i Microsoft ASP.NET Web API 2.2-klientbibliotek som kan parsa det här formatet och konvertera det till inbyggda HttpRequestMessage objekt och HttpResponseMessage objekt.

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 log-to-eventhub har ett attribut som kallas logger-id, som refererar till namnet på den logger som har skapats i API Management-tjänsten. Information om hur du konfigurerar en Händelsehubbloggare i API Management-tjänsten finns i dokumentet Så här loggar du händelser till 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 beställda leveransen av meddelanden garanteras endast inom en partition. Om vi inte instruerar Event Hub i vilken partition meddelandet ska placeras använder den en resursallokeringsalgoritm för att distribuera belastningen. Det kan dock leda till att vissa av våra meddelanden bearbetas i fel ordning.

Partitioner

För att säkerställa att våra meddelanden levereras till konsumenterna i ordning och dra nytta av partitionernas kapacitet för belastningsfördelning 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 används i ordning och att alla svar förbrukas i ordning. Det är möjligt att använda ett svar 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 du har skapat requestLinekontrollerar 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-meddelandeorgan inte får plats i ett enda meddelande. När du utför loggning och analys kan en betydande mängd information härledas från bara HTTP-begäranderaden och rubrikerna. Dessutom returnerar många API:er endast små organ och därför är förlusten av informationsvärdet genom att trunkera stora kroppar 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 anteckning om bearbetning av brödtexten är att vi måste skicka true till As<string>() metoden eftersom vi läser brödtextinnehållet, men ville också att serverdels-API:et skulle kunna läsa brödtexten. Genom att skicka true till den här metoden gör vi att brödtexten buffras så att den kan läsas en andra gång. Detta är viktigt att vara medveten om 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 kroppen alls.

HTTP-rubriker

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 onödigt läckande information om autentiseringsuppgifter. 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 context objektet och lägga till det i meddelandet.

Meddelandemetadata

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 svar 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 bibehålls och begäran visas i loggningsinstrumentpanelen tidigare. Det kan också finnas scenarier där ett giltigt svar aldrig skickas till händelsehubben, möjligen 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-svaret ser ut ungefär 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 är tillgängligt för både log-to-eventhub principen i <inbound> avsnittet och avsnittet <outbound> .

Ta emot händelser från Event Hubs

Händelser från Azure Event Hub tas emot med hjälp av AMQP-protokollet. Microsoft Service Bus-teamet har gjort klientbibliotek tillgängliga för att göra de tidskrävande 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 programmeringsguiden för Event Hubs. Den korta versionen av skillnaderna är, Direct Consumer ger dig fullständig kontroll och EventProcessorHost gör en del av VVS-arbetet åt dig men gör vissa antaganden om hur du bearbetar dessa händelser.

EventProcessorHost

I det här exemplet använder vi för enkelhetens EventProcessorHost skull, men det kanske inte är det bästa valet för det här scenariot. EventProcessorHost gör det hårda arbetet med att se till att du inte behöver oroa dig för trådningsproblem i en viss händelseprocessorklass. Men i vårt scenario konverterar vi bara meddelandet till ett annat format och skickar 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 ingen risk för trådningsproblem. För de flesta scenarier EventProcessorHost är förmodligen det bästa valet och det är verkligen det enklare alternativet.

IEventProcessor

Det centrala konceptet när du använder EventProcessorHost är att skapa en implementering av IEventProcessor gränssnittet, som innehåller metoden ProcessEventAsync. Kärnan i den 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 den listan. Byteen 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 HttpMessage innehåller ett MessageId 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 en HttpRequestMessage och HttpResponseMessage. Genom att använda de inbyggda HTTP-klasserna från System.Net.Httpkunde jag dra nytta av parsningskoden application/http som ingår i System.Net.Http.Formatting.

IHttpMessageProcessor

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

Vidarebefordra HTTP-meddelandet

För det här exemplet bestämde jag mig för att det skulle vara intressant att skicka HTTP-begäran över till Moesif API Analytics. Moesif är en molnbaserad tjänst som specialiserar sig 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 via 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));
    }
}

Dra MoesifHttpMessageProcessor nytta av ett C#API-bibliotek för Moesif som gör det enkelt att skicka HTTP-händelsedata till deras tjänst. 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 den övre högra menyn –> Appinstallation.

Fullständigt exempel

Källkoden och testerna för exemplet finns på GitHub. Du behöver en API Management-tjänst, en ansluten händelsehubb och ett lagringskonto för att kunna köra exemplet själv.

Exemplet är bara ett enkelt konsolprogram som lyssnar efter händelser som kommer från Händelsehubb, konverterar dem till ett Moesif EventRequestModel och EventResponseModel objekt och vidarebefordrar dem sedan till Moesif Collector API.

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 den begäran och det svar som visas i Händelseströmmen.

Demonstration av begäran som vidarebefordras till Runscope

Sammanfattning

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

Nästa steg