Pantau API Anda dengan Azure API Management, Azure Event Hub, dan Moesif

Layanan API Management menyediakan banyak kemampuan untuk meningkatkan pemrosesan permintaan HTTP yang dikirim ke API HTTP Anda. Namun, keberadaan permintaan dan tanggapan bersifat sementara. Permintaan dibuat dan mengalir melalui layanan API Management ke API backend Anda. API Anda memproses permintaan dan respons mengalir kembali ke konsumen API. Layanan API Management menyimpan beberapa statistik penting tentang API untuk ditampilkan di dasbor portal Microsoft Azure, tetapi di luar itu, detailnya hilang.

Dengan menggunakan kebijakan log-to-eventhub di layanan API Management, Anda dapat mengirim detail apa pun dari permintaan dan respons ke Azure Event Hub. Ada berbagai alasan mengapa Anda mungkin ingin membuat aktivitas dari pesan HTTP yang dikirim ke API Anda. Beberapa contoh termasuk jejak audit pembaruan, analitik penggunaan, peringatan pengecualian, dan integrasi pihak ketiga.

Artikel ini menunjukkan cara menangkap seluruh permintaan dan pesan respons HTTP, mengirimkannya ke Hub Kejadian lalu menyampaikan pesan tersebut ke layanan pihak ketiga yang menyediakan layanan pencatatan dan pemantauan HTTP.

Mengapa Mengirim Dari Layanan API Management?

Dimungkinkan untuk menulis middleware HTTP yang dapat dicolokkan ke kerangka kerja API HTTP untuk menangkap permintaan dan respons HTTP dan mengumpankan ke sistem pencatatan dan pemantauan. Kelemahan dari pendekatan ini adalah middleware HTTP perlu diintegrasikan ke dalam API backend dan harus cocok dengan platform API. Jika ada beberapa API, maka masing-masing API harus menyebarkan middleware. Seringkali ada alasan mengapa API backend tidak dapat diperbarui.

Menggunakan layanan Azure API Management untuk mengintegrasikan dengan infrastruktur pencatatan memberikan solusi terpusat dan independen platform. Layanan ini juga dapat diskalakan, sebagian karena kemampuan geo-replikasi Azure API Management.

Mengapa mengirim ke Azure Event Hub?

Masuk akal untuk bertanya, mengapa membuat sebuah kebijakan yang khusus untuk Azure Event Hubs? Ada banyak tempat berbeda di mana saya mungkin ingin mencatat permintaan saya. Mengapa tidak mengirim saja permintaan secara langsung ke tujuan akhir? Itu adalah pilihan. Namun, saat membuat permintaan pencatatan dari layanan API Management, perlu dipertimbangkan bagaimana pesan pencatatan memengaruhi kinerja API. Peningkatan beban secara bertahap dapat ditangani dengan meningkatkan instans komponen sistem yang tersedia atau dengan memanfaatkan replikasi geografis. Namun, lonjakan lalu lintas yang pendek dapat menyebabkan permintaan tertunda jika permintaan untuk mencatat infrastruktur mulai melambat di bawah beban.

Azure Event Hubs dirancang untuk mengurangi data dalam jumlah besar, dengan kapasitas untuk menangani jumlah aktivitas yang jauh lebih tinggi daripada jumlah permintaan HTTP yang sebagian besar proses API. Event Hub bertindak sebagai semacam buffer canggih antara layanan API Management Anda dan infrastruktur yang menyimpan dan memproses pesan. Hal ini memastikan bahwa kinerja API Anda tidak akan menderita karena infrastruktur pencatatan.

Setelah data diteruskan ke Hub Kejadian, data akan tetap ada dan akan menunggu konsumen Hub Kejadian untuk memprosesnya. Hub Kejadian tidak peduli bagaimana prosesnya, hanya peduli untuk memastikan pesan akan berhasil dikirimkan.

Azure Event Hubs memiliki kemampuan untuk melakukan streaming aktivitas ke beberapa grup konsumen. Hal ini memungkinkan aktivitas diproses oleh sistem yang berbeda. Ini memungkinkan mendukung banyak skenario integrasi tanpa menempatkan penundaan penambahan pada pemrosesan permintaan API dalam layanan API Management karena hanya satu aktivitas yang perlu dibuat.

