Delen via


Uw API's bewaken met Azure API Management, Event Hubs en Moesif

VAN TOEPASSING OP: Alle API Management-lagen

De API Management-service biedt veel mogelijkheden om de verwerking van HTTP-aanvragen die naar uw HTTP-API worden verzonden, te verbeteren. Het bestaan van de aanvragen en antwoorden is echter tijdelijk. De aanvraag wordt gedaan en loopt via de API Management-service naar uw back-end-API. Uw API verwerkt de aanvraag en een antwoord loopt terug naar de API-consument. De API Management-service bewaart belangrijke statistieken over de API's die moeten worden weergegeven in het Dashboard van Azure Portal, maar verder zijn de details verdwenen.

Met behulp van het logboek-naar-eventhub-beleid in de API Management-service kunt u alle details van de aanvraag en reactie naar een Azure Event Hub verzenden. Er zijn verschillende redenen waarom u mogelijk gebeurtenissen wilt genereren van HTTP-berichten die naar uw API's worden verzonden. Enkele voorbeelden zijn audittrail van updates, gebruiksanalyse, uitzonderingswaarschuwingen en integraties van derden.

In dit artikel wordt gedemonstreert hoe u het volledige HTTP-aanvraag- en antwoordbericht vastlegt, verzendt naar een Event Hub en dat bericht vervolgens doorstuurt naar een service van derden die HTTP-logboekregistratie en -bewakingsservices biedt.

Waarom verzenden vanuit API Management Service?

Het is mogelijk om HTTP middleware te schrijven die kan worden aangesloten op HTTP API-frameworks om HTTP-aanvragen en -antwoorden vast te leggen en deze in logboekregistratie- en bewakingssystemen in te voeren. Het nadeel van deze benadering is dat de HTTP-middleware moet worden geïntegreerd in de back-end-API en moet overeenkomen met het platform van de API. Als er meerdere API's zijn, moet elke API de middleware implementeren. Vaak zijn er redenen waarom back-end-API's niet kunnen worden bijgewerkt.

Het gebruik van de Azure API Management-service voor integratie met logboekregistratie-infrastructuur biedt een gecentraliseerde en platformonafhankelijke oplossing. Het is ook schaalbaar, deels vanwege de geo-replicatiemogelijkheden van Azure API Management.

Waarom verzenden naar een Azure Event Hub?

Het is redelijk om te vragen waarom u een beleid maakt dat specifiek is voor Azure Event Hubs? Er zijn veel verschillende plaatsen waar ik mijn aanvragen kan registreren. Waarom verstuurt u de aanvragen niet rechtstreeks naar de uiteindelijke bestemming? Dat is een optie. Wanneer u echter logboekregistratieaanvragen van een API Management-service maakt, moet u overwegen hoe logboekberichten van invloed zijn op de prestaties van de API. Geleidelijke toename van de belasting kan worden verwerkt door het verhogen van beschikbare exemplaren van systeemonderdelen of door gebruik te maken van geo-replicatie. Korte pieken in het verkeer kunnen er echter toe leiden dat aanvragen worden vertraagd als aanvragen voor het registreren van de infrastructuur langzaam worden belast.

De Azure Event Hubs is ontworpen om enorme hoeveelheden gegevens binnen te halen, met capaciteit voor het verwerken van een veel hoger aantal gebeurtenissen dan het aantal HTTP-aanvragen dat de meeste API's verwerken. De Event Hub fungeert als een soort geavanceerde buffer tussen uw API Management-service en de infrastructuur waarin de berichten worden opgeslagen en verwerkt. Dit zorgt ervoor dat uw API-prestaties niet te lijden hebben vanwege de infrastructuur voor logboekregistratie.

Zodra de gegevens zijn doorgegeven aan een Event Hub, blijven deze behouden en wachten tot de Event Hub-gebruikers deze verwerken. De Event Hub maakt niet uit hoe het wordt verwerkt, het geeft alleen om ervoor te zorgen dat het bericht wordt bezorgd.

Event Hubs heeft de mogelijkheid om gebeurtenissen naar meerdere consumentengroepen te streamen. Hierdoor kunnen gebeurtenissen door verschillende systemen worden verwerkt. Hierdoor kunnen veel integratiescenario's worden ondersteund zonder dat er extra vertragingen zijn opgetreden bij de verwerking van de API-aanvraag in de API Management-service, omdat er slechts één gebeurtenis hoeft te worden gegenereerd.

