DiagnosticSource 和 DiagnosticListener
本文適用於:✔️.NET Core 3.1 和更新版本 ✔️ .NET Framework 4.5 和更新版本
System.Diagnostics.DiagnosticSource 模組允許檢測程式碼,供實際執行期間記錄豐富的資料承載,以在進行檢測的處理序內取用。 在執行時間,取用者可以動態地探索資料來源,並訂閱感興趣的資料來源。 System.Diagnostics.DiagnosticSource 依設計可允許處理序內的工具,存取豐富的資料。 使用 System.Diagnostics.DiagnosticSource 時,會假設取用者位於相同的處理序中,因此可以傳遞未序列化的類型 (例如 HttpResponseMessage
或 HttpContext
),讓客戶能運用大量的資料。
開始使用 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
接收事件的第一個步驟,是探索您關注哪個 DiagnosticListeners
。 DiagnosticListener
提供一種方法,能在執行時間探索系統內作用中的 DiagnosticListeners
。 而完成此作業的 API,是 AllListeners 屬性。
實作繼承自 IObservable
介面 (介面 IEnumerable
的「回呼」版本) 的 Observer<T>
類別。 若要深入了解,請參閱回應式延伸模組 (英文) 網站。
一個 IObserver
有三個回呼,OnNext
、OnComplete
和 OnError
。 IObservable
有一個稱為 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
類型。 有兩種方式可以取得更具體的資料:
如果該承載是已知的類型 (例如 string
或 HttpMessageRequest
),則可以只要將 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()
方法來連結回呼。 如此會對回呼提供所有事件。 但 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()
,這些呼叫就會包含在只接受事件名稱的多載中。 取用者必須確定其篩選可允許沒有內容的事件通過。