Condividi tramite


DiagnosticSource e DiagnosticListener

Questo articolo si applica a: ✔️ .NET Core 3.1 e versioni successive ✔️ .NET Framework 4.5 e versioni successive

System.Diagnostics.DiagnosticSource è un modulo che consente di instrumentare il codice per la registrazione in fase di produzione di payload di dati avanzati per l'utilizzo all'interno del processo instrumentato. In fase di esecuzione, i consumer possono individuare in modo dinamico le origini dati e sottoscrivere quelle di interesse. System.Diagnostics.DiagnosticSource è stato progettato per consentire agli strumenti in-process di accedere a dati avanzati. Quando si usa System.Diagnostics.DiagnosticSource, si presuppone che il consumer sia all'interno dello stesso processo e, di conseguenza, i tipi non serializzabili (ad esempio, HttpResponseMessage o HttpContext) possono essere passati, offrendo ai clienti un sacco di dati da usare.

Introduzione a DiagnosticSource

Questa procedura dettagliata illustra come creare un evento DiagnosticSource e instrumentare il codice con System.Diagnostics.DiagnosticSource. Spiega quindi come utilizzare l'evento trovando interessanti DiagnosticListeners, sottoscrivendo i relativi eventi e decodificando i payload dei dati degli eventi. Termina descrivendo il filtro, che consente solo a eventi specifici di passare attraverso il sistema.

Implementazione di DiagnosticSource

Si lavorerà con il codice seguente. Questo codice è una classe HttpClient con un metodo SendWebRequest che invia una richiesta HTTP all'URL e riceve una risposta.

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.
}

L'esecuzione dell'implementazione fornita viene stampata nella console.

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

Registrare un evento

Il tipo DiagnosticSource è una classe base astratta che definisce i metodi necessari per registrare gli eventi. La classe che contiene l'implementazione è DiagnosticListener. Il primo passaggio della strumentazione del codice con DiagnosticSource consiste nel creare un oggetto DiagnosticListener. Ad esempio:

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

Si noti che httpLogger viene digitato come DiagnosticSource. Questo perché questo codice scrive solo eventi e pertanto riguarda solo i metodi DiagnosticSource implementati da DiagnosticListener. A DiagnosticListeners vengono assegnati dei nomi al momento della creazione e questo nome deve essere il nome di un raggruppamento logico di eventi correlati (in genere il componente). Successivamente, questo nome viene usato per trovare il listener e sottoscrivere uno dei relativi eventi. Pertanto, i nomi degli eventi devono essere univoci solo all'interno di un componente.


L'interfaccia di registrazione DiagnosticSource è costituita da due metodi:

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

Si tratta di uno strumento specifico per il sito. È necessario controllare il sito di strumentazione per verificare quali tipi vengono passati a IsEnabled. In questo modo vengono fornite le informazioni per sapere a cosa eseguire il cast del payload.

Un sito di chiamata tipico sarà simile al seguente:

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

Ogni evento ha un nome string (ad esempio, RequestStart) ed esattamente un object come payload. Se è necessario inviare più di un elemento, è possibile farlo creando un object con proprietà per rappresentare tutte le relative informazioni. La funzionalità tipo anonimo di C# viene in genere usata per creare un tipo da passare "al volo", e rende questo schema molto pratico. Nel sito di strumentazione, è necessario proteggere la chiamata a Write() con un controllo IsEnabled() sullo stesso nome dell'evento. Senza questo controllo, anche quando la strumentazione è inattiva, le regole del linguaggio C# richiedono tutto il lavoro di creazione del payload object e la chiamata di Write() da eseguire, anche se nulla è effettivamente in ascolto dei dati. Sorvegliando la chiamata Write(), è possibile renderla efficiente quando l'origine non è abilitata.

Combinazione di tutti gli elementi disponibili:

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;
    }
}

Individuazione di DiagnosticListeners

Il primo passaggio per la ricezione di eventi consiste nell'individuare i DiagnosticListeners a cui si è interessati. DiagnosticListener supporta un modo per individuare DiagnosticListeners che sono attivi nel sistema in fase di esecuzione. L'API per eseguire questa operazione è la proprietà AllListeners.

Implementare una classe Observer<T> che eredita dall'interfaccia IObservable, ovvero la versione 'callback' dell'interfaccia IEnumerable. Per altre informazioni, vedere il sito di Reactive Extensions. Un oggetto IObserver ha tre callback, OnNext, OnComplete e OnError. Un oggetto IObservable ha un singolo metodo denominato Subscribe che viene passato a uno di questi osservatori. Dopo la connessione, l'Osservatore ottiene i callback (principalmente callback OnNext) quando si verificano le cose.

Un uso tipico della proprietà statica AllListeners è simile al seguente:

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.
}

Questo codice crea un delegato di callback e, usando il metodo AllListeners.Subscribe, richiede che il delegato venga chiamato per ogni DiagnosticListener attivo nel sistema. La decisione di sottoscrivere o meno il listener viene presa esaminando il suo nome. Il codice precedente cerca il listener "System.Net.Http" creato in precedenza.

Analogamente a tutte le chiamate a Subscribe(), questo restituisce un oggetto IDisposable che rappresenta la sottoscrizione stessa. I callback continueranno a verificarsi finché nulla chiama Dispose() su questo oggetto di sottoscrizione. L'esempio di codice non chiama mai Dispose(), quindi riceverà i callback per sempre.

