共用方式為


利用 Azure API 管理、事件中樞與 Moesif 監視您的 API

適用於:所有 APIM 層

API 管理服務 提供許多功能,以增強傳送至 HTTP API 之 HTTP 要求的處理。 不過,要求和回應的存在都是暫時的。 要求會提出,並透過 API 管理 服務流向您的後端 API。 您的 API 會處理此要求,而回應會傳回給 API 取用者。 API 管理服務會保留一些有關 API 的重要統計資料,以顯示在 Azure 入口網站儀表板上,但除此之外,詳細資料會消失。

藉由在「API 管理」服務中使用 log-to-eventhub 原則,您便可以將任何來自要求和回應的詳細資料傳送到 Azure 事件中樞 (部分機器翻譯)。 您可能想要從傳送至 API 的 HTTP 訊息產生事件的原因有很多。 範例包括更新稽核線索、使用量分析、例外狀況警示和第三方整合。

本文示範如何擷取整個 HTTP 要求和回應訊息,將它傳送至事件中樞,然後將該訊息轉送至提供 HTTP 記錄和監視服務的協力廠商服務。

為什麼要從 API 管理 服務傳送?

您可以編寫可以插入 HTTP API 框架的 HTTP 中介軟體,以捕獲 HTTP 請求和響應,並將其饋送到日誌記錄和監控系統中。 這種方法的缺點是 HTTP 中間件需要整合到後端 API 中,並且必須與 API 平台相匹配。 如果有多個 API,則每個 API 都必須部署中介軟體。 後端 API 無法升級通常有一些原因。

使用 Azure API 管理服務來與記錄基礎結構整合,提供了一個集中式且平台獨立的解決方案。 它也是可調整的,部分原因是 Azure API 管理 的 異地複寫 功能。

為什麼要傳送到事件中樞?

有理由問:為什麼要建立 Azure 事件中樞特有的原則? 您可能想要在許多不同的地方記錄您的請求。 為什麼不能直接將要求傳送到最終目的地? 這是一個選擇。 不過,從 API 管理服務提出記錄要求時,必須考慮記錄訊息如何影響 API 的效能。 增加系統元件的可用執行個體或利用異地複寫功能,可以處理逐漸增加的負載。 不過,如果記錄基礎結構的要求開始變慢而低於負載,則流量短期突增可能會導致延遲要求。

Azure 事件中樞的設計目的是要輸入大量資料,其處理事件數目的能力遠高於大部分 API 處理的 HTTP 要求數目。 事件中樞可作為您的 API 管理服務與基礎結構之間有點複雜的緩衝區,將會儲存及處理訊息。 這可確保您的 API 效能不會因為記錄基礎結構而受到影響。

資料一旦傳遞至事件中樞,就會保存起來並等待事件中樞取用者進行處理。 事件中樞不在乎其處理方式,只在乎確保系統能成功傳遞訊息。

事件中樞能夠將事件串流至多個取用者群組。 如此一來,即可由不同的系統來處理事件。 這支援許多整合案例,而不會在 API 管理 服務內處理 API 要求時造成更多延遲,因為只需要產生一個事件。

傳送應用程式/HTTP 訊息的原則

事件中樞接受簡單字串形式的事件資料。 該字串的內容由您決定。 若要能夠封裝 HTTP 要求並將其傳送至 Azure 事件中樞,您必須使用要求或回應資訊來格式化字串。 在這種情況下,如果有可以重複使用的現有格式,那麼您可能不必編寫自己的解析程式碼。 一開始,您可以考慮使用 HAR 來傳送 HTTP 要求和回應。 不過,這種格式最適合用於儲存 JSON 格式的一連串 HTTP 要求。 其中包含了許多必要元素,讓透過網路傳遞 HTTP 訊息的案例增加了不必要的複雜度。

另一個選項是使用 application/http HTTP 規格 RFC 7230 中所述的媒體類型。 此媒體類型使用與實際透過網路傳送 HTTP 訊息的格式完全相同,但整個訊息可以放在另一個 HTTP 要求的內文中。 在我們的案例中,我們只會使用此本文作為我們傳送到事件中樞的訊息。 Microsoft ASP.NET Web API 2.2 用戶端 (英文) 程式庫中有一個剖析器,可以剖析此格式並將它轉換成原生 HttpRequestMessageHttpResponseMessage 物件,相當方便。