Kebijakan untuk mengirim pesan aplikasi/http

Hub Kejadian menerima data aktivitas sebagai string sederhana. Isi dari string itu terserah Anda. Agar dapat mengemas permintaan HTTP dan mengirimkannya ke Pusat Aktivitas, kita perlu memformat string dengan informasi permintaan atau respons. Dalam situasi seperti ini, jika ada format yang ada yang dapat kita gunakan kembali, maka kita mungkin tidak perlu menulis kode penguraian kita sendiri. Awalnya saya mempertimbangkan menggunakan HAR untuk mengirim permintaan dan respons HTTP. Namun, format ini dioptimalkan untuk menyimpan urutan permintaan HTTP dalam format berbasis JSON. Ini berisi sejumlah elemen wajib yang menambahkan kompleksitas yang tidak perlu untuk skenario meneruskan pesan HTTP melalui kabel.

Pilihan alternatifnya adalah menggunakan jenis application/http media seperti yang dijelaskan dalam spesifikasi HTTP RFC 7230. Jenis media ini menggunakan format yang sama persis yang digunakan untuk benar-benar mengirim pesan HTTP melalui kabel, tetapi seluruh pesan dapat dimasukkan ke dalam isi permintaan HTTP lain. Dalam kasus kami, kami hanya akan menggunakan isi sebagai pesan kami untuk dikirim ke Pusat Aktivitas. Dengan nyaman, ada parser yang ada di pustaka Microsoft ASP.NET Web API 2.2 Client yang dapat mengurai format ini dan mengubahnya menjadi native HttpRequestMessage dan objek HttpResponseMessage.

Untuk dapat membuat pesan ini, kita perlu memanfaatkan C# berbasis Ekspresi kebijakan di Azure API Management. Berikut adalah kebijakan, yang mengirimkan pesan permintaan HTTP ke 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>

Deklarasi kebijakan

Ada beberapa hal tertentu yang layak disebutkan mengenai ekspresi kebijakan ini. Kebijakan log-to-eventhub memiliki atribut yang disebut logger-id, yang mengacu pada nama pencatat yang telah dibuat dalam layanan API Management. Detail cara menyiapkan pencatat Hub Kejadian di layanan API Management dapat ditemukan di dokumen Cara mencatat aktivitas ke Azure Event Hubs di Azure API Management. Atribut kedua adalah parameter opsional yang menginstruksikan Pusat Aktivitas yang partisinya untuk menyimpan pesan. Pusat Aktivitas menggunakan partisi untuk mengaktifkan skalabilitas dan memerlukan minimal dua partisi. Pengiriman pesan yang dipesan hanya dijamin dalam sebuah partisi. Jika kita tidak menginstruksikan Hub Kejadian di mana partisi untuk menempatkan pesan, ia menggunakan algoritma round-robin untuk mendistribusikan beban. Namun, hal itu dapat menyebabkan beberapa pesan kita diproses rusak.

Partisi

Untuk memastikan pesan kami dikirimkan kepada konsumen sesuai urutan dan memanfaatkan kemampuan distribusi beban partisi, saya memilih untuk mengirim pesan permintaan HTTP ke satu partisi dan pesan respons HTTP ke partisi kedua. Ini memastikan distribusi beban yang merata dan kami dapat menjamin bahwa semua permintaan akan dikonsumsi secara berrutan dan semua respons dikonsumsi secara berurutan. Dimungkinkan untuk respons yang akan dikonsumsi sebelum permintaan yang sesuai, tetapi karena itu bukan masalah karena kami memiliki mekanisme yang berbeda untuk mengkorelasikan permintaan ke tanggapan dan kami tahu bahwa permintaan selalu datang sebelum tanggapan.

Payload HTTP

