共用方式為


DiagnosticSource 和 DiagnosticListener

本文適用於: ✔️ .NET Core 3.1 和更新版本 ✔️ .NET Framework 4.5 和更新版本

System.Diagnostics.DiagnosticSource 是一個模組,可讓程式代碼進行檢測,以記錄豐富數據承載的生產時間記錄,以在已檢測的程式內取用。 執行時,消費者可以動態發現資料來源並訂閱感興趣的資料。 System.Diagnostics.DiagnosticSource 是設計來允許進程內工具存取豐富數據。 使用 System.Diagnostics.DiagnosticSource時,會假設取用者位於相同的進程中,因此可以傳遞不可串行化的類型(例如 HttpResponseMessageHttpContext),讓客戶能夠使用大量數據。

DiagnosticSource 用戶入門

本逐步解說示範如何使用 建立 DiagnosticSource 事件和檢測程序 System.Diagnostics.DiagnosticSource代碼。 然後,它會說明如何透過尋找有趣的 DiagnosticListeners,訂閱他們的事件,並解碼事件數據的有效負載來處理事件。 其完成方式是描述 篩選,只允許特定事件通過系統。

DiagnosticSource 實作

您將使用下列程序代碼。 此程式代碼是 HttpClient 類別,其方法 SendWebRequest 會將 HTTP 要求傳送至 URL 並接收回復。

using System.Diagnostics;
MyListener TheListener = new MyListener();
TheListener.Listening();
HTTPClient Client = new HTTPClient();
Client.SendWebRequest("https://learn.microsoft.com/dotnet/core/diagnostics/");

class HTTPClient
{
    private static DiagnosticSource httpLogger = new DiagnosticListener("System.Net.Http");
    public byte[] SendWebRequest(string url)
    {
        if (httpLogger.IsEnabled("RequestStart"))
        {
            httpLogger.Write("RequestStart", new { Url = url });
        }
        //Pretend this sends an HTTP request to the url and gets back a reply.
        byte[] reply = new byte[] { };
        return reply;
    }
}
class Observer<T> : IObserver<T>
{
    public Observer(Action<T> onNext, Action onCompleted)
    {
        _onNext = onNext ?? new Action<T>(_ => { });
        _onCompleted = onCompleted ?? new Action(() => { });
    }
    public void OnCompleted() { _onCompleted(); }
    public void OnError(Exception error) { }
    public void OnNext(T value) { _onNext(value); }
    private Action<T> _onNext;
    private Action _onCompleted;
}
class MyListener
{
    IDisposable networkSubscription;
    IDisposable listenerSubscription;
    private readonly object allListeners = new();
    public void Listening()
    {
        Action<KeyValuePair<string, object>> whenHeard = delegate (KeyValuePair<string, object> data)
        {
            Console.WriteLine($"Data received: {data.Key}: {data.Value}");
        };
        Action<DiagnosticListener> onNewListener = delegate (DiagnosticListener listener)
        {
            Console.WriteLine($"New Listener discovered: {listener.Name}");
            //Subscribe to the specific DiagnosticListener of interest.
            if (listener.Name == "System.Net.Http")
            {
                //Use lock to ensure the callback code is thread safe.
                lock (allListeners)
                {
                    if (networkSubscription != null)
                    {
                        networkSubscription.Dispose();
                    }
                    IObserver<KeyValuePair<string, object>> iobserver = new Observer<KeyValuePair<string, object>>(whenHeard, null);
                    networkSubscription = listener.Subscribe(iobserver);
                }

            }
        };
        //Subscribe to discover all DiagnosticListeners
        IObserver<DiagnosticListener> observer = new Observer<DiagnosticListener>(onNewListener, null);
        //When a listener is created, invoke the onNext function which calls the delegate.
        listenerSubscription = DiagnosticListener.AllListeners.Subscribe(observer);
    }
    // Typically you leave the listenerSubscription subscription active forever.
    // However when you no longer want your callback to be called, you can
    // call listenerSubscription.Dispose() to cancel your subscription to the IObservable.
}

執行提供的實作會印出至控制台。

