本文適用於: ✔️ .NET Core 3.1 和更新版本 ✔️ .NET Framework 4.5 和更新版本
System.Diagnostics.DiagnosticSource 是一個模組,可讓程式代碼進行檢測,以記錄豐富數據承載的生產時間記錄,以在已檢測的程式內取用。 執行時,消費者可以動態發現資料來源並訂閱感興趣的資料。
System.Diagnostics.DiagnosticSource 是設計來允許進程內工具存取豐富數據。 使用 System.Diagnostics.DiagnosticSource時,會假設取用者位於相同的進程中,因此可以傳遞不可串行化的類型(例如 HttpResponseMessage 或 HttpContext),讓客戶能夠使用大量數據。
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 實作的方法 DiagnosticListener 。
DiagnosticListeners 會在建立名稱時指定名稱,而此名稱應該是相關事件的邏輯群組名稱(通常是元件)。
之後會使用此名稱來尋找「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
接收事件的第一個步驟是發現您感興趣的事件 DiagnosticListeners 。
DiagnosticListener 支援在執行時偵測系統中活躍的DiagnosticListeners的方法。 API要完成此作業的是AllListeners屬性。
實作一個Observer<T>類別,該類別繼承IObservable介面,這是IEnumerable介面的「回調」版本。 您可以在 Reactive Extensions 網站深入了解更多資訊。
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時,您會取得所有 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。 有兩種方式可取得更具體的數據:
如果承載是已知的類型(例如, 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 類別。
篩選
在上述範例中,程式代碼會使用 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(),那麼這些呼叫就會被包含在一個只接收事件名稱的多載中。 消費者必須確保其篩選器允許沒有上下文的事件通過。