Een beleid voor het verzenden van toepassings-/HTTP-berichten

Een Event Hub accepteert gebeurtenisgegevens als een eenvoudige tekenreeks. De inhoud van die tekenreeks is aan u. Als u een HTTP-aanvraag wilt verpakken en naar Event Hubs wilt verzenden, moet u de tekenreeks opmaken met de aanvraag- of antwoordgegevens. In situaties zoals deze, als er een bestaande indeling is die we opnieuw kunnen gebruiken, hoeven we mogelijk niet onze eigen parseringscode te schrijven. In eerste instantie heb ik overwogen de HAR te gebruiken voor het verzenden van HTTP-aanvragen en -antwoorden. Deze indeling is echter geoptimaliseerd voor het opslaan van een reeks HTTP-aanvragen in een JSON-indeling. Het bevat een aantal verplichte elementen die onnodige complexiteit hebben toegevoegd voor het scenario van het doorgeven van het HTTP-bericht via de kabel.

Een alternatieve optie was het application/http mediatype te gebruiken zoals beschreven in de HTTP-specificatie RFC 7230. Dit mediatype gebruikt dezelfde indeling die wordt gebruikt voor het verzenden van HTTP-berichten via de kabel, maar het hele bericht kan in de hoofdtekst van een andere HTTP-aanvraag worden geplaatst. In ons geval gebruiken we gewoon de hoofdtekst als ons bericht om naar Event Hubs te verzenden. Handig is er een parser die bestaat in Microsoft ASP.NET Web API 2.2-clientbibliotheken die deze indeling kunnen parseren en deze kunnen converteren naar de systeemeigen HttpRequestMessage en HttpResponseMessage objecten.

Om dit bericht te kunnen maken, moeten we profiteren van op C# gebaseerde beleidsexpressies in Azure API Management. Dit is het beleid, waarmee een HTTP-aanvraagbericht naar Azure Event Hubs wordt verzonden.

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

Beleidsdeclaratie

Er zijn enkele dingen die het vermelden waard zijn over deze beleidsexpressie. Het log-to-eventhub-beleid heeft een kenmerk met de naam logger-id, dat verwijst naar de naam van de logboekregistratie die is gemaakt in de API Management-service. De details over het instellen van een Event Hub-logboekregistratie in de API Management-service vindt u in het document Gebeurtenissen registreren bij Azure Event Hubs in Azure API Management. Het tweede kenmerk is een optionele parameter waarmee Event Hubs worden geïnstrueerd waarin het bericht moet worden opgeslagen. Event Hubs maakt gebruik van partities om schaalbaarheid mogelijk te maken en minimaal twee te vereisen. De geordende bezorging van berichten wordt alleen gegarandeerd binnen een partitie. Als we Event Hub niet instrueren in welke partitie het bericht moet worden geplaatst, wordt er een round robin-algoritme gebruikt om de belasting te verdelen. Dit kan er echter toe leiden dat sommige van onze berichten niet op volgorde worden verwerkt.

Partities

Om ervoor te zorgen dat onze berichten aan consumenten worden bezorgd en profiteren van de belastingsdistributiemogelijkheden van partities, heb ik ervoor gekozen om HTTP-aanvraagberichten te verzenden naar één partitie en HTTP-antwoordberichten naar een tweede partitie. Dit zorgt voor een gelijkmatige belastingverdeling en we kunnen garanderen dat alle aanvragen op volgorde worden verbruikt en dat alle antwoorden in volgorde worden verbruikt. Het is mogelijk dat een reactie vóór de bijbehorende aanvraag wordt gebruikt, maar omdat dat geen probleem is omdat we een ander mechanisme hebben voor het correleren van aanvragen aan antwoorden en we weten dat aanvragen altijd vóór antwoorden komen.

HTTP-nettoladingen