為了能夠建立此訊息,我們需要在 Azure API 管理中利用以 C# 為基礎的原則運算式。 以下是原則,會將 HTTP 要求訊息傳送至 Azure 事件中樞。

<log-to-eventhub logger-id="myapilogger" 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>

原則宣告

此原則運算式有一些值得一提的特別之處。 策略 log-to-eventhub 具有名為 logger-id 的屬性,該屬性參考在 API 管理服務中建立的記錄器的名稱。 您可以在 如何在 Azure API 管理 中將事件記錄到 Azure 事件中樞文件中找到如何在 API 管理 服務中設定事件中樞記錄器的詳細數據。 第二個屬性是一個選擇性參數,其指示事件中樞要在哪個資料分割中儲存訊息。 事件中樞會使用資料分割來達到延展性,而且需要至少兩個資料分割。 只保證在一個資料分割內的訊息會依序傳遞。 如果我們未指示 Azure 事件中樞要在哪一個資料分割中放置訊息,它會使用循環配置資源演算法來分散負載。 不過,這可能會導致某些訊息處理不正常。

資料分割

為了確保訊息依序傳遞給消費者,並利用分割區的負載分配能力,我們可以將 HTTP 請求訊息傳送到一個分割區,並將 HTTP 回應訊息傳送到第二個分割區。 這可確保負載分佈均勻,並可以保證所有請求和所有回應都按順序使用。 回應可能會在對應的請求之前被使用,但這不是問題,因為我們有不同的機制來將請求與回應相關聯,而且我們知道請求總是先於回應。

HTTP 裝載

建置 requestLine之後,請檢查是否應該截斷要求本文。 要求本文會被截斷成只有 1024 個字元。 您可以增加此值;不過,個別的事件中樞訊息受限於 256 KB,因此有些 HTTP 訊息本文可能會無法放入單一訊息中。 執行日誌記錄和分析時,您可以僅從 HTTP 請求行和標頭中獲取大量資訊。 此外,許多 API 只要求傳回小型正文,因此與減少保留所有正文內容的傳輸、處理和儲存成本相比,截斷大型正文所造成的資訊價值損失相當小。

關於處理正文的最後一點注意事項是,我們需要傳遞 trueAs<string>() 方法,因為我們正在讀取正文內容,但我們也希望後端 API 能夠讀取正文。 我們將 true 傳遞至這個方法,造成本文進行緩衝處理,以便第二次讀取本文。 如果您的 API 會上傳大型檔案或使用長輪詢,這一點很重要。 在這些情況下,最好完全避免讀取本文。

HTTP 標頭

HTTP 標頭可以轉換成採用簡單索引鍵/值組格式的訊息格式。 我們選擇刪除某些安全敏感欄位,以避免不必要地洩露憑證資訊。 API 金鑰及其他認證不太可能用於分析。 如果我們想要分析使用者和他們正在使用的特定產品,那麼我們可以從物件中 context 獲取並將其添加到訊息中。

訊息中繼資料

在建立要傳送至事件中樞的完整訊息時,前線實際上並不是 application/http 訊息的一部分。 第一行是額外的中繼資料,由訊息是要求或回應訊息以及用來使回應與要求相互關聯的訊息識別碼所組成。 使用如下所示的另一個原則,即可建立訊息識別碼:

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

我們可以建立請求訊息,將其儲存在變數中,直到傳回回應,然後將請求和回應作為單一訊息發送。 然而,透過獨立發送請求和回應並使用 a message-id 將兩者關聯起來,我們在訊息大小方面獲得了更大的靈活性,能夠利用多個分割區同時保持訊息順序,並更快地將請求到達我們的日誌記錄儀表板。 在某些情況下,有效回應可能永遠不會傳送至事件中樞 (可能是因為 API 管理 服務中的嚴重要求錯誤) ,但我們仍然有要求的記錄。

用於傳送回應 HTTP 訊息的原則看起來類似要求,所以完整的原則組態如下所示:

<policies>
  <inbound>
      <set-variable name="message-id" value="@(Guid.NewGuid())" />
      <log-to-eventhub logger-id="myapilogger" 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="myapilogger" 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>