Quando si sottoscrive AllListeners, si ottiene un callback per ALL ACTIVE DiagnosticListeners. Pertanto, al momento della sottoscrizione, si ottiene un flurry di callback per tutti i DiagnosticListeners esistenti e, man mano che ne vengono creati di nuovi, si riceve un callback anche per quelli. Si riceve un elenco completo di tutti gli elementi per cui è possibile effettuare la sottoscrizione.

Sottoscrivere DiagnosticListeners

Un elemento DiagnosticListener implementa l'interfaccia IObservable<KeyValuePair<string, object>>, in modo da poter chiamare anche Subscribe(). Il codice seguente illustra come compilare l'esempio precedente:

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);
}

In questo esempio, dopo aver trovato il DiagnosticListener "System.Net.Http", viene creata un'azione che stampa il nome del listener, l'evento e payload.ToString().

Nota

DiagnosticListener implementa IObservable<KeyValuePair<string, object>>. Ciò significa che in ogni callback si ottiene un oggetto KeyValuePair. La chiave di questa coppia è il nome dell'evento e il valore è il payload object. L'esempio registra semplicemente queste informazioni nella console.

È importante tenere traccia delle sottoscrizioni a DiagnosticListener. Nel codice precedente, la variabile networkSubscription memorizza questo valore. Se si forma un altro creation, è necessario annullare la sottoscrizione del listener precedente e sottoscrivere il nuovo listener.

Il codice DiagnosticSource/DiagnosticListener è thread-safe, ma anche il codice di callback deve essere thread-safe. Per assicurarsi che il codice di callback sia thread-safe, vengono usati dei blocchi. È possibile creare due DiagnosticListeners con lo stesso nome contemporaneamente. Per evitare race condition, gli aggiornamenti delle variabili condivise vengono eseguiti con la protezione di un blocco.

Dopo l'esecuzione del codice precedente, alla successiva esecuzione di un Write() su DiagnosticListener "System.Net.Http", le informazioni verranno registrate nella console.

Le sottoscrizioni sono indipendenti l'una dall'altra. Di conseguenza, un altro codice può eseguire esattamente la stessa operazione dell'esempio di codice e generare due "pipe" delle informazioni di registrazione.

Decodificare i payload

L'oggetto KeyvaluePair passato al callback ha il nome e il payload dell'evento, ma il payload viene digitato semplicemente come object. Esistono due modi per ottenere dati più specifici:

Se il payload è un tipo noto (ad esempio, stringo HttpMessageRequest), è sufficiente eseguire il cast del object al tipo previsto (usando l'operatore as in modo da non causare un'eccezione se si è sbagliato) e quindi accedere ai campi. Questa è un’operazione molto efficiente.

Usare l'API reflection. Si supponga, ad esempio, che sia presente il metodo seguente.

    /// 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);
        }
    }

Per decodificare il payload in modo più completo, è possibile sostituire la chiamata listener.Subscribe() con il codice seguente.

    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);
        }
    });

Si noti che l'uso della reflection è relativamente costoso. Tuttavia, l'uso della reflection è l'unica opzione se i payload sono stati generati usando tipi anonimi. Questo sovraccarico può essere ridotto rendendo più veloci e specializzati i recuperi di proprietà usando PropertyInfo.GetMethod.CreateDelegate() o spazio dei nomi System.Reflection.Emit, ma non rientra nell'ambito di questo articolo. (Per un esempio di recupero di proprietà rapido e basato su delegato, vedere Classe PropertySpec usata in DiagnosticSourceEventSource).

Filtro

Nell'esempio precedente il codice usa il metodo IObservable.Subscribe() per associare il callback. In questo modo tutti gli eventi vengono assegnati al callback. Tuttavia, DiagnosticListener dispone di overload di Subscribe() che consentono al controller di controllare quali eventi vengono specificati.

La chiamata listener.Subscribe() nell'esempio precedente può essere sostituita con il codice seguente da illustrare.

    // 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.

In questo modo si sottoscrivono in modo efficiente solo gli eventi 'RequestStart'. Tutti gli altri eventi causeranno la restituzione di false del metodo DiagnosticSource.IsEnabled() e pertanto verranno filtrati in modo efficiente.

Nota

L'applicazione di un filtro ha il solo scopo di ottimizzare le prestazioni. È possibile che un listener riceva eventi anche se non soddisfano il filtro. Questa situazione può verificarsi perché un altro listener ha sottoscritto l'evento o perché nell'origine dell'evento non è stato selezionato il parametro IsEnabled() prima di inviarlo. Se si vuole avere la certezza che un determinato evento soddisfi il filtro, sarà necessario selezionarlo all'interno del callback. Ad esempio:

    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());
            }
        };
Filtro basato sul contesto

Alcuni scenari richiedono filtri avanzati in base al contesto esteso. I producer possono chiamare overload DiagnosticSource.IsEnabled e fornire proprietà di evento aggiuntive, come illustrato nel codice seguente.

//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 });

Nell'esempio di codice seguente viene illustrato che i consumer possono usare tali proprietà per filtrare gli eventi in modo più preciso.

    // 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);

I produttori non sono a conoscenza del filtro fornito da un consumer. DiagnosticListener richiamerà il filtro fornito, omettendo argomenti aggiuntivi, se necessario, pertanto il filtro dovrebbe aspettarsi di ricevere un contesto null. Se un producer chiama IsEnabled() con nome di evento e contesto, tali chiamate vengono racchiuse in un overload che accetta solo il nome dell'evento. I consumer devono assicurarsi che il filtro consenta agli eventi senza contesto di passare.