共用方式為


DiagnosticSource 和 DiagnosticListener

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

System.Diagnostics.DiagnosticSource 模組允許檢測程式碼,供實際執行期間記錄豐富的資料承載,以在進行檢測的處理序內取用。 在執行時間,取用者可以動態地探索資料來源,並訂閱感興趣的資料來源。 System.Diagnostics.DiagnosticSource 依設計可允許處理序內的工具,存取豐富的資料。 使用 System.Diagnostics.DiagnosticSource 時,會假設取用者位於相同的處理序中,因此可以傳遞未序列化的類型 (例如 HttpResponseMessageHttpContext),讓客戶能運用大量的資料。

開始使用 DiagnosticSource

本逐步解說示範如何使用 System.Diagnostics.DiagnosticSource,建立 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。 這是因為此程式碼只會寫入事件,因此只會關注 DiagnosticListener 所實作的方法 DiagnosticSource。 建立 DiagnosticListeners 時,會為其指定名稱,而此名稱應為相關事件 (通常是元件) 的邏輯群組名稱。 此名稱稍後會用來尋找接聽程式,並訂閱其任一事件。 因此,事件名稱只有在元件內才是唯一的。


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# 的匿名型別功能,一般可用於建立一個可傳遞「動態產生」的類型,並可讓此配置非常方便。 在檢測站台上,必須對相同事件名稱進行 IsEnabled() 檢查,以保護對 Write() 的呼叫。 如果沒有這項檢查,即使檢測處於非作用中狀態,按照 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 屬性。

實作繼承自 IObservable 介面 (介面 IEnumerable 的「回呼」版本) 的 Observer<T> 類別。 若要深入了解,請參閱回應式延伸模組 (英文) 網站。 一個 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 時,會取得所有作用中 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。 為避免發生競爭狀況,會在保護鎖定的情況下,執行共用變數的更新。

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

訂閱彼此無關。 因此,其他程式碼可以執行與程式碼範例完全相同的作業,並產生兩個記錄資訊的「管道」。

解碼承載

傳遞到回呼的 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 中所用的 PropertySpec (英文) 類別。)

篩選

在上述範例中,程式碼會使用 IObservable.Subscribe() 方法來連結回呼。 如此會對回呼提供所有事件。 但 DiagnosticListenerSubscribe() 的多載,可讓控制器能控制會指定哪些事件。

上述範例中的 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(),這些呼叫就會包含在只接受事件名稱的多載中。 取用者必須確定其篩選可允許沒有內容的事件通過。