New Listener discovered: System.Net.Http
Data received: RequestStart: { Url = https://learn.microsoft.com/dotnet/core/diagnostics/ }

記錄事件

DiagnosticSource 類型是抽象基類,可定義記錄事件所需的方法。 儲存實作的類別為 DiagnosticListener。 在程式碼中添加工具 DiagnosticSource 的第一步驟是建立 DiagnosticListener 工具。 例如:

private static DiagnosticSource httpLogger = new DiagnosticListener("System.Net.Http");

請注意, httpLogger 類型為 DiagnosticSource。 這是因為此程式代碼只會寫入事件,因此只關注 DiagnosticSource 實作的方法 DiagnosticListenerDiagnosticListeners 會在建立名稱時指定名稱,而此名稱應該是相關事件的邏輯群組名稱(通常是元件)。 之後會使用此名稱來尋找「Listener」並訂閱其事件。 因此,事件名稱只需要在元件內是唯一的。


DiagnosticSource記錄介面包含兩種方法:

    bool IsEnabled(string name)
    void Write(string name, object value);

這是儀器場地專用的。 您必須檢查儀表位置,以查看有哪些類型傳入IsEnabled。 這可讓您了解要轉換承載的內容。

典型的通話網站看起來會像這樣:

if (httpLogger.IsEnabled("RequestStart"))
{
    httpLogger.Write("RequestStart", new { Url = url });
}

每個事件都有一個 string 名稱(例如 RequestStart),而且只有一個 object 做為承載。 如果您需要傳送多個項目,您可以建立 object 來包含其所有資訊的屬性。 C# 的 匿名類型 功能通常用來建立類型來傳遞「即時」,並讓此配置非常方便。 在儀器站臺上,您必須使用相同事件名稱的 Write() 檢查來保護對 IsEnabled() 的呼叫。 如果沒有這項檢查,即便儀器處於非使用中狀態,C# 語言的規則要求仍需完成建立承載 object 和呼叫 Write() 的所有工作,即使實際上沒有任何事物接收數據。 藉由監控 Write() 呼叫,您可以在來源未啟用時提高其效率。

結合您擁有的一切

class HTTPClient
{
    private static DiagnosticSource httpLogger = new DiagnosticListener("System.Net.Http");
    public byte[] SendWebRequest(string url)
    {
        if (httpLogger.IsEnabled("RequestStart"))
        {
            httpLogger.Write("RequestStart", new { Url = url });
        }
        //Pretend this sends an HTTP request to the url and gets back a reply.
        byte[] reply = new byte[] { };
        return reply;
    }
}

發現 DiagnosticListeners

接收事件的第一個步驟是發現您感興趣的事件 DiagnosticListenersDiagnosticListener 支援在執行時偵測系統中活躍的DiagnosticListeners的方法。 API要完成此作業的是AllListeners屬性。

實作一個Observer<T>類別,該類別繼承IObservable介面,這是IEnumerable介面的「回調」版本。 您可以在 Reactive Extensions 網站深入了解更多資訊。 IObserver有三個回呼:OnNextOnCompleteOnErrorIObservable 具有一個名為 Subscribe 的單一方法,這個方法接收其中一個觀察者。 一旦連接,觀察者會在發生情況時收到回呼通知(大部分是 OnNext 回呼)。

靜態屬性的 AllListeners 一般用法如下所示:

class Observer<T> : IObserver<T>
{
    public Observer(Action<T> onNext, Action onCompleted)
    {
        _onNext = onNext ?? new Action<T>(_ => { });
        _onCompleted = onCompleted ?? new Action(() => { });
    }
    public void OnCompleted() { _onCompleted(); }
    public void OnError(Exception error) { }
    public void OnNext(T value) { _onNext(value); }
    private Action<T> _onNext;
    private Action _onCompleted;
}
class MyListener
{
    IDisposable networkSubscription;
    IDisposable listenerSubscription;
    private readonly object allListeners = new();
    public void Listening()
    {
        Action<KeyValuePair<string, object>> whenHeard = delegate (KeyValuePair<string, object> data)
        {
            Console.WriteLine($"Data received: {data.Key}: {data.Value}");
        };
        Action<DiagnosticListener> onNewListener = delegate (DiagnosticListener listener)
        {
            Console.WriteLine($"New Listener discovered: {listener.Name}");
            //Subscribe to the specific DiagnosticListener of interest.
            if (listener.Name == "System.Net.Http")
            {
                //Use lock to ensure the callback code is thread safe.
                lock (allListeners)
                {
                    if (networkSubscription != null)
                    {
                        networkSubscription.Dispose();
                    }
                    IObserver<KeyValuePair<string, object>> iobserver = new Observer<KeyValuePair<string, object>>(whenHeard, null);
                    networkSubscription = listener.Subscribe(iobserver);
                }

            }
        };
        //Subscribe to discover all DiagnosticListeners
        IObserver<DiagnosticListener> observer = new Observer<DiagnosticListener>(onNewListener, null);
        //When a listener is created, invoke the onNext function which calls the delegate.
        listenerSubscription = DiagnosticListener.AllListeners.Subscribe(observer);
    }
    // Typically you leave the listenerSubscription subscription active forever.
    // However when you no longer want your callback to be called, you can
    // call listenerSubscription.Dispose() to cancel your subscription to the IObservable.
}

此程式碼會建立回調委託,並使用 AllListeners.Subscribe 方法要求委託被呼叫,以針對系統中每個作用中的 DiagnosticListener。 是否訂閱接聽程式的決定是藉由檢查其名稱來進行。 上述程式代碼正在尋找您先前建立的 『System.Net.Http' 接聽程式。

像所有對Subscribe()的呼叫一樣,此呼叫會傳回一個IDisposable,代表訂閱本身。 只要沒有在這個訂閱物件上呼叫 Dispose(),回呼就會繼續發生。 程式代碼範例永遠不會呼叫 Dispose(),因此它會永遠接收回呼。

當您訂閱 AllListeners時,您會取得所有 ACTIVE DiagnosticListeners的回呼。 因此,當您訂閱時,會收到所有現有 DiagnosticListeners 的回呼,並且當有新的 DiagnosticListeners 創建時,您也會收到它們的回呼。 您會收到可訂閱之所有專案的完整清單。

訂閱「DiagnosticListeners」

DiagnosticListener 實作 IObservable<KeyValuePair<string, object>> 介面,因此您也可以呼叫 Subscribe()。 下列程式代碼示範如何填寫上一個範例:

IDisposable networkSubscription;
IDisposable listenerSubscription;
private readonly object allListeners = new();
public void Listening()
{
    Action<KeyValuePair<string, object>> whenHeard = delegate (KeyValuePair<string, object> data)
    {
        Console.WriteLine($"Data received: {data.Key}: {data.Value}");
    };
    Action<DiagnosticListener> onNewListener = delegate (DiagnosticListener listener)
    {
        Console.WriteLine($"New Listener discovered: {listener.Name}");
        //Subscribe to the specific DiagnosticListener of interest.
        if (listener.Name == "System.Net.Http")
        {
            //Use lock to ensure the callback code is thread safe.
            lock (allListeners)
            {
                if (networkSubscription != null)
                {
                    networkSubscription.Dispose();
                }
                IObserver<KeyValuePair<string, object>> iobserver = new Observer<KeyValuePair<string, object>>(whenHeard, null);
                networkSubscription = listener.Subscribe(iobserver);
            }

        }
    };
    //Subscribe to discover all DiagnosticListeners
    IObserver<DiagnosticListener> observer = new Observer<DiagnosticListener>(onNewListener, null);
    //When a listener is created, invoke the onNext function which calls the delegate.
    listenerSubscription = DiagnosticListener.AllListeners.Subscribe(observer);
}

在此範例中,尋找 'System.Net.Http' DiagnosticListener之後,系統會建立一個動作,列印出監聽器的名稱、事件名稱,以及payload.ToString()的詳細資訊。

備註

DiagnosticListener 實現 IObservable<KeyValuePair<string, object>>。 這表示在每個回呼上,我們都會得到KeyValuePair。 這個配對的索引鍵是事件的名稱,而值是承載 object。 此範例只會將此資訊記錄至主控台。

務必追蹤 DiagnosticListener 的訂用帳戶。 在先前的程式代碼中,變數 networkSubscription 會記住這件事。 如果您建立另一個 creation,則必須取消訂閱先前的監聽器並訂閱新的監聽器。

程序 DiagnosticSource/DiagnosticListener 代碼是安全線程,但回呼程式代碼也必須是安全線程。 為了確保回呼程式代碼是執行緒安全,使用鎖來管理。 可以同時建立兩個具有相同名稱的 DiagnosticListeners。 為了避免競爭條件,共用變數的更新會在鎖的保護下執行。

執行先前的程式代碼之後,下次 Write() 在 'System.Net.Http' DiagnosticListener 上完成 時,資訊就會記錄到控制台。

訂閱是彼此獨立的。 因此,其他程式代碼可以執行與程式碼範例完全相同的事情,併產生兩個記錄資訊的「管道」。

解碼有效載荷

KeyvaluePair 傳遞至回呼函數的對象具有事件名稱和有效負載,但有效負載被簡單地類型化為 object。 有兩種方式可取得更具體的數據:

如果承載是已知的類型(例如, stringHttpMessageRequest),則您可以直接將 轉換成 object 預期的型別(使用 as 運算符,以免在發生錯誤時造成例外狀況),然後存取字段。 這是非常有效率的。

使用反射 API。 例如,假設有下列方法。

    /// Define a shortcut method that fetches a field of a particular name.
    static class PropertyExtensions
    {
        static object GetProperty(this object _this, string propertyName)
        {
            return _this.GetType().GetTypeInfo().GetDeclaredProperty(propertyName)?.GetValue(_this);
        }
    }

若要更完整地解碼承載,您可以將 listener.Subscribe() 呼叫替換掉,改為使用下列程式碼。

    networkSubscription = listener.Subscribe(delegate(KeyValuePair<string, object> evnt) {
        var eventName = evnt.Key;
        var payload = evnt.Value;
        if (eventName == "RequestStart")
        {
            var url = payload.GetProperty("Url") as string;
            var request = payload.GetProperty("Request");
            Console.WriteLine("Got RequestStart with URL {0} and Request {1}", url, request);
        }
    });

請注意,使用反射相對來說資源消耗較多。 不過,如果載荷是使用匿名類型產生的,那麼使用反射是唯一的選擇。 使用 PropertyInfo.GetMethod.CreateDelegate() 或在 System.Reflection.Emit 命名空間中透過快速、專門的屬性擷取方法,可以降低此額外負荷,但超出本文的範圍。 (如需快速委派型屬性擷取器範例,請參閱 中所使用的 DiagnosticSourceEventSource 類別。

篩選

在上述範例中,程式代碼會使用 IObservable.Subscribe() 方法來連結回呼。 這會導致所有事件將會傳遞給回呼。 不過, DiagnosticListener 具有的多載 Subscribe() ,可讓控制器控制指定的事件。

來示範,上述範例中的 listener.Subscribe() 呼叫可以替換成以下程式碼。

    // Create the callback delegate.
    Action<KeyValuePair<string, object>> callback = (KeyValuePair<string, object> evnt) =>
        Console.WriteLine("From Listener {0} Received Event {1} with payload {2}", networkListener.Name, evnt.Key, evnt.Value.ToString());

    // Turn it into an observer (using the Observer<T> Class above).
    Observer<KeyValuePair<string, object>> observer = new Observer<KeyValuePair<string, object>>(callback);

    // Create a predicate (asks only for one kind of event).
    Predicate<string> predicate = (string eventName) => eventName == "RequestStart";

    // Subscribe with a filter predicate.
    IDisposable subscription = listener.Subscribe(observer, predicate);

    // subscription.Dispose() to stop the callbacks.

這會高效地僅訂閱『RequestStart』事件。 所有其他事件都會造成 DiagnosticSource.IsEnabled() 方法傳回 false ,因而有效率地篩選掉。

備註

篩選只是設計為效能優化。 即使事件不符合篩選條件,接聽程式仍然有可能收到事件。 這可能是因為某些其他接聽程式已訂閱事件,或是因為事件的來源在傳送之前未檢查 IsEnabled()。 如果您想要確定指定的事件符合篩選條件,您必須在回呼內檢查它。 例如:

    Action<KeyValuePair<string, object>> callback = (KeyValuePair<string, object> evnt) =>
        {
            if(predicate(evnt.Key)) // only print out events that satisfy our filter
            {
                Console.WriteLine("From Listener {0} Received Event {1} with payload {2}", networkListener.Name, evnt.Key, evnt.Value.ToString());
            }
        };
以內容為基礎的篩選

某些案例需要根據擴充內容進行進階篩選。 產生者可以呼叫 DiagnosticSource.IsEnabled 多載並提供其他事件屬性,如下列程式代碼所示。

//aRequest and anActivity are the current request and activity about to be logged.
if (httpLogger.IsEnabled("RequestStart", aRequest, anActivity))
    httpLogger.Write("RequestStart", new { Url="http://clr", Request=aRequest });

下一個程式代碼範例示範取用者可以使用這類屬性,更精確地篩選事件。

    // Create a predicate (asks only for Requests for certain URIs)
    Func<string, object, object, bool> predicate = (string eventName, object context, object activity) =>
    {
        if (eventName == "RequestStart")
        {
            if (context is HttpRequestMessage request)
            {
                return IsUriEnabled(request.RequestUri);
            }
        }
        return false;
    }

    // Subscribe with a filter predicate
    IDisposable subscription = listener.Subscribe(observer, predicate);

生產者不知道消費者所提供的過濾器。 DiagnosticListener 會叫用提供的篩選,並視需要省略其他自變數,因此篩選應該預期會收到 null 內容。 如果產生者以事件名稱和上下文呼叫 IsEnabled(),那麼這些呼叫就會被包含在一個只接收事件名稱的多載中。 消費者必須確保其篩選器允許沒有上下文的事件通過。