Azure API Management, Event Hubs ve Moesif ile API'lerinizi izleme

UYGULANANLAR: Tüm API Management katmanları

API Management hizmeti, HTTP API'nize gönderilen HTTP isteklerinin işlenmesini geliştirmek için birçok özellik sağlar. Ancak, isteklerin ve yanıtların varlığı geçicidir. İstek yapılır ve API Management hizmeti aracılığıyla arka uç API'nize akar. API'niz isteği işler ve bir yanıt API tüketicisine geri akar. API Management hizmeti, API'lerle ilgili bazı önemli istatistikleri Azure portalı panosunda görüntülenmek üzere tutar, ancak bunun ötesinde ayrıntılar kaybolur.

API Management hizmetinde log-to-eventhub ilkesini kullanarak, istekten ve yanıttan Azure Olay Hub'ına tüm ayrıntıları gönderebilirsiniz. API'lerinize gönderilen HTTP iletilerinden olaylar oluşturmak istemenizin çeşitli nedenleri vardır. Bazı örnekler güncelleştirmelerin denetim kaydını, kullanım analizini, özel durum uyarılarını ve üçüncü taraf tümleştirmelerini içerir.

Bu makalede, HTTP isteğinin ve yanıt iletisinin tamamını yakalama, bir Olay Hub'ına gönderme ve ardından bu iletiyi HTTP günlüğe kaydetme ve izleme hizmetleri sağlayan bir üçüncü taraf hizmetine geçirme işlemi gösterilmektedir.

Neden API Management Hizmetinden Gönder?

HTTP isteklerini ve yanıtlarını yakalamak ve bunları günlüğe kaydetme ve izleme sistemlerine beslemek için HTTP API çerçevelerine takabilen HTTP ara yazılımı yazmak mümkündür. Bu yaklaşımın dezavantajı, HTTP ara yazılımının arka uç API'sine tümleştirilmesi ve API'nin platformuyla eşleşmesi gerektiğidir. Birden çok API varsa, her birinin ara yazılımı dağıtması gerekir. Genellikle arka uç API'lerinin güncelleştirilememesinin nedenleri vardır.

Günlüğe kaydetme altyapısıyla tümleştirmek için Azure API Management hizmetini kullanmak merkezi ve platformdan bağımsız bir çözüm sağlar. Kısmen Azure API Management'ın coğrafi çoğaltma özelliklerinden dolayı da ölçeklenebilir.

Neden bir Azure Olay Hub'ına gönderesiniz?

Neden Azure Event Hubs'a özgü bir ilke oluşturmak mantıklıdır? İsteklerimi günlüğe kaydetmek isteyebileceğim birçok farklı yer var. Neden istekleri doğrudan son hedefe göndermiyor? Bu bir seçenektir. Ancak, bir API management hizmetinden günlük istekleri yaparken, günlüğe kaydetme iletilerinin API'nin performansını nasıl etkilediğini göz önünde bulundurmak gerekir. Yükteki kademeli artışlar, sistem bileşenlerinin kullanılabilir örneklerini artırarak veya coğrafi çoğaltmadan yararlanarak işlenebilir. Ancak, altyapıyı günlüğe kaydetme istekleri yük altında yavaşlamaya başlarsa trafikteki kısa ani artışlar isteklerin gecikmesine neden olabilir.

Azure Event Hubs, çoğu API işleminin HTTP isteği sayısından çok daha fazla sayıda olayla ilgilenme kapasitesiyle çok büyük hacimli verileri giriş için tasarlanmıştır. Olay Hub'ı, API yönetim hizmetinizle iletileri depolayan ve işleyen altyapı arasında bir tür karmaşık arabellek işlevi görür. Bu, günlüğe kaydetme altyapısı nedeniyle API performansınızın düşmemesini sağlar.

Veriler bir Olay Hub'ına geçirildikten sonra kalıcı hale geçer ve Event Hub tüketicilerinin işlemesini bekler. Olay Hub'ı nasıl işlendiğiyle ilgilenmez, yalnızca iletinin başarıyla teslim edilmesine dikkat eder.

Event Hubs, olayları birden çok tüketici grubuna akışla aktarabilme özelliğine sahiptir. Bu, olayların farklı sistemler tarafından işlenmesini sağlar. Bu, API Management hizmeti içindeki API isteğinin işlenmesinde yalnızca bir olayın oluşturulması gerektiğinden ekleme gecikmeleri uygulamadan birçok tümleştirme senaryosunu desteklemeye olanak tanır.