Setelah membangun requestLine, kami memeriksa untuk melihat apakah isi permintaan harus dipotong. Isi permintaan dipotong menjadi hanya 1024. Ini dapat ditingkatkan, namun pesan Hub Kejadian individual dibatasi hingga 256 KB, sehingga kemungkinan beberapa isi pesan HTTP tidak akan cukup dalam satu pesan. Saat melakukan pencatatan dan analitik, sejumlah besar informasi dapat diperoleh hanya dari baris permintaan HTTP dan judul. Juga, banyak permintaan API hanya mengembalikan isi kecil dan sehingga hilangnya nilai informasi dengan memotong isi besar cukup minimal dibandingkan dengan pengurangan biaya transfer, pemrosesan, dan penyimpanan untuk menjaga semua isi konten. Salah satu catatan akhir tentang memproses isi adalah bahwa kita perlu meneruskan true ke metode As<string>() karena kita membaca isi konten, tetapi juga ingin API backend untuk dapat membaca isinya. Dengan meneruskan true ke metode ini, kami menyebabkan isi menjadi buffer sehingga bisa dibaca untuk kedua kalinya. Hal ini penting untuk diperhatikan jika Anda memiliki API yang melakukan pengunggahan file besar atau menggunakan poling yang panjang. Dalam kasus ini, akan lebih baik untuk menghindari membaca isi sama sekali.

Header HTTP

Header HTTP dapat ditransfer ke dalam format pesan dalam format pasangan kunci/nilai sederhana. Kami telah memilih untuk menanggalkan bidang sensitif keamanan tertentu, untuk menghindari bocornya informasi kredensial yang tidak perlu. Tidak dimungkinkan kunci API dan kredensial lainnya akan digunakan untuk tujuan analitik. Jika kita ingin melakukan analisis pada pengguna dan produk tertentu yang mereka gunakan, maka kita bisa mendapatkannya dari objek context dan menambahkannya ke pesan.

Metadata Pesan

Saat membangun pesan lengkap untuk dikirim ke hub kejadian, baris pertama sebenarnya bukan bagian dari application/http pesan. Baris pertama adalah metadata tambahan yang terdiri dari apakah pesan tersebut merupakan pesan permintaan atau tanggapan dan ID pesan, yang digunakan untuk menghubungkan permintaan dengan tanggapan. ID pesan dibuat dengan menggunakan kebijakan lain yang terlihat seperti ini:

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

Kita dapat membuat pesan permintaan, menyimpannya dalam variabel hingga respons dikembalikan dan kemudian mengirim permintaan dan respons sebagai satu pesan tunggal. Namun, dengan mengirimkan permintaan dan respons secara independen dan menggunakan id pesan untuk menghubungkan keduanya, kami mendapatkan sedikit lebih banyak fleksibilitas dalam ukuran pesan, kemampuan untuk memanfaatkan beberapa partisi sembari mempertahankan urutan pesan dan permintaan akan muncul di dasbor pencatatan kami lebih cepat. Mungkin juga ada beberapa skenario di mana respons yang valid tidak pernah dikirim ke hub kejadian, mungkin karena kesalahan permintaan yang fatal di layanan API Management, tetapi kami masih memiliki catatan permintaan.

Kebijakan untuk mengirim pesan HTTP respons terlihat mirip dengan permintaan dan sehingga konfigurasi kebijakan lengkap akan terlihat seperti ini:

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

Kebijakan set-variable ini menciptakan nilai yang dapat diakses oleh kebijakan log-to-eventhub di bagian <inbound> dan bagian <outbound>.

Menerima kejadian dari Azure Event Hubs

Acara dari Azure Event Hub diterima menggunakan protokol AMQP. Tim Microsoft Service Bus telah menyediakan pustaka klien untuk mempermudah konsumsi kejadian. Ada dua pendekatan berbeda yang didukung, satu adalah menjadi Konsumen Langsung dan yang lain menggunakan kelas EventProcessorHost. Contoh kedua pendekatan ini dapat ditemukan di Panduan Pemrograman Azure Event Hubs. Versi singkat dari perbedaan tersebut adalah, Direct Consumer memberi Anda kontrol penuh dan EventProcessorHost melakukan beberapa pekerjaan pemipaan untuk Anda namun membuat asumsi tertentu tentang bagaimana Anda memproses kejadian tersebut.

EventProcessorHost