set-variable 原則會建立一個可供 log-to-eventhub 區段和 <inbound> 區段中的 <outbound> 原則存取的值。

從事件中樞接收事件

使用 AMQP 通訊協定 (英文) 可從 Azure 事件中樞接收事件。 Microsoft 服務匯流排團隊已提供用戶端程式庫,以便取用事件。 支援兩種不同的方法:一個方法是成為「直接取用者」,另一個方法是使用 EventProcessorHost 類別。 您可以在 事件中樞範例存放庫中找到這兩種方法的範例。 簡而言之,差別在於:Direct Consumer 給您完整控制權,而 EventProcessorHost 會替您做一些繁雜工作,但會假設您將如何處理這些事件。

EventProcessorHost

在此範例中,為了簡單起見,我們使用 , EventProcessorHost 不過它可能不是此特定案例的最佳選擇。 EventProcessorHost 會努力確定您不必擔心特定事件處理器類別內的執行緒問題。 不過,在我們的案例中,我們會將訊息轉換成另一種格式,並使用非同步方法將它傳遞至另一個服務。 不需要更新共用狀態,因此沒有執行緒問題的風險。 在大部分的情況下,EventProcessorHost 可能是最佳選擇,當然也是比較容易的選項。

IEventProcessor

使用 EventProcessorHost 時的中心概念是建立 IEventProcessor 介面的實作,其中包含 ProcessEventAsync 方法。 這是該方法的本質:

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 物件清單傳遞至此方法,而我們會逐一查看該清單。 每個方法的位元組會被剖析為 HttpMessage 物件,而該物件會傳遞至 IHttpMessageProcessor 的執行個體。

HttpMessage

HttpMessage 執行個體包含三個資料片段:

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

}

HttpMessage 執行個體包含 MessageId GUID,它可讓我們將 HTTP 要求連接到對應的 HTTP 回應和一個布林值 (以識別物件是否包含 HttpRequestMessage 和 HttpResponseMessage 的執行個體)。 從 System.Net.Http 使用內建 HTTP 類別,我才能夠利用 application/http 內含的 System.Net.Http.Formatting 剖析程式碼。

IHttpMessageProcessor

HttpMessage 執行個體會接著轉送到 IHttpMessageProcessor 的實作,這是我所建立的介面,用於分離事件的接收及解譯與 Azure 事件中樞及其實際處理。

轉送 HTTP 訊息

在此範例中,我們決定將 HTTP 請求推送至 Moesif API Analytics。 Moesif 是一項基於雲端的服務,專門從事 HTTP 分析和調試。 因為有免費套餐,所以很容易嘗試。 Moesif 可讓我們即時查看流經 API 管理 服務的 HTTP 要求。

IHttpMessageProcessor 實作如下所示:

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 的 C# API 程式庫,此程式庫可讓您輕鬆將 HTTP 事件資料推送到其服務。 若要將 HTTP 資料傳送至 Moesif 收集器 API,您需要帳戶和應用程式識別碼。您可以通過在 Moesif 網站上 創建一個帳戶來獲得 Moesif 應用程序 ID,然後轉到右上角菜單並選擇 應用程序設置

完整範例

範例的原始程式碼和測試位於 GitHub 上。 您需要 API 管理服務已連線的事件中樞儲存體帳戶,才能自行執行此範例。

此範例只是一個簡單的主控台應用程式,可接聽來自事件中樞的事件,將它們轉換成 Moesif EventRequestModelEventResponseModel 物件,然後將它們轉送至 Moesif 收集器 API。

在下列動畫影像中,您可以看到對 Developer Portal 中的 API 提出要求,主控台應用程式顯示正在接收、處理及轉遞的訊息,然後要求及回應顯示在事件串流中。

請求轉送至 Runscope 的動畫影像示範

摘要

Azure API 管理服務提供了一個理想位置,可供擷取您的 API 的雙向 HTTP 流量。 Azure 事件中樞是一個可高度擴充、低成本的解決方案,用來擷取該流量並將它饋送到次要處理系統中,以便進行記錄、監視和其他複雜的分析。 連線到第三方監視系統 (像是 Moesif) 就像數十行程式碼一樣簡單。