Uygulama/http iletileri gönderme ilkesi

Olay Hub'ı olay verilerini basit bir dize olarak kabul eder. Bu dizenin içeriği size bağlı. HTTP isteğini paketleyebilmek ve Event Hubs'a gönderebilmek için dizeyi istek veya yanıt bilgileriyle biçimlendirmemiz gerekir. Böyle durumlarda, yeniden kullanabileceğimiz mevcut bir biçim varsa kendi ayrıştırma kodumuzu yazmamız gerekmeyebilir. Başlangıçta HTTP istekleri ve yanıtları göndermek için HAR kullanmayı göz önüne aldım. Ancak bu biçim, bir dizi HTTP isteğini JSON tabanlı bir biçimde depolamak için en iyi duruma getirilmiştir. HTTP iletisini kablo üzerinden geçirme senaryosu için gereksiz karmaşıklık katan bir dizi zorunlu öğe içeriyordu.

Alternatif bir seçenek, HTTP belirtimi RFC 7230'da açıklandığı gibi medya türünü kullanmaktıapplication/http. Bu medya türü, kablo üzerinden GERÇEKTEN HTTP iletileri göndermek için kullanılan biçimin aynısını kullanır, ancak iletinin tamamı başka bir HTTP isteğinin gövdesine yerleştirilebilir. Bizim örneğimizde, event hubs'a göndermek için ileti olarak gövdesini kullanacağız. Uygun bir şekilde, Microsoft ASP.NET Web API 2.2 İstemci kitaplıklarında bu biçimi ayrıştırıp yerel HttpRequestMessage ve HttpResponseMessage nesnelere dönüştürebilen bir ayrıştırıcı vardır.

Bu iletiyi oluşturabilmek için Azure API Management'ta C# tabanlı İlke ifadelerinden yararlanmamız gerekir. Azure Event Hubs'a http isteği iletisi gönderen ilke aşağıdadır.

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

İlke bildirimi

Bu ilke ifadesi hakkında bahsetmeye değer birkaç şey vardır. Log-to-eventhub ilkesi, API Management hizmetinde oluşturulan günlükçü adını ifade eden logger-id adlı bir özniteliğine sahiptir. API Management hizmetinde Olay Hub'ı günlükçüsünün nasıl ayarlanacağına ilişkin ayrıntılar, Azure API Management'ta Azure Event Hubs'a olayları günlüğe kaydetme belgesinde bulunabilir. İkinci öznitelik, Event Hubs'a iletinin depolandığı bölümü belirten isteğe bağlı bir parametredir. Event Hubs, ölçeklenebilirliği sağlamak için bölümleri kullanır ve en az iki bölüm gerektirir. İletilerin sıralı teslimi yalnızca bir bölüm içinde garanti edilir. Olay Hub'ına iletinin yerleştirildiği bölüme yönerge vermezsek, yükü dağıtmak için hepsini bir kez deneme algoritması kullanır. Ancak bu, bazı iletilerimizin sıra dışı işlenmesine neden olabilir.

Bölümler

İletilerimizin tüketicilere sırayla teslim edilmesini sağlamak ve bölümlerin yük dağıtım özelliğinden yararlanmak için, bir bölüme HTTP istek iletileri, ikinci bir bölüme de HTTP yanıt iletileri göndermeyi seçtim. Bu, eşit bir yük dağıtımı sağlar ve tüm isteklerin sırayla tüketileceğini ve tüm yanıtların sırayla tüketileceğini garanti edebiliriz. Bir yanıtın ilgili istek öncesinde tüketilmesi mümkündür, ancak bu bir sorun olmadığından, istekleri yanıtlarla ilişkilendirmek için farklı bir mekanizmamız vardır ve isteklerin her zaman yanıtlardan önce geldiğini biliyoruz.

HTTP yükleri

