利用 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 訊息的案例增加了不必要的複雜性。
替代選項是使用如 HTTP 規格 RFC 7230 中所述的 application/http
媒體類型。 此媒體類型會使用與透過網路用來實際傳送 HTTP 訊息完全相同的格式,但整個訊息可以放在另一個 HTTP 要求的本文中。 在我們的案例中,我們只會使用本文作為訊息傳送至事件中樞。 很方便地,有一個剖析器存在於 Microsoft ASP.NET Web API 2.2 用戶端 連結庫,可以剖析此格式,並將其轉換成原生 HttpRequestMessage
和 HttpResponseMessage
物件。
為了能夠建立此訊息,我們需要在 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 管理 服務內建立的記錄器名稱。 如需如何在 API 管理 服務中設定事件中樞記錄器的詳細數據,請參閱如何在 Azure API 管理 中將事件記錄至 Azure 事件中樞 檔。 第二個屬性是一個選擇性參數,其指示事件中樞要在哪個資料分割中儲存訊息。 事件中樞會使用分割區來啟用延展性,而且至少需要兩個。 只保證在一個資料分割內的訊息會依序傳遞。 如果未指示 Azure 事件中樞 要放置訊息的分割區,它會使用迴圈配置資源演算法來散發負載。 不過,這可能導致一些訊息未依順序處理。
資料分割
若要確保我們的訊息依序傳遞給取用者並利用資料分割的負載分散功能,我選擇將 HTTP 要求訊息傳送到一個資料分割,將 HTTP 回應訊息傳送到第二個資料分割。 如此可確保負載平均分散,而且我們可以保證所有要求和所有回應都會依序取用。 回應有可能在對應的要求之前取用,但這不是問題,因為我們有不同的機制將要求與回應相互關聯,我們知道要求一律會在回應之前出現。
HTTP 裝載
在建置 requestLine
之後,我們會查看是否應該截斷要求本文。 要求本文會被截斷成只有 1024 個字元。 這可能會增加,但個別事件中樞訊息限製為 256 KB,因此某些 HTTP 訊息主體可能不適合單一訊息。 進行記錄和分析時,可以從 HTTP 要求行和標頭衍生大量資訊。 此外,許多 API 要求只會傳回小型本文,所以相較於降低傳輸、處理和儲存成本來保留所有的本文內容,截斷大型本文所造成的資訊值遺失相當微小。 處理本文的最後一個注意事項是,我們需要傳遞 true
至 As<string>()
方法,因為我們正在讀取本文內容,但也希望後端 API 能夠讀取本文。 我們將 true 傳遞至這個方法,造成本文進行緩衝處理,以便第二次讀取本文。 一定要留意您的 API 是否上傳大型檔案或使用長輪詢。 在這些情況下,最好完全避免讀取本文。
HTTP 標頭
HTTP 標頭可以轉換成採用簡單索引鍵/值組格式的訊息格式。 我們已選擇刪除某些安全性機密欄位,以避免不必要地洩漏認證資訊。 API 金鑰和其他認證不太可能用於分析用途。 如果我們想要對使用者及其使用的特定產品進行分析,我們可以從 context
物件取得該數據,並將它新增至訊息。
訊息中繼資料
建置要傳送至事件中樞的完整訊息時,前線實際上不是訊息的 application/http
一部分。 第一行是額外的中繼資料,由訊息是要求或回應訊息以及用來使回應與要求相互關聯的訊息識別碼所組成。 使用如下所示的另一個原則,即可建立訊息識別碼:
<set-variable name="message-id" value="@(Guid.NewGuid())" />
我們可能已建立要求訊息,將它儲存在變數中,直到傳回回應為止,然後將要求和回應當作單一訊息傳送。 不過,藉由獨立傳送要求和回應,並使用 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
原則會建立一個可供 <inbound>
區段和 <outbound>
區段中的 log-to-eventhub
原則存取的值。
從事件中樞接收事件
使用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 類別,我才能夠利用 System.Net.Http.Formatting
內含的 application/http
剖析程式碼。
IHttpMessageProcessor
然後,實例HttpMessage
會轉送至 的IHttpMessageProcessor
實作,這是我建立的介面,將事件的接收和解譯與 Azure 事件中樞 和實際處理分離。
轉送 HTTP 訊息
在此範例中,我認為將 HTTP 要求推送至 Moesif API Analytics 很有趣。 Moesif 是專門從事 HTTP 分析與偵錯的雲端服務。 該服務有免費層,因此可輕易試用,它可讓我們即時看到流經 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 Collector API,您需要帳戶與應用程式識別碼。您可以透過在 Moesif 的網站 (英文) 上建立帳戶來取得 Moesif 應用程式識別碼,並移至 [右上方功能表] ->[App Setup] \(應用程式設定\)。
完整範例
範例的原始程式碼和測試位於 GitHub 上。 您需要 API 管理服務、已連線的事件中樞及儲存體帳戶,才能自行執行此範例。
此範例只是簡單的主控台應用程式,可接聽來自事件中樞的事件,將它們轉換為 Moesif EventRequestModel
和 EventResponseModel
物件,然後再轉送到 Moesif Collector API。
在下列動畫影像中,您可以看到在開發人員入口網站中對API提出要求,主控台應用程式會顯示正在接收、處理和轉送的訊息,然後要求和響應顯示在事件數據流中。
摘要
Azure API 管理服務提供了一個理想位置,可供擷取您的 API 的雙向 HTTP 流量。 Azure 事件中樞是一個可高度擴充、低成本的解決方案,用來擷取該流量並將它饋送到次要處理系統中,以便進行記錄、監視和其他複雜的分析。 連線到第三方監視系統 (像是 Moesif) 就像數十行程式碼一樣簡單。
下一步
- 深入了解 Azure 事件中樞
- 深入了解 API 管理和事件中樞的整合