Na het bouwen van de requestLine, controleren we of de aanvraagbody moet worden afgekapt. De aanvraagbody wordt afgekapt tot slechts 1024. Dit kan worden verhoogd, maar afzonderlijke Event Hub-berichten zijn beperkt tot 256 kB, dus het is waarschijnlijk dat sommige HTTP-berichtteksten niet in één bericht passen. Bij het uitvoeren van logboekregistratie en analyse kan een aanzienlijke hoeveelheid informatie worden afgeleid van alleen de HTTP-aanvraagregel en headers. Bovendien retourneren veel API's alleen kleine lichamen en is het verlies van informatiewaarde door het afkappen van grote lichamen vrij minimaal in vergelijking met de verlaging van de overdrachts-, verwerkings- en opslagkosten om alle inhoud van de hoofdtekst te behouden. Een laatste opmerking over het verwerken van de hoofdtekst is dat we de As<string>() methode moeten doorgeven true omdat we de inhoud van de hoofdtekst lezen, maar ook de back-end-API de hoofdtekst moeten kunnen lezen. Door true door te geven aan deze methode, zorgen we ervoor dat de hoofdtekst wordt gebufferd, zodat deze een tweede keer kan worden gelezen. Dit is belangrijk om rekening mee te houden als u een API hebt die grote bestanden uploadt of lange polling gebruikt. In dergelijke gevallen is het raadzaam om te voorkomen dat het lichaam helemaal wordt gelezen.

HTTP-kopteksten

HTTP-headers kunnen worden overgebracht naar de berichtindeling in een eenvoudige indeling voor sleutel-/waardepaar. We hebben ervoor gekozen om bepaalde beveiligingsgevoelige velden te verwijderen, om onnodige lekken van referentiegegevens te voorkomen. Het is onwaarschijnlijk dat API-sleutels en andere referenties worden gebruikt voor analysedoeleinden. Als we analyses willen uitvoeren op de gebruiker en het specifieke product dat ze gebruiken, kunnen we dat ophalen uit het context object en dat toevoegen aan het bericht.

Berichtmetagegevens

Bij het bouwen van het volledige bericht dat naar de Event Hub moet worden verzonden, maakt de eerste regel geen deel uit van het application/http bericht. De eerste regel bestaat uit aanvullende metagegevens die bestaan uit het feit of het bericht een aanvraag- of antwoordbericht is en een bericht-id, die wordt gebruikt om aanvragen aan reacties te correleren. De bericht-id wordt gemaakt met behulp van een ander beleid dat er als volgt uitziet:

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

We kunnen het aanvraagbericht hebben gemaakt, dat in een variabele hebben opgeslagen totdat het antwoord is geretourneerd en vervolgens de aanvraag en het antwoord als één bericht hebben verzonden. Door de aanvraag en het antwoord echter onafhankelijk te verzenden en een bericht-id te gebruiken om de twee te correleren, krijgen we wat meer flexibiliteit in de berichtgrootte, de mogelijkheid om te profiteren van meerdere partities terwijl de berichtvolgorde wordt gehandhaafd en de aanvraag sneller wordt weergegeven in ons dashboard voor logboekregistratie. Er kunnen ook scenario's zijn waarin een geldig antwoord nooit naar de Event Hub wordt verzonden, mogelijk vanwege een fatale aanvraagfout in de API Management-service, maar we hebben nog steeds een record van de aanvraag.

Het beleid voor het verzenden van het HTTP-antwoordbericht lijkt op de aanvraag en de volledige beleidsconfiguratie ziet er als volgt uit:

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

Het set-variable beleid maakt een waarde die toegankelijk is voor zowel het log-to-eventhub beleid in de <inbound> sectie als de <outbound> sectie.

Gebeurtenissen ontvangen van Event Hubs

Gebeurtenissen van Azure Event Hub worden ontvangen met behulp van het AMQP-protocol. Het Microsoft Service Bus-team heeft clientbibliotheken beschikbaar gemaakt om de verbruikende gebeurtenissen eenvoudiger te maken. Er worden twee verschillende benaderingen ondersteund, één is een Direct Consumer en de andere gebruikt de EventProcessorHost klasse. Voorbeelden van deze twee benaderingen vindt u in de Programmeerhandleiding voor Event Hubs. De korte versie van de verschillen is, Direct Consumer geeft u volledige controle en het EventProcessorHost doet enkele van de sanitairwerkzaamheden voor u, maar maakt bepaalde veronderstellingen over hoe u deze gebeurtenissen verwerkt.

EventProcessorHost