'yi requestLinederledikten sonra istek gövdesinin kesilmesi gerekip gerekmediğini denetleriz. İstek gövdesi yalnızca 1024 olarak kesilir. Bu artırılabilir, ancak tek tek Olay Hub'ı iletileri 256 KB ile sınırlıdır, bu nedenle bazı HTTP ileti gövdeleri tek bir iletiye sığmayabilir. Günlüğe kaydetme ve analiz yaparken yalnızca HTTP istek satırından ve üst bilgilerinden önemli miktarda bilgi türetilebilir. Ayrıca, birçok API isteği yalnızca küçük gövdeler döndürür ve bu nedenle büyük gövdeleri keserek bilgi değerinin kaybı, tüm gövde içeriğini korumak için aktarım, işleme ve depolama maliyetlerinin azaltılmasıyla karşılaştırıldığında oldukça düşüktür. Gövdenin işlenmesiyle ilgili son bir not, gövde içeriğini okuduğumuz için yöntemine As<string>() geçmemiz true gerektiğidir, ancak arka uç API'sinin de gövdeyi okuyabilmesi istendi. Bu yönteme true geçirerek, vücudun ikinci kez okunabilmesi için arabelleğe alınmasına neden oluruz. Büyük dosyaları karşıya yükleyen veya uzun yoklama kullanan bir API'niz olup olmadığını bilmeniz önemlidir. Bu gibi durumlarda, cesedi okumaktan kaçınmak en iyisidir.

HTTP üst bilgileri

HTTP Üst Bilgileri ileti biçimine basit bir anahtar/değer çifti biçiminde aktarılabilir. Kimlik bilgilerinin gereksiz yere sızmasını önlemek için bazı güvenlik duyarlı alanlarının çıkarılmasını seçtik. API anahtarlarının ve diğer kimlik bilgilerinin analiz amacıyla kullanılması pek olası değildir. Kullanıcı ve kullandığı ürün üzerinde analiz yapmak istiyorsak bunu nesneden context alıp iletiye ekleyebiliriz.

İleti Meta Verileri

Olay hub'ına gönderilecek tam iletiyi oluştururken, ilk satır aslında iletinin application/http bir parçası değildir. İlk satır, iletinin istek mi yoksa yanıt iletisi mi olduğunu ve istekleri yanıtlarla ilişkilendirmek için kullanılan ileti kimliğini içeren ek meta verilerdir. İleti kimliği, şuna benzer başka bir ilke kullanılarak oluşturulur:

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

İstek iletisini oluşturup yanıt döndürülene kadar bunu bir değişkende depolayabilir ve ardından istek ve yanıtı tek bir ileti olarak gönderebilirdik. Ancak, isteği ve yanıtı bağımsız olarak göndererek ve ikisini ilişkilendirmek için bir ileti kimliği kullanarak, ileti boyutu konusunda biraz daha fazla esneklik elde ederiz, ileti sırasını korurken birden çok bölümden yararlanma olanağı elde ederiz ve istek günlük panomuzda daha erken görünür. Ayrıca, büyük olasılıkla API Management hizmetindeki önemli bir istek hatası nedeniyle olay hub'ına hiçbir zaman geçerli bir yanıt gönderilmediği ancak isteğin kaydına sahip olduğumuz bazı senaryolar da olabilir.

Yanıt HTTP iletisini gönderme ilkesi isteğe benzer ve bu nedenle ilke yapılandırmasının tamamı şöyle görünü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>

İlke, set-variable hem bölümdeki hem de bölümdeki log-to-eventhub<outbound> ilke <inbound> tarafından erişilebilen bir değer oluşturur.

Event Hubs'dan olay alma

Azure Olay Hub'ından olaylar AMQP protokolü kullanılarak alınır. Microsoft Service Bus ekibi, tüketen olayları kolaylaştırmak için istemci kitaplıklarını kullanıma sundu. Desteklenen iki farklı yaklaşım vardır: biri Doğrudan Tüketici, diğeri ise sınıfını EventProcessorHost kullanmaktır. Bu iki yaklaşımın örnekleri Event Hubs Programlama Kılavuzu'nda bulunabilir. Farklılıkların kısa sürümü tam Direct Consumer denetim sağlar ve EventProcessorHost bazı tesisat işleri sizin için yapar, ancak bu olayları nasıl işlediğiniz hakkında bazı varsayımlarda bulunur.

EventProcessorHost