Dalam sampel ini, kami menggunakan EventProcessorHost untuk kesederhanaan, namun itu mungkin bukan pilihan yang terbaik untuk skenario khusus ini. EventProcessorHost melakukan kerja keras untuk memastikan Anda tidak perlu khawatir mengenai masalah threading dalam kelas prosesor kejadian tertentu. Namun, di dalam skenario kami, kami hanya mengonversi pesan ke format lain dan meneruskannya ke layanan lain menggunakan metode asinkron. Tidak perlu memperbarui keadaan bersama dan karena itu tidak ada risiko dalam masalah threading. Untuk sebagian besar skenario, EventProcessorHost mungkin adalah pilihan terbaik dan tentu saja menjadi pilihan yang lebih mudah.

IEventProcessor

Konsep sentral saat menggunakan EventProcessorHost adalah membuat implementasi antarmuka IEventProcessor, yang berisi metode ProcessEventAsync. Inti dari metode tersebut ditunjukkan di sini:

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

Daftar objek EventData diteruskan ke dalam metode dan kami melakukan iterasi ke daftar tersebut. Byte dari setiap metode diuraikan ke dalam objek HttpMessage dan objek tersebut diteruskan ke instans IHttpMessageProcessor.

HttpMessage

Instans HttpMessage berisi tiga bagian data:

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

}

Instans HttpMessage berisi GUID MessageId yang memungkinkan kami untuk menghubungkan permintaan HTTP ke respons HTTP yang sesuai dan nilai boolean yang mengidentifikasi apakah objek berisi instans httpRequestMessage dan HttpResponseMessage. Dengan menggunakan kelas HTTP bawaan dari System.Net.Http, saya dapat memanfaatkan kode application/http penguraian yang disertakan dalam System.Net.Http.Formatting.

IHttpMessageProcessor

Instans HttpMessage kemudian diteruskan ke implementasi IHttpMessageProcessor, yang merupakan antarmuka buatan saya untuk memisahkan penerimaan dan interpretasi kejadian dari Azure Event Hub dan pemrosesan aktualnya.

Meneruskan pesan HTTP

Untuk sampel ini, saya memutuskan bahwasanya akan menarik untuk mendorong Permintaan HTTP ke Moesif API Analytics. Moesif adalah layanan berbasis cloud yang mengkhususkan diri dalam analitik dan penelusuran kesalahan HTTP. Mereka memiliki tingkat gratis, sehingga mudah untuk dicoba dan memungkinkan kami untuk melihat permintaan HTTP secara real-time yang mengalir melalui layanan API Management kami.

Implementasi IHttpMessageProcessor akan terlihat seperti ini,

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 memanfaatkan pustaka C# API untuk Moesif yang memudahkan untuk mendorong data kejadian HTTP ke dalam layanan mereka. Untuk mengirim data HTTP ke Moesif Collector API, Anda memerlukan akun dan Id Aplikasi. Anda mendapatkan Id Aplikasi Moesif dengan membuat akun di situs web Moesif lalu masuk ke Pengaturan Aplikasi -> Menu Kanan Atas.

Sampel lengkap

Kode sumber dan pengujian untuk sampel berada di GitHub. Anda memerlukan API Management Service, Hub Kejadian yang terhubung, dan Akun Penyimpanan untuk menjalankan sampel sendiri.

Sampel hanyalah aplikasi Konsol sederhana yang mendengarkan kejadian yang berasal dari Hub Kejadian, mengubahnya menjadi sebuah Moesif EventRequestModel dan objek EventResponseModel serta meneruskannya ke Moesif Collector API.

Dalam gambar animasi berikut, Anda dapat melihat permintaan yang dibuat ke API di Portal Pengembang, aplikasi Konsol yang menampilkan pesan yang diterima, diproses, dan diteruskan, lalu permintaan dan respons muncul di Event Stream.

Demonstrasi permintaan diteruskan ke Runscope

Ringkasan

Layanan Azure API Management menyediakan tempat yang ideal untuk menangkap lalu lintas HTTP yang bepergian ke dan dari API Anda. Azure Event Hubs adalah solusi berbiaya rendah yang sangat dapat diskalakan untuk menangkap lalu lintas tersebut dan mengumpankannya ke dalam sistem pemrosesan sekunder untuk pencatatan, pemantauan, dan analitik canggih lainnya. Menghubungkan ke sistem pemantauan lalu lintas pihak ketiga seperti Moesif sesederhana beberapa lusin baris kode.

Langkah berikutnya