In dit voorbeeld gebruiken we het voor het EventProcessorHost gemak, maar het is mogelijk niet de beste keuze voor dit specifieke scenario. EventProcessorHost doet het harde werk om ervoor te zorgen dat u zich geen zorgen hoeft te maken over threadingproblemen binnen een bepaalde gebeurtenisprocessorklasse. In ons scenario converteren we het bericht echter gewoon naar een andere indeling en geven we het door aan een andere service met behulp van een asynchrone methode. Er is geen noodzaak om de gedeelde status bij te werken en daarom is er geen risico op threadingproblemen. Voor de meeste scenario's EventProcessorHost is dit waarschijnlijk de beste keuze en is het zeker de eenvoudigere optie.

IEventProcessor

Het centrale concept bij gebruik EventProcessorHost is het maken van een implementatie van de IEventProcessor interface, die de methode ProcessEventAsyncbevat. De essentie van deze methode wordt hier weergegeven:

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

Een lijst met EventData-objecten wordt doorgegeven aan de methode en we herhalen deze lijst. De bytes van elke methode worden geparseerd in een HttpMessage-object en dat object wordt doorgegeven aan een exemplaar van IHttpMessageProcessor.

HttpMessage

Het HttpMessage exemplaar bevat drie stukjes gegevens:

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

}

Het HttpMessage exemplaar bevat een MessageId GUID waarmee we de HTTP-aanvraag kunnen verbinden met het bijbehorende HTTP-antwoord en een Booleaanse waarde die aangeeft of het object een exemplaar van een HttpRequestMessage en HttpResponseMessage bevat. Door gebruik te maken van de ingebouwde HTTP-klassen van System.Net.Http, kon ik profiteren van de application/http parseringscode die is opgenomen in System.Net.Http.Formatting.

IHttpMessageProcessor

Het HttpMessage exemplaar wordt vervolgens doorgestuurd naar de implementatie van IHttpMessageProcessor, een interface die ik heb gemaakt om het ontvangen en interpreteren van de gebeurtenis van Azure Event Hub los te koppelen en de daadwerkelijke verwerking ervan.

Het HTTP-bericht doorsturen

Voor dit voorbeeld heb ik besloten dat het interessant zou zijn om de HTTP-aanvraag naar Moesif API Analytics te pushen. Moesif is een cloudservice die gespecialiseerd is in HTTP-analyse en foutopsporing. Ze hebben een gratis laag, dus het is eenvoudig om te proberen en stelt ons in staat om de HTTP-aanvragen in realtime te zien die via onze API Management-service stromen.

De IHttpMessageProcessor implementatie ziet er als volgt uit:

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

Het MoesifHttpMessageProcessor maakt gebruik van een C#-API-bibliotheek voor Moesif waarmee u eenvoudig HTTP-gebeurtenisgegevens naar hun service kunt pushen. Als u HTTP-gegevens wilt verzenden naar de Moesif Collector-API, hebt u een account en een toepassings-id nodig. U krijgt een Moesif-toepassings-id door een account te maken op de website van Moesif en vervolgens naar het menu rechtsboven ->App Setup te gaan.

Volledig voorbeeld

De broncode en tests voor het voorbeeld bevinden zich op GitHub. U hebt een API Management-service, een verbonden Event Hub en een opslagaccount nodig om het voorbeeld voor uzelf uit te voeren.

Het voorbeeld is slechts een eenvoudige consoletoepassing die luistert naar gebeurtenissen die afkomstig zijn van Event Hub, converteert ze naar een Moesif EventRequestModel en EventResponseModel objecten en stuurt ze vervolgens door naar de Moesif Collector-API.

In de volgende geanimeerde afbeelding ziet u een aanvraag die wordt ingediend bij een API in de ontwikkelaarsportal, de consoletoepassing met het bericht dat wordt ontvangen, verwerkt en doorgestuurd en vervolgens de aanvraag en het antwoord weergegeven in de Event Stream.

Demonstratie van aanvraag die wordt doorgestuurd naar Runscope

Samenvatting

De Azure API Management-service biedt een ideale plek om het HTTP-verkeer van en naar uw API's vast te leggen. Azure Event Hubs is een zeer schaalbare, goedkope oplossing voor het vastleggen van dat verkeer en het invoeren ervan in secundaire verwerkingssystemen voor logboekregistratie, bewaking en andere geavanceerde analyses. Verbinding maken het bewaken van verkeer van derden, zoals Moesif, is net zo eenvoudig als een paar tientallen coderegels.

Volgende stappen