Bu örnekte, basitlik için kullanırız EventProcessorHost , ancak bu senaryo için en iyi seçenek olmayabilir. EventProcessorHost , belirli bir olay işlemcisi sınıfı içindeki iş parçacığı oluşturma sorunları hakkında endişelenmenize gerek olmadığından emin olmak için sıkı bir çalışma yapar. Ancak senaryomuzda, iletiyi başka bir biçime dönüştürüyor ve zaman uyumsuz bir yöntem kullanarak başka bir hizmete geçiriyoruz. Paylaşılan durumu güncelleştirmeye gerek yoktur ve bu nedenle iş parçacığı oluşturma sorunları riski yoktur. Çoğu senaryo için, EventProcessorHost muhtemelen en iyi seçenektir ve kesinlikle daha kolay bir seçenektir.

IEventProcessor

kullanılırken EventProcessorHost temel kavram, yöntemini ProcessEventAsynciçeren arabiriminin IEventProcessor bir uygulamasını oluşturmaktır. Bu yöntemin özü burada gösterilmiştir:

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

EventData nesnelerinin listesi yöntemine geçirilir ve bu listeyi yineleriz. Her yöntemin baytları bir HttpMessage nesnesine ayrıştırılır ve bu nesne bir IHttpMessageProcessor örneğine geçirilir.

HttpMessage

Örnek HttpMessage üç veri parçası içerir:

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

}

Örnek, HttpMessage HTTP isteğini ilgili HTTP yanıtına bağlamamıza olanak tanıyan bir MessageId GUID ve nesnenin bir HttpRequestMessage ve HttpResponseMessage örneği içerip içermediğini tanımlayan bir boole değeri içerir. içindeki yerleşik HTTP sınıflarını System.Net.Httpkullanarak içinde bulunan System.Net.Http.Formattingayrıştırma kodundan application/http yararlandım.

IHttpMessageProcessor

Daha HttpMessage sonra örnek, Azure Event Hub'dan olayın alma ve yorumlamasını ve gerçek işlemesini birbirinden ayırmaya yönelik oluşturduğum bir arabirim olan uygulamasına IHttpMessageProcessoriletilir.

HTTP iletisini iletme

Bu örnek için HTTP İsteğini Moesif API Analytics'e göndermenin ilginç olacağına karar verdim. Moesif, HTTP analizi ve hata ayıklama konusunda uzmanlaşmış bulut tabanlı bir hizmettir. Ücretsiz bir katmanı olduğundan kolayca deneyebilir ve API Management hizmetimiz aracılığıyla gerçek zamanlı akışla HTTP isteklerini görmemizi sağlar.

Uygulama IHttpMessageProcessor şuna benzer:

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

, MoesifHttpMessageProcessor Moesif için http olay verilerini kendi hizmetine göndermeyi kolaylaştıran bir C# API kitaplığından yararlanır. Moesif Toplayıcı API'sine HTTP verileri göndermek için bir hesaba ve Uygulama Kimliğine ihtiyacınız vardır. Moesif'in web sitesinde bir hesap oluşturup Sağ Üst Menü ->Uygulama Kurulumu'na giderek Moesif Uygulama Kimliği alırsınız.

Tam örnek

Örneğin kaynak kodu ve testleri GitHub'dadır. Örneği kendiniz çalıştırmak için bir API Management Hizmeti, bağlı bir Olay Hub'ı ve bir Depolama Hesabı gerekir.

Örnek, Event Hub'dan gelen olayları dinleyen, bunları Moesif'e EventRequestModel ve nesnelere dönüştüren ve EventResponseModel ardından Moesif Toplayıcı API'sine ileten basit bir Konsol uygulamasıdır.

Aşağıdaki animasyonlu görüntüde, Geliştirici Portalı'nda API'ye yapılan bir isteği, alınan, işlenen ve iletilen iletiyi gösteren Konsol uygulamasını ve ardından istek ve yanıtın Olay Akışı'nda gösterildiğini görebilirsiniz.

Runscope'a iletilen isteğin gösterimi

Özet

Azure API Management hizmeti, API'lerinize ve API'lerinizden gelen HTTP trafiğini yakalamak için ideal bir yer sağlar. Azure Event Hubs, bu trafiği yakalamaya ve günlüğe kaydetme, izleme ve diğer gelişmiş analizler için ikincil işleme sistemlerine beslemeye yönelik yüksek oranda ölçeklenebilir, düşük maliyetli bir çözümdür. Moesif gibi üçüncü taraf trafik izleme sistemlerine Bağlan birkaç düzine kod satırı kadar basittir.

Sonraki adımlar