記錄用戶端對 Web API 發出的所有請求

摘要:本文說明如何使用自訂的 ITraceWriter 服務來記錄用戶端對你的 Web API 發出的請求。適用 ASP.NET Web API 2.x。

把用戶端對你的 Web API 請求全部記錄下來(例如寫入 log 檔案)有兩個好處:
  • 除錯
  • 了解用戶端應用程式是如何使用你的 Web API
至於怎麼做,網路上已經可以找到不少相關的教學文章,例如 Mike Wasson 的 Tracing in ASP.NET Web API 2。其中的關鍵在於了解如何實作自訂的 ITraceWriter 服務,並將你的自訂服務註冊至 ASP.NET Web API 框架。

這裡不打算提 Web API 框架運作的細節。不過,對於「如何記錄用戶端請求」這個目的,必須特別提醒:ITraceWriter 服務能夠記錄的東西很多,並不是只能夠用來記錄用戶端請求。若不理解這點,你可能會發現實際寫入 log 檔案的訊息多到不像話,就像接下來的範例所展示的那樣。

後記:經 91 兄提醒,欲記錄 Web API 的用戶端請求,使用 message handler 更輕巧,也更適合。我把這個部分補在文後〈延伸閱讀〉裡面。(明明以前寫過,卻硬是忘了提,該不會年紀大了...Orz)

範例

首先,在你的 Web API 專案中加入一個類別,命名為 WebApiTraceWriter,如下所示。

using System;
using System.Web.Http.Tracing;

namespace TraceWriterDemo
{
    public class WebApiTraceWriter : ITraceWriter
    {
        public void Trace(System.Net.Http.HttpRequestMessage request, 
                          string category, TraceLevel level, 
                          Action<tracerecord> traceAction)
        {
            if (level != TraceLevel.Off)
            {
                TraceRecord traceRecord = new TraceRecord(request, category, level);
                traceAction(traceRecord);            
                Log(traceRecord);
            }
        }

        private void Log(TraceRecord traceRecord) 
        {
            if (traceRecord.Request == null || traceRecord.Request.Method == null 
                || traceRecord.Request.RequestUri == null)
            {
                return;
            }
            string s = String.Format("{0} {1}\r\n", traceRecord.Request.Method, traceRecord.Request.RequestUri);
            System.IO.File.AppendAllText(@"C:\temp\webapi-log.txt", s);
        }
    }
}

簡單起見,這裡只是用 File.AppendAllText 來把 log 內容寫入至檔案。通常我是用 NLog 來記錄,而且是記錄在一個單獨的檔案裡(例如 webapi-log.txt)。

接著,將你的自訂 ITraceWriter 服務註冊至 Web API 框架:

    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services
            // (略)

            config.Services.Replace(typeof(ITraceWriter), new WebApiTraceWriter());
        }
    }


在 Visual Studio 中把這個 Web 應用程式執行起來,然後用瀏覽器對此應用程式的某個 Web API 發出請求,以便觀察 webapi-log.txt 的檔案內容。

結果是,明明只發出一次請求,webapi-log.txt 裡面卻出現了 26 筆重複的紀錄。像這樣:

GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default
GET http://localhost:34942/api/Default

正如前面說的,ITraceWriter 服務能夠紀錄的東西很多(因為觸發時機很多),這裡看到的重複紀錄只是因為 ITraceWriter 的 Trace 方法被觸發很多次而產生的現象。如果我們把剛才範例中的倒數第 5 行改成這樣:

    string s = String.Format("{0} {1} {2} {3}\r\n", 
            traceRecord.Method, traceRecord.Operation, 
            traceRecord.Operator, traceRecord.Request.RequestUri);

這次多輸出了 TraceRecord 的 Operation 和 Operator 屬性,結果會像這樣:

GET SelectController DefaultHttpControllerSelector http://localhost:34942/api/Default
GET SelectController DefaultHttpControllerSelector http://localhost:34942/api/Default
GET CreateController HttpControllerDescriptor http://localhost:34942/api/Default
GET Create DefaultHttpControllerActivator http://localhost:34942/api/Default
GET Create DefaultHttpControllerActivator http://localhost:34942/api/Default
GET CreateController HttpControllerDescriptor http://localhost:34942/api/Default
GET ExecuteAsync DefaultController http://localhost:34942/api/Default
GET SelectAction ApiControllerActionSelector http://localhost:34942/api/Default
GET SelectAction ApiControllerActionSelector http://localhost:34942/api/Default
GET ExecuteBindingAsync HttpActionBinding http://localhost:34942/api/Default
GET ExecuteBindingAsync HttpActionBinding http://localhost:34942/api/Default
GET InvokeActionAsync ApiControllerActionInvoker http://localhost:34942/api/Default
GET ExecuteAsync ReflectedHttpActionDescriptor http://localhost:34942/api/Default
GET ExecuteAsync ReflectedHttpActionDescriptor http://localhost:34942/api/Default
GET Negotiate DefaultContentNegotiator http://localhost:34942/api/Default
GET GetPerRequestFormatterInstance XmlMediaTypeFormatter http://localhost:34942/api/Default
GET GetPerRequestFormatterInstance XmlMediaTypeFormatter http://localhost:34942/api/Default
GET Negotiate DefaultContentNegotiator http://localhost:34942/api/Default
GET InvokeActionAsync ApiControllerActionInvoker http://localhost:34942/api/Default
GET ExecuteAsync DefaultController http://localhost:34942/api/Default
GET   http://localhost:34942/api/Default
GET WriteToStreamAsync XmlMediaTypeFormatter http://localhost:34942/api/Default
GET WriteToStreamAsync XmlMediaTypeFormatter http://localhost:34942/api/Default
GET Dispose DefaultController http://localhost:34942/api/Default
GET Dispose DefaultController http://localhost:34942/api/Default

這就明白為什麼先前範例所輸出的 log 檔案有來那麼多筆重複的紀錄了。

由於我只是想要紀錄用戶端對 Web API 發出的請求,因此勢必得要濾掉重複的紀錄。對此問題,我是利用 TraceRecord 的 RequestId 屬性來解決。

利用 TraceRecord.RequestId 來過濾重複的紀錄

把先前的範例改成底下這樣就行了:

    public class WebApiTraceWriter : ITraceWriter
    {
        private Guid _lastRequestId;

        public void Trace(System.Net.Http.HttpRequestMessage request, 
                          string category, TraceLevel level, 
                          Action<tracerecord> traceAction)
        {
            // 這部分維持不變
        }

        private void Log(TraceRecord traceRecord) 
        {
            if (traceRecord.Request == null || traceRecord.Request.Method == null 
                || traceRecord.Request.RequestUri == null)
            {
                return;
            }
            if (traceRecord.RequestId.Equals(_lastRequestId)) // 忽略已經記錄過的請求
            {
                return;
            }
            _lastRequestId = traceRecord.RequestId;

            string s = String.Format("{0} {1}\r\n", traceRecord.Request.Method, traceRecord.Request.RequestUri);
            System.IO.File.AppendAllText(@"C:\temp\webapi-log.txt", s);
        }
    }

小結

如果擔心此作法會影響應用程式的執行效能,可以在 web.config 裡面加一個參數,比如說 WebApiTracingEnabled。然後,在 WebApiConfig 的 Register 方法裡面判斷此參數的值,若為 False,就不要註冊我們自訂的 WebApiTraceWriter。

到 Github 取得範例原始碼(使用 NLog)

Happy coding!

延伸閱讀

沒有留言:

技術提供:Blogger.
回頂端⬆️